Брюс Моли
Unix/Linux Теория и практика программирования
Перевод с английского
КУДИЦ-ОБРАЗ МОСКВА • 2004
ББК 32.973-018.2 Моли Б. Unix®/Linux: теория и практика программирдвания. Пер. с англ. - М.: КУДИЦ-ОБРАЗ, 2004. - 576 с.
Книга посвящена вопросам системного программирования в среде Unix. Излагае мый материал является общим для всех разновидностей систем Unix. Теоретиче ский материал сопровождается примерами реальных программ и большим коли чеством тем для обсуждения и самостоятельной разработки. Книга будет полезна прежде всего студентам, а также всем, кто программирует в среде Unix и хочет наилучшим образом использовать инструментальные возможности системы. ISBN 0-13-008396-8 ISBN 5-93378-087-1
Брюс Моли Unix®/Linux: теория и практика программирования Учебно-справочное издание________________________________ Корректор М. Матёкин Перевод с англ. В. Д. Никитин Научный редактор Л. И. Шустова Лицензия ЛР № 071806 от 02.03.99. НОУ «ОЦ КУДИЦ-ОБРАЗ». 119034, Москва, Гагаринский пер., д. 21, стр. 1. Тел.: 333-82-11,
[email protected] Подписано в печать 12.02.2004. Формат 70x100/16. Печать офсетная. Уел. печ. л. 46,4. Тираж 2000. Заказ 422^ Отпечатано с готовых диапозитивов в ООО «Типография НПО профсоюзов Профиздат», 109044, Москва, Крутицкий вал, 18.
ISBN 0-13-008396-8 ISBN 5-93378-087-1 © НОУ «ОЦ КУДИЦ-ОБРАЗ», 2004 Авторизованный перевод с англоязычного издания, озаглавленного UNDERSTANDING UNIX/LINUX PROGRAMMING, 1st Edition by MOLAY, BRUCE, опубликованного Pearson Education, Inc, под издатель ской маркой Prentice Hall, Copyright © 2003 by Pearson Education, Inc. All rights reserved. No part of this book may be reproduced or transmitted in any forms or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education Inc. Все права защищены. Никакая часть этой книги не может воспроизводиться или распространяться в любой форме или любыми средствами, электронными или механическими, включая фотографирование, магнитную запись или информационно-поисковые системы хранения информации без разрешения от Pearson Education, Inc Русское издание опубликовано издательством КУДИЦ-ОБРАЗ, © 2004
Предисловие Понимание Unix программирования
Что такое UNIX? Я написал эту книгу, чтобы объяснить, как работает Unix, и показать, как нужно писать системны^ программы для Unfac. Unix, развиваясь более тридцати лет, стал богаче, но ненамного сложнее. Длй нее все также остаются справедливыми фундаментальная струк тура и принципы проекта. Помимо того, что вам станут понятны структура, принципы и ис тория системы, вы можете читать, расширять и добавлять знания, касающиеся програм мирования в Unix, знания, которые рассредоточены в обширной литературе. Вам будет пред ставлена возможность и немного поразвлечься. Для того, чтобы донести суть идей я преподношу их в книге в различных формах: в форме картинок, используя аналогии, применяя псевдокод и реальный код, используя эксперименты, упражнения, и анекдоты. Эти объяснения и факты брались из реальных, полезных задач и проектов.
Кому будет полезна эта книга? Вы должны иметь навык программирования в С. Если Вы обладаете навыком работы в C++, то вы быстро адаптируетесь и будете отслеживать предлагаемые коды. Вы должны знать о мас сивах, структурах, указателях, связанных списках и должны понимать как использовать эти элементы при написании программных кодов. От вас не требуется знания особенностей использования Unix или знания внутренней структуры Unix. Каждую главу мы будем начинать с представления Unix с пользователь ского уровня. Вопрос “Что делает этот механизм?”, поставленный на пользовательском уровне, неизбежно приводит к вопросу системного уровня “Как это работает?” Вам нужно иметь доступ к системе Unix и подготовиться к тому, что потребуется иногда рисковать. >
Зачем это мне? Эта книга дает теоретическое представление о компонентах системы Unix с позиций, что они делают, дает теорию с позиций, как они работают, и как следует программировать, используя эти компоненты. Вы также увидите, как можно объединять все эти компоненты, чтобы получить понятную и ясную операционную систему.
6
Благодарности
Эта книга базируется на материале, курса лекций Системное Программирование в Unix, который я читал с 1990 в Harvard Extension School. Студенты, как при оценках курса, так и позже, по электронной почте писали, что дал им этот курс. Так один студент сообщил, что курс дал ему " ключи к королевству." Он понял Unix на пользовательском, програм мистском и теоретическом уровнях в достаточной степени для того, чтобы почувствовать все это вместе и применить в отношении большинства из возникающих проблем. Это на поминает подготовку врачей, когда студенты медики учатся работать с реальными пробле мами. Другой студент, один из тех , кто он поставил целью стать лидером проекта OSF (Open Software), сказал, что курс научил его идеям и позволил получить профессиональную под готовку, необходимую для этой работы.
В отношении какой версии Unix написана книга? Материал распространим по отношению большинства систем Unix, включая GNU/Linux. В книге внимание сосредоточено на структуре и подходах, из которых сформированы основы всех версий Unix. Изложение не фокусируется на специфичных отличиях между отдельными диалектами. Если были поняты основные идеи, то можно легко изучить и эти детали.
Благодарности Появление этой книги стало возможным благодаря помощи многих людей. Я благодарен Петра Рехтеру (Prentice-Hall) за предоставление возможности издания и руко водство проектом, а также благодарен Грегори Даллесу за работу со мной по иллюстрации книги, предложения этой возможности и для руководства Я благодарен рецензентам книги за их внимательную работу, за замечания, способст вующие улучшению книги, и конкретные предложения: Бену Абботу, Джону Б. Коннели, Геофу Сацлайфу, Луису Таберу, Сэму Р. Тангиаху и Лоуренсу Б.Уэлсу. Я благодарен Пегги Бастаманту и Амит Чаттержи за предоставление кардинальной информации о графиче ском программном обеспечении. Я благодарю Юрико Кувабара за несчетное число бесед, за моральную и практическую поддержку в этом проекте. Я благодарен тем многим студентам и преподавателям, которые были заняты в курсе Системное Программирование в Unix, чьи вопросы и замечания в аудиторных дискуссиях и при проведении консультаций помогли оформлению схем, объяснений, метафор и образов, использованных в этой книги. Особую благодарность выношу Ларри деЛюка, который работал в качестве ассистента по курсу, и за материал, который был изложен в главе 13.
Содержание Глава 1 Системное программирование в Unix. Общие представления
24
1.1. Введение ...........................................................................................................24 1.2. Что такое системное программирование?............................. .....................24 1.2.1. Простая модель программы................................................................... 24 1.2.2. Реальность ................ ................................................................................25 1.2.3. Роль операционной системы ................................................................. 26 1.2.4. Поддержка сервиса для программ ........................................................27 1.3. Понимание системного программирования...............................................28 1.3.1. Системные ресурсы................................................................................. 28 1.3.2. Наша цель: понимание системного программирования................... 29 1.3.3. Наш метод: три простых шага............................................................... 29 1.4. UNIX с позиций пользователя ... ..................................................................30 1.4.1. Что делает Unix? ...................................................................................... 30 1.4.2. Вхождение в систему — запуск программ — выход из системы 30 1.4.3. Работа с каталогами.................................................................................32 1.4.4. Работа с файлами............................................... ......................................34 1.5. Расширенное представление об UNIX......................................................... 36 1.5.1 Взаимодействие (связь) между людьми и программами.................... 36 1.5.2.Турниры по игре в бридж через Интернет...........................................37 1.5.3..Ьс: секреты настольного калькулятора в Unix................................... 38 1.5.4. От системы bc/dc к Web.......................................................................... 41 1.6. Могу ли я сделать то же самое? ................................................................... 41 1.7. Еще несколько вопросов и маршрутная карта..........................................49 1.7.1. О чем пойдет теперь речь?......................................................................49 1.7.2. А теперь - карта........................................................................................49 1.7.3 Что такое Unix? История и диалекты ................................................... 50 Заключение.............................................................................................................. 51
Глава 2 Пользователи, файлы и справочник. Что рассматривать в первую очередь?
52
2.1. Введение .................................... ...................................................................... 52 2.2. Вопросы, относящиеся к команде who........................................................ 53 2.2.1. Программы состоят из команд......................... .....................................53 2.3. Вопрос 1: Что делает команда who? ............................................................ 54 2.3.1. Обращение к справочнику......................................................................54 2.4 Вопрос 2: Как работает команда who?...........................................................56 2.4.1. Мы теперь знаем, как работает who......................................................60
8
2.5 Вопрос 3: Могу ли я написать who?..............................................................60 2.5.1. Вопрос: Как я буду читать структуры из файла? .............................. 61 2.5.2. Ответ: Использование open, read и close ............................................. 62 2.5.3. Написание программы whol.c ............................................................... 65 2.5.4.0тображение записей о вхождениях в систему.......................................65 2.5.5. Написание версии who2.c .......................................................................67 2.5.6. Взгляд назад и взгляд вперед................................................................. 72 2.6. Проект два: Разработка программы ср (чтение и запись)....................... 73 2.6.1. Вопрос 1: Что делает команда ср? ........................................................ 73 2.6.2. Вопрос 2: Как команда ср создает файл и как пишет в него? ..........73 2.6.3. Вопрос 3: Могу ли я написать программу ср?....................... :............74 2.6.4. Программирование в Unix кажется достаточно простым ................77 2.7. Увеличение эффективности файловых операций ввода/вывода: Буферирование................................................ .......................................................77 2.7.1. Какой размер буфера следует считать лучшим? ............................... 77 2.7.2 Почему на системные вызовы требуется тратить время?................. 78 2.7.3. Означает ли, что наша программа who2.c неэффективна?............... 79 2.7.4. Добавление буферирования к программе who2.c............................... 80 2^8. Буферизация и ядро ...................................................................................... 83 2.8.1. Если буферизация столь хороша, то почему ее не использует ядро? 83 2.9. Чтение файла и запись в файл ..................................................................... 84 2.9.1. Выход из системы: Что происходит? :.................................................. 84 2.9.2. Выход из системы: Как это происходит............................................... 85 2.9.3. Смещение текущего указателя: lseek.................................................... 86 2.9.4. Кодирование выхода из системы через терминал.............................. 87 2.10. Что делать с ошибками системных вызовов? .........................................88 Заключение................................................................................. ............................ 90 Исследования ..................................................................... ................................ 91 Программные упражнения................................................................................92 Проекты ...............................................................................................................93
Глава 3 Свойства каталогов и файлов при просмотре с помощью команды Is............................................................................ 95 3.1. Введение ...........................................................................................................95 3.2. Вопрос 1: Что делает команда Is?.................................................................96 3.2.1. Команда Is выводит список имен файлов и оповещает об атрибутах файлов................................................................................................................... 96 3.2.2. Получение листинга о других каталогах, получение информации о других файлах................................................................. ;................................ 96 3.2.3. Наиболее употребимые опции................................................................97 3.2.4. Первый ответ: Итоговые замечания ....................................................97 3.3. Краткий обзор дерева файловой системы.................................................. 97
Содержание
9
3.4. Вопрос 2: Как работает команда Is? ........................................................... 98 3.4.1. Что же такое каталог, в конце концов?................................................98 3.4.2. Работают ли системные вызовы open, read и close в отношении каталогов? ........................................................................................................... 99 3.4.3. Хорошо, хорошо. Но как же мне прочитать каталог?..................... 100 3.5. Вопрос 3: Могу ли я написать Is?...............................................................102 3.5.1. Что еще нужно делать? .........................................................................103 3.6. Проект 2: Написание версии Is -1...............................................................104 3.6.1. Вопрос 1: Что делает Is -1? ................................................................... 104 3.6.2. Вопрос 2: Как работает Is -1? ...............................................................105 3.6.3. Ответ: Системный вызов stat получает информацию о файле ...105 3.6.4. Какую еще информацию можно получить с помощью системного вызова stat?........................................................................................................ 106 3.6.5. Чего мы достигли?................. ........ .......................................................108 3.6.6. Преобразование числового значения поля mode в символьное значение.............................................................................................................. 108 3.6.7. Преобразования числового представления идентификаторов собственника/группы в строковое представление ..................................... 112 3.6.8. Объединение всего вместе: ls2.c .......................................................... 115 3.7. Три специальных разряда .......................................................................... 119 3.7.1. Разряд Set-User-ID ................................................................................. 119 3.7.2 Разряд Set-Group-ID................................................................................121 3.7.3 Разряд Sticky Bit.......................................................................................121 3.7.4. Специальные разряды и Is -1 .............................................................. 122 3.8. Итоги для команды Is ..................................................................................122 3.9. Установка и модификация свойств файла............................................... 123 3.9.1. Тип файла ............................................................................................... 123 3.9.2. Разряды прав доступа и специальные разряды................................ 123 3.9.3. Число ссылок на файл .......................................... ............................... 124 3.9.4. Собственник и группа для файла........................................................ 125 3.9.5. Размер файла...........................................................................................126 3.9.6. Время последней модификации и доступа......................................... 126 3.9.7. Имя файла.............................. .................................................................127 Заключение...................................................... .....................................................127 Исследования .................................................................................................... 128 Программные упражнёния..............................................................................130 Проекты .......................................... ..................................................................132
Глава 4 Изучение файловых систем. Разработка версии pwd.......... 133 4.1. Введение .........................................................................................................133 4.2. Пользовательский взгляд на файловую систему.................................... 134 4.2.1. Каталоги и файлы ................................................................................. 134 4.2.2. Команды для работы с каталогами ....................................................134
10
Содержание
4.2.3. Команды для работы с файлами......................................................... 135 4.2.4. Команды для работы с деревом........................................................... 136 4.2.5. Практически нет пределов на древовидную структуру...................137 4.2.6. Итоговые замечания по файловой системе Unix.............................. 137 4.3. Внутренняя структура файловой системы UNIX ...................................137 4.3.1. Абстракция 0: От пластин к разделам .............................................. 138 4.3.2. Абстракция 1: От плат к массиву блоков.......................................... 138 4.3.3. Абстракция 2: От массива блоков к дереву разделов ..................... 138 4.3.4. Файловая система с практических позиций: Создание файла ...139 4.3.5. Файловая система с практических позиций: Как работают каталоги.................................................................................... 141 4.3.6. Файловая система с практических позиций: Как работает команда cat........................................................................ '.......142 4.3.7 Inodes и большие файлы ............................................... :.......................143 4.3.8. Варианты файловых систем в Unix.....................................................145 4.4. Понимание каталогов .................................................................................145 4.4.1. Понимание структуры каталога..........................................................146 Реальное значение фразы “Каталог содержит подкаталоги” ...................148 4.4.2. Команды и системные вызовы для работы с деревьями каталогов . 149 4.5. Разработка программы pwd ........;........ .....................................................153 4.5.1. Как работает команда pwd? .. ..............................................................153. 4.5.2. Версия команды pwd .....................................,............ ......................... 154 4.6. Множественность файловых систем: Дерево из деревьев..................... 156 4.6.1 Точки монтирования ............................................................................. 157 4.6.2. Дублирование номеров Inode и связей между устройствами ....158 4.6.3. Символические ссылки: Панацея или блюдо спагетти?.................159 Заключение............................................................................................................ 160 Исследования ..................................................... .............................................. 161 Программные упражнения..............................................................................164 Проекты ............................................................................................................. 164
Глава 5 Управление соединениями. Изучение stty............................ 165 5.1. Программирование устройств ........... ....................................................... 166 5.2. Устройства подобны файлам...................................................................... 166 5.2.1. Устройства имеют имена файлов........................................................ 166 5.2.2. Устройства и системные вызовы ....................................................... 167 5.2.3. Пример: Терминалы аналогичны файлам ....................................... 167 5.2.4 Свойства файлов устройств.................................................................. 168 5.2.5. Разработка команды write.................................................................... 169 5.2.6. Файлы устройств и Inodes.................................................................... 170 5.3. Устройства не похожи на файлы................................................................171 5.3.1. Атрибуты соединения и контроль.......................................................172
Содержание
11
5.4. Атрибуты дисковых соединений................................................................173 5.4.1. Атрибут 1: Буферизация ...................................... ............................... 173 5.4.2. Атрибут 2: Режим Auto-Append .......................................................... 174 5.4.3. Управление файловыми дескрипторами с помощью системного вызова open........................................................................................................ 177 5.4.4. Итоговые замечания о дисковых соединениях................................. 178 5.5. Атрибуты терминальных соединений ......................................................178 5.5.1. Терминальный ввод/вывод не такой, как он кажется ....................178 5.5.2. Драйвер терминала................................................................................180 5.5.3. Команда stty............................................................................................ 181 5.5.4. Программирование драйвера терминала: Установки ....................182 5.5.5. Программирование драйвера терминала: Функции....................... 182 5.5.6. Программирование драйвера терминалов: Флаги.......................... 184 5.5.7. Программирование драйвера терминала: Примеры программ ..186 5.5.8. Итоговые замечания по соединениям с терминалами ....................189 5.6. Программирование других устройств: iocti............................................. 190 5.7. О небо! Это файл, это устройство, это поток! ..........................................190 Заключение.................................................... ....................................................... 191 Исследования .................................................................................................... 193 Программные упражнения............................................................................. 195 Проекты ............................................................................................................. 197
Глава 6 Программирование дружественного способа управления терминалом и сигналы..................................................................................................... 198 6.1. Инструментальные программные средства ............................................198 6.2. Режимы работы драйвера терминала....................................................... 200 6.2.1. Канонический режим: Буферизация и редактирование..................200 6.2.2. Неканоническая обработка.................................................................. 202 6.2.3. Итоговые замечания по режимам терминала................................... 203 6.3. Написание пользовательской программы: play_again.c........................ 204 6.3.1. Неблокируемый ввод: play_again3.c ...................................................210 6.4. Сигналы ........................................................................................................ 214 6.4.1. Что делает управляющая последовательность Ctrl-C ....................215 6.4.2. Что такое сигнал? ................................................................................. 215 6.4.3. Что может процесс сделать с сигналом?............................................ 217 6.4.4. Пример обработчика сигнала ............................................................. 218 6.5 Подготовка к обработке сигналов: play_again4.c .................................... 221 6.6. Процессы смертны ...................................................................................... 223 6.7. Программирование для устройств ... ............................ ........................... 223 Заключение............................................................................................................224 Исследования ..................................................................................... .............. 224 Программные упражнения..............................................................................225
12
Содержание
Глава 7 Событийно-ориентированное программирование. Разработка видеоигры...........................................................................228 7.1. Видеоигры и операционные системы ............. .........................................228 7.2 Проект: Разработка pong-игры в настольный теннис для одного игрока .................................. ....................... ......................................231 7.3. Программирование пространства: Библиотека curses ..........................231 7.3.1. Введение в curses ................................................................................... 231 7.3.2. Внутренняя архитектура curses: Виртуальный и реальный экраны 234 7.4. Программирование времени: sleep ........................................................... 235 7.5. Программирование времени 1: ALARMS ................................................238 7.5.1. Добавление задержки: sleep.................................................................. 238 7.5.2. Как работает sleep(): Использование alarms в Unix..........................238 7.5.3. Планирование действий на будущее .................................................. 241 7.6. Программирование времени II: Интервальные таймеры..................... 241 7.6.1. Добавление улучшеной задержки: usleep........................................... 241 7.6.2. Три вида таймеров: реальные, процессные и профильные ...........241 7.6.3. Два вида интервалов: начальный и период...................................... 242 7.6.4 Программирование с помощью интервальных таймеров............... 243 7.6.5. Сколько часов можно иметь на компьютере?.................................. 246 7.6.6. Итоговые замечания по таймерам...................................................... 248 7.7. Управление сигналами I: Использование signal .....................................248 7.7.1. Управление сигналами в старом стиле.............................................. 248 7.7.2. Управление множеством сигналов .....................................................249 7.7.3. Тестирование множества сигналов..................................................... 251 7.7.4. Слабые места схемы управления множеством сигналов................ 253 7.8. Управление сигналами II: sigaction........................................................... 254 7.8.1. Управление сигналами: sigaction ........................................................254 7.8.2. Заключительные замечания по сигналам ........................................ 257 7.9. Предотвращение искажений данных........................................................ 257 7.9.1. Примеры, иллюстрирующие искажение данных............................. 258 7.9.2. Критические секции ............................................................................. 258 7.9.3. Блокирование сигналов: sigprocmask и sigsetops..............................259 7.9.4. Повторно входной код: Опасность рекурсии.................................... 260 7.9.5. Критические секции в видеоиграх...................................................... 261 7.10. kill: Посылка сигнала процессом............................................................. 261 7.11. Использование таймеров и сигналов: видеоигры.................................262 7.11.1. bounceld.c: Управляемая анимация на строке................................263 7.11.2. bounce2d.c: Двухмерная анимация....................................................266 7.11.3. Вся игра целиком.................................................................................271 7.12. Сигналы при вводе: Асинхронный ввод/вывод....................................271 7.12.1. Организация перемещения с помощью асинхронного ввода/вывода ............................... ;....................................................................272 7.12.2 Метод 1: Использование 0_ASYNC....................................................272
Содержание
13
7.12.3. Метод 2: Использование aio_read .....................................................274 7.12.4. А нужно ли нам производить асинхронное чтение для организации перемещения?............................................................................ 277 7.12.5. Асинхронный ввод, видеоигры и операционные системы........... 277 Заключение............................................................................................................278 Исследования .................................................................................................... 278 Программные упражнения..............................................................................280 Проекты ............................................................. ............................................... 282
Глава 8 Процессы и программы. Изучение sh........................................................... 283 8.1. Процессы = программы в исполнении ......;.............................................. 283 8.2. Изучение процессов с помощью команды ps........................................... 284 8.2.1. Системные процессы............................................................................. 286 8.2.2. Управление процессами и управление файлами.............................. 287 8.2.3. Память компьютера и память для программ .................................. 288 8.3. SHELL: Инструмент для управления процессами и программами .289 8.4. Как SHELL запускает программы на исполнение..................................290 8.4.1. Основной цикл shell............................................................................... 290 8.4.2. Вопрос 1: Каким образом производится запуск программы? ....292 8.4.3. Вопрос 2: Как получить новый процесс?........................................... 296 8.4.4. Вопрос 3: Как процесс-отец ожидает окончания дочернего процесса? ........................................................................................................... 300 8.4.5. Итог: Как Shell запускает программы на исполнение..................... 306 8.5. Создание shell: psh2.c ................................................................................... 307 8.5.1. Сигналы и psh2.c.................................................................................... 310 8.6. Защита: программирование процессов.....................................................311 8.7. Дополнение относительно EXIT и ЕХЕС..................................................312 8.7.1. Окончание процесса: exit и _exit.......................................................... 312 8.7.2. Семейство вызовов ехес........................................................................ 313 Заключение............................................................................................................314 Исследования .................................................................................................... 315 Программные упражнения..............................................................................317
Глава 9 Программируемый shell. Переменные и среда shell....................................318 9.1. Программирование в среде SHELL ................................... .......................318 9.2. SHELL-скрипты: что это такое и зачем?..................................................319 9.2.1. Shell скрипт - это пакет команд........................................................... 319 9.3. smshl-Разбор текста командной строки ........................................ ........... 321 9.3.1. Замечания относительно smshl ........................................................... 328 9.4. Поток управления в SHELL: почему и как?............................................328 9.4.1. Что делает if?...........................................................................................328 9.4.2. Как работает if........................................................................................ 329 9.4.3. Добавление if к smsh ..............................................................................330 9.4.4. smsh2.c: Модифицированный код....................................... ............... 331
14
Содержание
9.5. SHELL-переменные: локальные и глобальные...................................... 336 9.5.1. Использование переменных shell ...J.................................................. 337 9.5.2. Система памяти для переменных ........................................ :............. 338 9.5.3. Команды для добавления переменных: встроенные команды ...339 9.5.4. Как все работает?................................................................................... 341 9.6. Среда: персонализированные установки................................................. 342 9.6.1. Использование среды ........................................................................... 343 9.6.2. Что собой представляет среда? Как она работает? ..........................344 9.6.3. Добавления средств по управлению средой в smsh.......................... 346 9.6.4. Код varlib.c...............................................................................................349 9.7. Общие замечания о SHELL ........................................................................353 Заключение............................................................... •...........................................353 Исследования ............... .................................................................................... 354 Программные упражнения .;...........................................................................354
Глава 10 Перенаправление ввода/вывода и программные каналы .....356 10.1. SHELL-программирование ......................................................................356 10.2. Приложение SHELL: наблюдение за пользователями.........................357 10.3. Сущность стандартного ввода/вывода и перенаправления................ 359 10.3.1. Фактор 1: Три стандартных файловых дескриптора.................... 359 10.3.2. Соединения по умолчанию: терминал............................................. 360 10.3.3. Вывод происходит только на stdout.................................................. 360 10.3.4. Shell, отсутствие программы, перенаправление ввода/вывода 360 10.3.5. Соглашения по перенаправлению ввода/вывода...........................362 10.3.6. Фактор 2: Принцип “Первый доступный,самый малый по значению дескриптор” ............................................................................... 362 10.3.7. Синтез ....................................................................................................363 10.4. Каким образом можно подключить stdin к файлу................................ 363 10.4.1. Метод 1: Закрыть, а затем открыть........ ......................................... 363 10.4.2. Метод 2: open..close..dup..close............................................................ 365 10.4.3. Обобщенная информация о системном вызове dup ......................367 10.4.4. Метод 3: open..dup2..close.................................................................... 368 10.4.5. Shell перенаправляет stdin не для себя, а для других программ ... 368 10.5. Перенаправление ввода/вывода для других программ: who > userlist 368 10.5.1. Итоговые замечания по перенаправлению стандартных потоков в файлы...............................................................................................................372 10.6. Программирование программных каналов ..........................................372 10.6.1. Создание программного канала ....................................................... 373 10.6.2. Использование fork для разделения программного канала......... 375 10.6.3. Финал: Использование pipe, fork и ехес...........................................377 10.6.4. Технические детали: Программные каналы не являются файлами.......................................................................................379
Содержание
15
Заключение...........................................................................................................381 Основные идеи...................................................................................................381 Исследования ....................................................................................................381 Программные упражнения..............................................................................382
Глава 11
Соединение между локальными и удаленными процессами. Серверы и сокеты..............................................................................................................384 11.1. Продукты и сервисы.................................................................................. 385 11.2. Вводная метафора: интерфейс автомата для получения напитка ..385 11.3. Ьс: калькулятор в UNIX .:.........................................................................386 11.3.1. Кодирование be: pipe, fork, dup, ехес.................................................388 11.3.2. Замечания, касающиеся сопрограмм.......................... .....:...............39Г 11.3.3. fdopen: файловые дескрипторы становятся похожими на файлы . 392 11.4. рореп: делает процессы похожими на файлы .......................................392 11.4.1. Что делает функция рореп ........................................................ ........392 11.4.2. Разработка функции рореп: использование fdopen .......................394 11.4.3. Доступ к данным: файлы, программный интерфейс API и сервера '........................................................................................................... 396 11.5. Сокеты: соединения с удаленными процессами................................... 397 11.5.1. Аналогия: “....время равно...” ........................................................... 397 11.5.2. Время Internet, DAP и метеорологические серверы.......................401 11.5.3. Списки сервисов: широко известные порты.................................. 402 11.5.4. Разработка timeserv.c: сервер времени ............................................ 403 11.5.5. Проверка работы программы timeserv.c..........................................407 11.5.6 Разработка программы timeclnt.c: клиент времени........................ 408 11.5.7. Проверка работы программы timeclnt.c.......................................... 410 11.5.8. Другие серверы: удаленный Is.......................................................... 411 11.6. Программные демоны .............................................................................. 416 Заключение............................................................................................................416 Исследования .................................................................................................... 417 Программные упражнения..............................................................................417
Глава 12
Соединения и протоколы. Разработка Web-cepeepa.............................421 12.1. В центре внимания - сервер ..................................................................... 421 12.2. Три основные операции ............................................................................422 12.3. Операции 1 и 2: установление соединения ............................................422 12.3.1. Операция 1: установка сокета на сервере........................................422 12.3.2. Операция 2: соединение с сервером ................................................. 423 12.3.3. socklib.c ................................................................................................. 424 12.4. Операция 3: взаимодействие между клиентом и сервером ................ 425 12.4.1. timeserv/timeclnt, использующие socklib.c........................................ 426 12.4.2. Вторая версия сервера: использование fork....................................427
16
Содержание
12.4.3. Вопрос по ходу проектирования: делать самому и делегировать работу другому?.................................................................................................428 12.5. Написание Web-сервера............................................................................ 430 12.5.1. Что делает Web-сервер........................................................................430 12.5.2. Планирование работы нашего Web-cepeepa................................... 431 12.5.3. Протокол Web-cepeepa........................................................................ 431 12.5.4. Написание Web-cepeepa...................................................................... 433 12.5.5. Запуск Web-cepeepa............................................................................. 435 12.5.6. Исходный крд webserv ........................................................................436 12.5.7. Сравнение Web-серверов....................................................................440 Заключение............................................................................................................440 Исследования ................................................................................................ ....441 Программные упражнения..............................................................................441 Проекты ............................................................................................................. 442
Глава 13 Программирование с использованием дейтаграмм. Лицензионный сервер............................................................................443 13.1. Программный контроль ...........................................................................444 13.2. Краткая история лицензионного контроля........................................... 445 13.3. Пример, не связанный с компьютерами: управление использованием автомобилей в компании.....................................................................................445 13.3.1. Описание системы управления ключами от автомобилей ..........446 13.3.2. Управление автомобилями в терминах модели клиент/сервер 446 13.4. Управление лицензией....................................... .'.....................................447 13.4.1. Система лицензионного сервера: что делает сервер? ................... 447 13.4.2. Система лицензионного сервера: как работает сервер? ...............448 13.4.3. Коммуникационная система.............................................................. 450 13.5.Сокеты дейтаграмм.....................................................................................450 13.5.1 Потоки (streams) и дейтаграммы........................................................450 13.5.2. Программирование дейтаграмм .......................................................452 13.5.3. Обобщение информации о sendto и recvfrom.................................. 457 13.5.4. Ответ на принятые дейтаграммы .................................................... 458 13.5.5. Итог по теме дейтаграмм.................................................................... 459 13.6..Лицензионный сервер. Версия 1.0 .......................................................... 460 13.6.1. Клиент. Версия 1 ................................................................................. 461 13.6.2. Сервер. Версия 1 ..................................................................................465 13.6.3. Тестирование Версии 1 .......................................................................469 13.6.4. Что еще нужно сделать?......................................................................470 13.7. Программирование с учетом существующих реалий ..........................470 13.7.1. Управление авариями в клиенте ......................................................470 13.7.2. Управление при возникновении аварийных сшуаций на сервере 473 13.7.3. Тестирование версии 2........................................................................ 476
Содержание
,17
13.8. Распределенные лицензионные сервера.................................................478 13.9. UNIX-сокеты доменов................................................................................480 13.9.1. Имена файлов, как адреса сокетов................................................... 480 13.9.2. Программирование с использование сокетов доменов................. 481 13.10. Итог: сокеты и сервера..........................................1................................483 Заключение............................................................................................................484 Исследования ....................................................................................................484 Программные Упражнения.............................................................................486 Проекты .............................................................................................................487
Глава 14 Нити. Параллельные функции............................................488 14.1. Одновременное выполнение нескольких нитей ...................................488 14.2. Нить исполнения........................................................................................ 489 14.2.1. Однонитьевая программа.................................................................. 489 14.2.2. Мультинитьевая программа..............................................................491 14.2.3 Обобщенная информация о функции pthread create...................... 493 14.3. Взаимодействие нитей .............................................................................. 494 14.3.1. Пример 1: incrprint.c ...........................................................................494 14.3.2. Пример 2: twordcount.c....................................................................... 495 14.3.3. Взаимодействие между нитями: итог............................................... 502 14.4. Сравнение нитей с процессами ............................................................... 503 14.5. Уведомление для нитей............................................................................. 504 14.5.1. Уведомление для центральной комиссии о результатах выборов .505 14.5.2. Программирование с использованием условных переменных 506 14.5.3. Функции для работы с условными переменными......................... 510 14.5.4. Обратимся опять к Web......................................................................510 14.6. Web-сервер, который использует механизм нитей ..............................511 14.6.1 Изменения в нашем Web-cepeepe ...................................................... 511 14.6.2. При использовании нитей появляются новые возможности ....511 14.6.3 Предотвращение появления зомби для нитей: отсоединение нитей...........................................................................................511 14.6.4. Код................. ........................................................................................ 512 14.7. Нити и анимация........................................................................................516 14.7.1. Преимущества нитей...........................................................................517 14.7.2 Программа bounceld.c, построенная с использованием нитей .518 14.7.3. Множественная анимация: tanimate.c..............................................519 14.7.4. Mutexes и tanimate.c............................................................................. 523 14.7.5. Нить для curses.....................................................................................524 Заключение............................................................................................................525 Исследования .......•........................................................................................... 526 Программные упражнения............................................................................. 526
18
Содержание
Глава 15 Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
529
15.1 Выбор при программиррвании................................................................. 530 15.2. Команда talk: Чтение многих входов...................................................... 530 15.2.1. Чтение из двух файловых дескрипторов.................................... :....531 15.2.2. Системный вызов select...................................................................... 532 15.2.3. select и talk............................................................................................. 535 15.2.4. select или poll......................................................................................... 535 15.3. Выбор соединения ......................................................................................535 15.3.1. Одна проблема и три ее решения...................................................... 535 15.3.2. Механизм IPC на основе использования файлов .......................... 536 •15.3.3. Именованные программные каналы................................................ 537 15.3.4. Разделяемая память ....................................................... ,...................539 15.3.5. Сравнение методов коммуникации.................................................. 541 15.4. Взаимодействие и координация процессов............................................ 543 15.4.1. Блокировки файлов.............................................................................543 15.4.2. Семафоры .............................................................................................546 15.4.3. Сравнение сокетов и каналов FIFO с разделяемой памятью ...554 15.5. Спулер печати ............................................................................................ 554 15.5.1. Несколько писателей, один читатель............................................... 554 15.5.2. Модель клиент/сервер......................................................................... 556 15.6. Обзор средств IPC................ ...................................................................... 557 15.7. Соединения и игры .................................................................................... 560 Заключение............................................................................................................561 Исследования .................................................. ...............!................................. 562 Программные упражнения..............................................................................562
Предметный указатель..........................................................................563
Список иллюстраций 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 1.17 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 3.1 3.2 3.3 3.4 3.5 3.6 3.7 4.1 4.2 4.3 4.4 4.5 4.6 4.7
Прикладная программа в компьютере............................................................................25 Как прикладные программы рассматривают пользовательский ввод/вывод ..............25 Реальность: много пользователей, программ и устройств............................................ 26 Как все это соединено?.................................................................................................... 26 Операционная система - это программа ........................................................................ 27 Ядро управляет всеми соединениями ............................................................................ 27 Вхождение пользователя в систему ...............................................................................31 Часть дерева каталогов.................................................................................................... 32 Четыре человека играют в бридж через Интернет ........................................................37 Стол для бриджа на серверном компьютере.................................................................. 37 Отдельные программы посылают сообщения друг другу ............................................38 Программы посылают сообщения друг другу .............................................................. 40 Отдельные программы посылают сообщения друг другу ............................................41 more читает со стандартного ввода.................................................................................45 Программа who читает пользовательский ввод с терминала ....................................... 46 Соединение с терминалом имеет настройки..................................................................48 Диаграмма основной структуры системы Unix ............................................................ 49 Пользователи, файлы, процессы и ядро ..................................................;..................... 53 Поток данных для команды who ....................................................................................60 Дескриптор файла - это соединение с файлом...............................................................63 Копирование файлов посредством чтения и записи......................................................75 Поток управления при работе системных вызовов ...................................................... 78 Поток управления при работе системных вызовов ......................................;........... . 80 Буферизация дисковых данных в ядре..................................................... ..................... 83 Каждый открытый файл имеет текущий указатель....................................................... 86 Дерево каталогов..............................................................................................................98 Чтение содержимого каталога.......................................................................................101 Чтение статусной информации о файле с помощью stat............................................. 105 Представление кодов типа файла и прав доступа........................................................108 Преобразования десятичного представления в двоичное........................................... 109 Использование двоичной маски....................................................................................110 Диск содержит файлы, каталоги и статусную информацию о них............................ 128 Дерево каталогов............................................................................................................134 Две связи к одному и тому же файлу............................................................................136 Нумерация дисковых блоков ........................................................................................138 Три области файловойгсистемы................................................................................... 139 Внутренняя структура файла........................................................................................ 140 От имени файла к дисковым блокам............................................................................ 142 Список распределения блоков содержится в области данных................................... 144
20
Список иллюстраций
4.8 Две точки зрения относительно дерева каталогов...................................... .................146 4.9 Имена файлов и указатели на файлы ........................................................................... 147 4.10 Имена каталогов и указатели на каталоги........................................... .........................147 4.11 Перемещение файла в новый каталог........................................................................... 151 4.12 Составление пути текущего каталога........................................................................... 153 4.13 ’’Прививка” деревьев .................................................................................................... 157 4.14 Номера inode и файловые системы .............................................................................. 158 4.15 Inodes, блоки данных, каталоги, указатели ..................................................................161 .5.1 Inode ссылается на блоки данных или на код драйвера.................................................... 171 5.2 Процесс с двумя файловыми дескрипторами........................................................... 172 5.3 Обрабатывающее устройство в потоке данных............................................... ............173 5.4 Модификация действия файлового дескриптора......................................................... 173 5.5 Присоединение записей с помощью lseek и write........................................................ 175 5.6 Чередующиеся lseek и write = хаос ...............................................................................175 5.7 Соединения с файлами имеют установки.....................................................................178 5.8 Соединения с файлами имеют установки.....................................................................178 5.9 Ядро обрабатывает данные терминала......................................................................... 180 5.10 Драйвер терминала является частью ядра...................:................................................ 180 5.11 Управление драйвером терминала с помощью tcgetattr и tcsetattr.................... ......... 183 5.12 Разряды и символы в составе членов termios............................................................... 185 5.13 Файловые дескрипторы, соединения и драйверы........................................................ 192 6.1 Три стандартных файловых дескриптора..................................................................... 199 6.2 То, что вы набираете, и то, что получает программа................................................... 201 6.3 Обрабатывающие уровни в драйвере терминала......................................................... 202 6.4 Основные компоненты драйвера терминала................................................................ 204 6.5 Ctrl-C убивает процесс исполнения программы. Программа заканчивается без восстановления ........................................................................................................214 6.6 Как работает Ctrl-C ........................................................................................................215 6.7 Три источника сигналов.................................................................................................216 6.8 По сигналу происходит обращение к подпрограмме...................................................219 6.9 Действие от выполнения вызова signal(SIGINT, SIGJK3N) ....................................... 220 7.1 Видеоигра для одного игрока........................................................................................ 231 7.2 Curses представляет экран в виде сетки........................................................................232 7.3 Наша первая программа с использованием curses.......... ........................................... . 233 7.4 Curses поддерживает копию реального экрана............................................................ 234 7.5 Изображение медленно перемещается вниз по экрану................................................236 7.6 Сообщение движется вперед и назад............................................................................ 237 7.7 Процесс устанавливает alarm, в течение которого он приостанавливает свое развитие.................................................................................................................. 238 7.8 Поток управления в обработчике сигнала....................................................................240 7.9 Каждый процесс имеет три таймера ............................................................................ 241 7.10 Как распределяются действия во времени? .................................................................242 7.11 Чтение и запись установок для таймера............................... ........................................ 243 7.12 Внутреннее представление интервальных таймеров................................................... 245
Список иллюстраций
7.13 7.14 7.15 7.16 7.17 7.18 7.19 7.20
21
Секунды и микросекунды ...................................................................................... *....246 Два таймера, одни часы................................................................................................. 247 Процесс принимает несколько сигналов .....................................................................250 Прохождение потока управления через эти функции................................................. 252 Процесс для посылки сигнала использует kill() ..........................................................261 Сложное использование сигналов................................................................................ 262 bounce Id в действии: анимация, управляемая пользователем .................:................ 263 Изменение значений через пользовательский ввод. Значения управляют действием..................... .............................. ;.............................. 264 7.21 Двухмерная анимация ...................................................................................................266 7.22 Траектория^дод углом 1/3 ................. ............................ ..............................................267 7.23 Перемещение по наклонной на один шаг за такт выглядит лучше ........................... 268 7.24 Сигналы поступают от клавиатуры и таймера.............................................................272 8.1 Процессы и программы ........... .....................................................................................284 8.2 Команда ps выводит список текущих процессов.........................................................284 8.3 Три модели памяти в компьютере....................................................... ........................ 288 8.4 Пользователь обращается к shell для выполнения запуска программы..................... 290 8.5 Распределение во времени основного цикла shell ...................................................... 291 8.6 execvp копирует программу в память и запускает ее на исполнение.........................292 8.7 Построение однострокового списка аргументов ........................................................ 295 8.8 fork() выполняет копирование процесса.......................................................................297 8.9 Дочерний процесс исполняет код после fork()............................................................ 298 8.10 Вызов wait переводит порождающий процесс в ожидание, пока не закончится дочерний процесс..........................................................................301 8.11 Управляющий поток и коммуникация с wait() ........................................................... 302 8.12 Представление статусной информации о дочернем процессе в трех полях.............. 304 .8.13 Последовательность шагов в цикле shell с выполнением fork(), ехес(), wait() .... 306 8.14 Логика shell в Unix................... ......................................................................................307 8.15 Сигналы от клавиатуры поступают на все присоединенные процессы .................... 310 8.16 Вызов функций и вызов программ...................... .........................................................312 9.1 Shell с сигналами, exit и разбором командной строки ................................................322 9.2 Добавление потока управления командами в smsh............................ *........'........'.....330 9.3 Скрипт, состоящий из различных областей ................................................................ 331 9.4 Система памяти для переменных shell......................................................................... 338 9.5 Добавление к smsh встроенных команд.......................................................................339 9.6 Среда - это массив указателей на строки..................................................................... 344 9.7 Строки из среды копируются при выполнении ехес() ................... ............................345 9.8 Копирование значений из среды в переменную vartab............................................... 346 9.9 Копирование значений из vartab в новую среду.......................................................... 347 9.10 Добавление средств управления средой в smsh...........................................................348 10.1 Соединение вывода команды who со входом команды sort .......................................357 10.2 Команда comm сравнивает два списка и выводит три набора строк..........................358 10.3 Программное средство читает входные данные и записывает результаты и сообщения об ошибках.............................. ................\.............................................. 359
22
10.4 10.5 10.6 10.7 10.8 10.9 10.10 10.11 10.12 10.13 10.14 10.15 10.16 10.17 10.18 10.19 10.20 10.21 11.1 11.2 11.3 11.4 11.5 11.6 11.7 11.8 11.9 11.10 11.11 12.1 12.2 12.3 12.4 12.5 12.6 13.1 13.2 13.3 13.4 13.5 13.6 13.7
Список иллюстраций
Три специальных файловых дескриптора.................................................................... 360 Принцип “Первый доступный, самый малый по значению дескриптор” ................. 362 Типичная начальная конфигурация ............................................................................. 363 Теперь stdin закрыт........................................................................................................ 364 Теперь stdin присоединен к файлу ............................................................................... 364 Использование dup для перенаправления.................................................................... 366 Shell перенаправляет вывод у дочернего процесса .....................................................368 Процесс имеет стандартный вывод и готов выполнить fork.......................................369 Стандартный вывод дочернего процесса был скопирован от процесса-отца............ 369 Дочерний процесс может закрыть свой стандартный вывод .....................................370 Дочерний процесс открывает новый файл и получает в результате fd = 1 ............... 370 Дочерний процесс запускает на исполнение программу с новым стандартным выводом.......................................................................................................................... 371 Два процесса соединены с помощью программного канала.......................................372 Программный канал.......................................................................................................373 Процесс создает программный канал .......................................................................... 374 Поток данных в программе pipedemo.c ....................................................................... 375 Разделение программного канала................................................................................. 376 Поток данных между процессами.................................................................................376 Напиток, который готовится сейчас или заранее?.................................................... 385 Один интерфейс и разные источники .......................................................................... 386 Ьс и dc, работающие как сопрограммы.........................................................................387 be, dc и ядро ............................................................................................................. . 388 fopen и рореп...................................................................................................................393 Чтение из команды.........................................................................................................394 Соединение с удаленным процессом............................................................................397 Служба времени ............................................................................................................ 398 Процессы на различных машинах.................................................................................410 Система remote Is ........................................................................... ...............................411 Использование рореп ("Is") для получения списка файлов из удаленных каталогов .413 Основные компоненты схемы клиент/серверного взаимодействия .......................... 422 Создание сокета на сервере........................................................................................... 423 Соединение с сервером ...................................... .......................................................... 423 Сервер и клиент для службы времени (версия 1) ........................................................426 Сервер выполняет fork для запуска программы date................................................... 427 Web-сервер обеспечивает удаленное выполнение Is, cat, ехес................................... 430 Лицензионный сервер дает разрешение ...................................................................... 444 Управление доступом к автомобилям ......................................................................... 446 Управление доступом к программному обеспечению.............. ..................................448 Передача данных с помощью пакетов в Internet.......................................................... 450 Коммуникации можно устанавливать либо с помощью соединения, либо без соединения.......................................................................................................451 Три составные части дейтаграммы .............................................................................. 452 Использование sendto и recvfrom................................................................................... 453
Список иллюстраций
13.8 13.9 13.10 13.11 13.12 13.13 13.14 13.15 14.1 14.2 14.3 14.4 14.5 14.6 14.7 14.8 14.9 14.10 14.11 14.12 14.13 15.1 15.2 15.3 15.4 15.5 15.6 15.7 15.8 15.9 15.10 15.11
23
Клиент уносит билет с собой в могилу.................................. ......................................470 Использование alarm для планирования процедуры восстановления билетов ... 471 Сервер повторно стартует после своего краха............................................................. 474 Клиент проверяет легальность билета..........................................................................474 Клиент проверяет легальность вначале и далее...........................................................475 Идентификаторы процессов (PIDs) не являются уникальными в сети ..................... 478 Процесс не может послать сигнал на другой хост.......................................................479 Работают локальные копии lserv ..................................................................................479 Единственная нить исполнения.................................................................................... 490 Несколько нитей исполнения ....................................................................................... 491 Две нити разделяют глобальную переменную.............................................................495 Общий счетчик для двух нитей ....................................................................................496 Две нити инкрементируют один и тот же счетчик...................................................... 497 Две нити используют mutex для разделения счетчика................................................ 499 Каждая нить имеет указатель на собственную структуру......................................502 Использование почтового ящика с замком для передачи данных..............................505 Использование блокируемой переменной для передачи данных .............................. 507 Анимируемое изображение и управление с помощью клавиатуры .......................... 517 Нить анимации и нить клавиатуры ..............................................................................517 Множество сообщений с изменением направления.................................................... 520 Отдельная нить взаимодействует с curses ................................................................... 524 Команда talk при работе в сети..................................................................................... 530 Команда talk....................................................................................................................531 Три файловых дескриптора........................................................................................... 535 У одного процесса есть информация, которая необходима другому процессу ... 536 Три пути для передачи данных..................................................................................... 536 Каналы FIFO являются независимыми от процессов .................................................537 Два процесса разделяют блок памяти......................................................................... 539 Семафорный набор: num_readers.................................................................................. 547 Несколько источников данных, один принтер.............................................................554 Получение файла для принтера.....................................................................................555 Клиент/серверная система печати.................................................................................556
Глава 1 Системное программирование в Unix. Общие представления
ЦЕЛИ Идеи Система Unix содержит пользовательские программы и системное ядро. Ядро Unix - это набор специальных подсистем. Ядро управляет всеми программами и организует доступ к ресурсам. Коммуникации между процессами являются важнейшим аспектом для Unix - программ. Что такое системное программирование?
Команды bc more
1.1. Введение Что такое системное программирование? Что такое системное программирование в Unix? Что мы будем рассматривать в этой книге? В этой главе мы, образно говоря, нарисуем об щую картину в соответствии с поставленными вопросами. Начнем с выяснения роли операционной системы и определения того, что означает про цесс написания программ, которые работают непосредственно с операционной системой. После краткого представления общих положений мы рассмотрим Unix - программы, которые используют услуги операционной системы, и затем перейдем к написанию наших собственных версий программ. Наконец, мы рассмотрим схему, на которой представлен Unix - компьютер. Схематические представления и техника раскрытия сути программ со ставляют основу этой книги.
1.2. Что такое системное программирование? 1.2.1. Простая модель программы Возможно, вы писали научные программы, либо финансовые программы, либо графиче ские программы, либо программы текстовой обработки. Существуют много разновидно стей программ. Большая часть программ строится в соответствии с моделью, представ ленной на рисунке 1.1.
1.2. Что такое системное программирование?
Компьютер
/
25
Программа
1
Рисунок 1.1 Прикладная программа в компьютере Программа - это некоторый код, который исполняется на компьютере. Данные.поступают на вход программы, программа выполняет некоторую обработку данных, и резуль тирующие данные выводятся из программы. Человек может набирать данные на клавиа туре и анализировать их на экране терминала, программа способна читать данные с диска или записывать на диск, программа может посылать данные на печать на принтер. Воз можны и другие варианты. В этой модели программы, которая достаточно очевидна, код выглядит следующим образом: Г копирование со стандартного ввода на стандартный вывод */
main()
{ int с; while((c = getchar()) != EOF) putchar(c);
} Этот код соответствует визуальной модели, представленной на рисунке 1.2.
Рисунок 1.2 Как прикладные программы рассматривают пользова тельский ввод/вывод' Рисунок подчеркивает то обстоятельство, что клавиатура и экран имеют связь с програм мой. В отношении обыкновенного персонального компьютера такая модель достаточно точно воспроизводит реальность. Клавиатура и дисплей подсоединены к материнской плате. Эти компоненты соединяются с помощью обыкновенных металлических проводни ков. Вы можете при случае увидеть на печатной плате эти проводники, которые соответ ствуют направлениям линий связи на рисунке.
1.2.2. Реальность Что происходит, когда вы входите в многопользовательскую систему, подобную типичной Unix - машине? В этом случае простая модель, где клавиатура и монитор связаны с про цессором (CPU), не соответствует действительности. Реальность более близка тому, что изображено на рисунке 1.3.
26
Системное программирование в Unix. Общие представления
Рисунок 1.3 Реальность: много пользователей, программ и устройств В данном случае есть несколько клавиатур и дисплеев, несколько дисков, один или более принтеров, а также есть несколько программ, исполняемых одновременно. При этом про граммы, которые получают данные, вводимые с клавиатуры, и передают данные на дисплей или на диск, прекрасно работают. Программы могут предполагать использование простой мо дели и получать правильные результаты. На самом деле все гораздо сложнее. Каким-то способом все эти различные клавиатуры со единяются с различными программами. Каким-то образом строится множество связей внутри машины. Если у вас появится возможность рассмотреть материнскую плату, то увидите ли вы то, что представлено на рисунке 1.4? Вряд ли. Такие соединения были бы кошмарными. Это все просто не будет работать, поскольку различные программы сменяют друг друга по мере вхождения различных поль зователей в систему и выхода их из системы. Для данного случая должна существовать другая модель, которая соответствовала бы мультипользовательскому, мультизадачному компьютеру.
Рисунок 1.4 Как все это соединено?
1.2.3. Роль операционной системы Роль операционной системы сводится к управлению и защите всех ресурсов, а также к при соединению устройств к различным программам. Физический смысл этого заключается в том, что операционная система, которая реализована программно, делает то же, что и ма теринская плата персонального компьютера, которая реализована аппаратно. Сплетение про водов, которое было на предшествующем рисунке, заменяется моделью на рисунке 1.5.
1.2 Что такое системное программирование? .
27
Операционная система - это программа. Код операционной системы, аналогично коду любой исполняемой программы, располагается в памяти компьютера. В памяти находятся также и другие программы - программы, которые были написаны пользователями и запу щены на исполнение. Операционная система соединяет эти программы с внешним миром.
1. 2.4. Поддержка сервиса для программ После того как мы рассмотрели проблему (как можно связать множество пользователей с множеством процессов?) и возможное решение проблемы (иметь основную управ ляющую программу для установления всех соединений), приступим к ее рассмотрению. Начнем сначала с некоторых определений. Память компьютера предназначена для под держания некоторого пространства для хранения программ и данных. Часть памяти ком пьютера, где размещается операционная система, называется системным пространст вом, а другая часть, где хранятся пользовательские программы, называется пользователь ским пространством. Операционная система называется ядром; клавиатуры и экраны подсоединяются к ком пьютеру (см. рисунок 1.6).
Рисунок 1.6 Ядро управляет всеми соединениями Заметим, что местом подсоединения устройств является системное пространство; таким образом, ядро является единственной программой, которая имеет доступ к этим устрой ствам. Пользовательские программы получают данные, обращаясь для этого к ядру. Ядро пере дает данные от клавиатуры к программе и пересылает данные от программы через уста новленное соединение на дисплей. Аналогично ядро может обеспечивать доступ к твердо му диску, принтерам, сетевым картам и другим периферийным устройствам. Если про грамме понадобится подсоединение или управление этими устройствами, то ей необходи мо обратиться с запросом к ядру.
28
Системное программирование в Unix. Общие представления
Линии связи на рисунке представляют собой виртуальные соединения, которые под держивает ядро. Ядро обеспечивает для пользовательских программ доступ к этим внеш ним объектам, что рассматривается как отдельные службы (сервисы). Мы теперь представляем контекст, в котором будет проходить объяснение системного программирования. Это и будет содержанием данной книги. Обычные прикладные про граммы могут быть написаны так, как будто они имеют непосредственное соединение с терминалами, дисками, принтерами. В этой расширенной модели работают системные программы, обеспечивая ресурсы и поддерживая службы. Мы будем изучать службы, которые поддерживает ядро, структуру этих служб, а также рассматривать, как писать программы, которые работают в этом расширенном контексте.
1.3. Понимание системного программирования Ядро обеспечивает доступ к системным ресурсам. Системные программы используют эти службы непосредственно. Что представляют собой эти службы и как мы будем изучать способы их использования?
1.3.1. Системные ресурсы Процессоры Программа представляет собой набор команд; процессор - это аппаратное устройство, которое выполняет команды. Процессор также называют обрабатывающим устройством. Некоторые компьютеры имеют несколько процессоров. Ядро назначает программы для исполнения на процессорах. Оно начинает исполнение, приостанавливает, возобновляет и заканчивает исполнение программы на процессоре.
Ввод/Вывод Все данные, которые поступают на входы программ и которые вырабатываются как вы ходные данные программ, проходят через ядро. Данные, поступающие от пользователей, и данные, которые поступают на терминалы пользователей, также проходят через ядро. Данные, которые читаются с дисков и которые записываются на диск, проходят через ядро. Такая централизация гарантирует, что передача данных будет происходить правиль но - данные попадают в необходимое место. Гарантируется эффективность - не требуется дополнительного времени, необходимого для передачи информации с одного места в дру гое. Гарантируется безопасность - ни один из процессов не может увидеть информацию, которая ему не предназначена. Управление процессами В Unix термин “процесс” используется для обозначения программы при ее исполнении. Процесс состоит из памяти, открытых файлов, других системных ресурсов, необходимых программе при исполнении. Новые процессы создает ядро. Ядро управляет процессами и организует их совместную работу.
Память Память компьютера является ресурсом. Программы могут потребовать некую дополни тельную память для хранения информации. Ядро следит за тем, какие секции памяти ис пользуют процессы, и защищает память одного процесса от возможного доступа со сторо ны других процессов.
1.3. Понимание системного программирования
29
Устройства К компьютеру могут быть присоединены самые разнообразные устройства. Ленточные устройства, CD-плееры, мышь, сканеры, видеокамеры - все это примеры устройств. Ядро обеспечивает доступ к устройствам и заботится обо всех сложностях управления. Когда программе необходимо получить картинку с видеокамеры, подсоединенной к ком пьютеру, она обращается к ядру, чтобы обеспечить доступ к этому ресурсу.
Таймеры Некоторые программы зависят от времени. Они могут выполнять 'действия по установке временных интервалов; им может потребоваться ожидать наступления некоторого момен та времени, после которого они будут что-то делать. Программам может потребоваться определение длительности выполнения неких действий. Ядро предоставляет для исполь зования процессоров определенное число таймеров. Межпроцессные коммуникации В повседневной жизни у людей возникает потребность в установлении коммуникаций ме жду собой. Для этого они используют телефоны, электронную почту, обыкновенную почту, радио, телевидение и другие средства для передачи информации. В вычислитель ной системе дри одновременном исполнении нескольких программ у процессов возникает потребность взаимодействия. Ядро поддерживает несколько способов межпроцессных коммуникаций. Такие коммуникационные системы, как телефонная сеть и почтовая служ ба, являются системными ресурсами. Сети Сеть, связывающая компьютеры, является расширенной формой межпроцессных комму никаций. Сеть предоставляет возможность процессам на различных машинах обмени ваться данными, даже если на этих машинах работают различные операционные системы. Сетевой доступ является службой ядра.
1.3.2. Наша цель: понимание системного программирования Мы только что ознакомились с некоторыми типами сервисов (служб) и с механизмами доступа к ресурсам в ядре, которые реализуются системными программами. Каково детальное представление каждого из типов служб? Как передавать данные от устройства к программе и обратно? Хотелось бы узнать, как работает ядро, как оно выполняет сервис ные действия и как писать программы, которые могли бы использовать такие службы.
1.3.3. Наш метод: три простых шага Мы будем изучать службы Unix, используя: 1. Просмотр “реальных” программ. Мы будем изучать стандартные Unix - программы, чтобы посмотреть, что они делают и каким образом программы используются на практике. Мы посмотрим, какие системные службы будут использоваться этими программами. 2. Изучение системных вызовов. Мы будем далее изучать системные вызовы, которые можно использовать при работе с упомянутыми службами. 3. Написание наших собственных версий. После того как поймем, как работает программа, какие системные службы она исполь зует и как используются эти службы, мы будем способны писать наши собственные системные программы. Эти программы является расширением существующих протямм urru fivnvT нгппттмлпять. ттпм плгтпаротги narmftmPHiTLTP гтмнимпи
30
Системное программирование в Unix. Общие представления
Мы будем изучать системное программирование в Unix, задавая себе многократно сле дующие три вопроса: Кто выполняет это действие? Как выполняется это действие? Могу ли я попытаться сделать то же?
1.4. UNIX с позиций пользователя 1.4.1. Что делает Unix? Первым нашим шагом при изучении любого аспекта Unix будет получение ответа на во прос - что делает система? Сначала ответ на этот вопрос будет относительно UNIX в це лом. Как пользователь воспринимает Unix? Как система воспринимается пользователем, который садится за Unix - терминал? После беглого рассмотрения этих вопросов у нас возникнут вопросы относительно того, как это все работает.
1.4.2. Вхождение в систему—запуск программ—выход из системы Работать в Unix просто. Вы входите в систему, запускаете какие-то программы и выходите из системы. При вхождении в систему вы набираете пользовательское имя и пароль: Linux 1.2.13 (maya) (ttypl) maya login: betsy Password: __
После вхождения в систему вы запускаете программы на исполнение. Вы можете запускать различные виды программ. Можете запустить программу для чтения и посылки электронной почты. Можете запустить программу для расчета места расположения планет или определения фондовых показателей. Можно запускать игровые программы. Запуск программ выполняется чрезвычайно просто. Система выводит на экран приглашение. В ответ вы набираете и вводите имя программы, которую хотели бы запустить на исполнение. Компьютер запускает програм му После выполнения этой программы система выводит на экран следующее приглашение. Даже изощренные графические десктопы следуют такому порядку действий. Приглашением является экран с иконками и меню, а нажатие кнопкой мышки на иконке или пункте меню эк вивалентно набору имени команды. За графическим интерфейсом стоит программное обес печение, которое связывает текстовые имена файлов изображений с именами программ. После окончания запущенных программ вы выполняете выход из системы (log out): $ exit
В зависимости от проведенных предварительно настроек для вашего входа в систему вы можете выйти из системы с помощью команды logout или при наборе на клавиатуре по следовательности Ctrl-D.
Как все это работает? Все выглядит достаточно просто, но что за этим стоит? Как это все работает? Что означает войти в систему? При работе с персональным компьютером используется идея персонального использования компьютера, что сравнимо с использованием семейного авто мобиля. На Unix - машине в одно и то же время в систему могут входить несколько человек, даже сотни человек. Как система узнает, кто вошел в систему и где это произошло?
1.4. UNIX с позиций пользователя
31
Рассмотрим этот процесс более детально. Если ваше входное имя и пароль были восприняты при входе, то система стартует программу, которая называется shell, и свяжет вас с ней. Каж дый пользователь, вошедший в систему, связывается с собственным shell-процессом. На рисунке 1.7 представлена иллюстрация вхождения пользователя в Unix - систему. Компьютер изображен в форме ящика слева, а пользователь сидит и работает с клавиа турой и экраном. Внутри компьютера находится память, где хранится ядро и пользова тельские процессы. Ядро производит контроль и управление за соединением пользователя с системой. Также оно передает данные между пользователем и shell. Shell выводит на экран приглашение, по которому пользователь оповещается о готовности запустить для него некую программу. В данном примере в качестве приглашения исполь зован знак доллара. В качестве приглашения может быть использована любая текстовая строка. Пользователь набирает имя программы, и ядро пересылает его на вход shell.
Рисунок 1.7 Вхождение пользователя в систему Например, чтобы запустить программу, которая выводит на экран текущее время и дату, пользователь должен набрать такую командную строку: $ $date SatJuM 21:34:10 EDT 2000
$. Запускается программа date, она выводит дату, а затем shell выводит новое приглашение. Для запуска другой программы достаточно набрать ее имя. Во многих Unix - системах имеется программа, которая называется fortune. Вот пример ее вызова: $ fortune Algol-60 surely must be regarded as the most important programming language yet developed. - T. Cheatham
$. Когда вы выйдете из системы, ядро уничтожит shell-процесс, который был вам ассоциирован. Каким образом ядро создает такой shell-процесс? Каким образом shell-процесс получает имя программы и запускает на исполнение эту вашу программу? Как shell узнает о том, что программа закончилась? Процедура вхождения в систему и запуски программ не так просты, как это может вначале показаться. Мы будем изучать детали в главе 8.
Системное программирование в Unix. Общие представления
32
1.4.3. Работа с каталогами После того как вы вошли в систему, становится возможным работать с вашими файлами. В ваших файлах может находиться электронная почта, графические изображения, исход ные коды программ, программы, готовые к исполнению, всевозможные данные. Файлы организованы в структуру с помощью каталогов.
Дерево каталогов В Unix файлы объединяются в древовидные структуры с помощью каталогов, а система предоставляет пользователю команды для просмотра компонентов дерева и навигации по дереву Ниже дана древовидная структура:
Рисунок 1.8 Часть дерева каталогов Корень файловой системы обозначают символом /. В каталоге / содержится несколько ка талогов. Каталог называют корневым (root directory), потому что из него вырастает полное дерево каталогов. Наиболее типичным составом корневого каталога для Unix - систем бу дут каталоги с такими именами, как /etc, /home, /bin, а также с другими стандартными име нами. Для каждого пользователя в дереве файловой системы назначается домашний ката лог для размещения в нем персональных файлов пользователя. Во многих системах поль зовательские каталоги являются подкаталогами в каталоге /home. В Unix имеется ряд команд, которые позволяют работать с древовидной структурой ката логов. Это программы для создания новых каталогов, удаления каталогов, для перемеще ния файлов и каталогов по дереву файловой системы и программы проверки содержимого каталогов. Войдите в систему и поработайте самостоятельно с этими командами.
Команды для работы с каталогами Is - представление содержимого каталога в списочном формате. Команда Is позволяет увидеть содержимое каталога в списочном формате. При выполне нии команды вида Is вы получите содержимое текущего каталога. Если вы наберете Is dirname, то увидите содержимое указанного каталога. Например, вы можете набрать команду Is /etc
1.4. UNIX с позиций пользователя
33
с тем, чтобы посмотреть, какие файлы и каталоги находятся в каталоге /etc. Если же вы на берете команду Ь/ то увидите файлы и каталоги, которые находятся в корневом каталоге, cd - сменить каталог. При выполнении команды cd происходит переход в указанный каталог. Когда вы входите в систему, то попадаете в ваш домашний каталог. Далее вы можете покинуть свой домаш ний каталог и перейти в другую часть дерева файлов с помощью команды изменения ка талога. Например, после выполнения команды cd /bin
вы попадаете в каталог, в котором содержится много системных программ. Когда вы пере шли в этот каталог, то можете выполнить команду Is, чтобы посмотреть, какие файлы и ка талоги здесь находятся. Из любого каталога можно переместиться по дереву на уровень вверх после набора и выполнения команды cd ..
Независимо от того, куда вы переместились по дереву, вы в любом месте можете вернуть ся в свой домашний каталог после выполнения команды cd pwd - вывести (распечатать) маршрутное имя текущего каталога.
Команда pwd информирует вас о том, в каком каталоге дерева вы сейчас находитесь. Она выводит на экран путь от корня системы каталогов до вашего текущего каталога. Напри мер, команда $ pwd /home/cse215/samples
показывает, что путь от корня дерева до нашего текущего каталога проходит через каталог home, затем через подкаталог cse215 и т. д. mkdir, rmdir - создание и удаление каталогов.
Для создания каталога следует использовать команду mkdir. Например, после выполнения команд $cd $ mkdir jokes
будет создан каталог jokes, который размещается в домашнем каталоге. Вам не разреша ется создавать новые каталоги в каталогах других пользователей. Для удаления каталога следует использовать команду rmdir. Например, после выполнения команды $ rmdir jokes
будет удален каталог jokes, если он не содержит файлов или каталогов. Вы должны уда лить или переместить содержимое каталога перед тем, как попытаться его удалить. Команды для работы с каталогами: как они работают? Мы рассмотрели, как может выглядеть твердый диск в форме дерева каталогов, где каждый каталог соединен с одним вышележащим и каждый каталог может содержать некоторое количество каталогов, ко торые находятся на уровнях ниже текущего. Каждый каталог может содержать файлы. Пользователь имеет возможнось перемещаться по этой древовидной структуре, переходя от одного каталога к другому, создавая при этом новые каталоги здесь и там или удаляя старые каталоги.
34
Системное программирование в Unix. Общие представления
А как это все работает? Твердый диск - это просто набор металлических пластин, которые способны хранить намагниченные элементы. А где же здесь каталоги? Что для вас означа ет выражение “находиться в вашем домашнем каталоге”? Что для вас значит переход в другой каталог? Какое-то число пользователей могут войти и работать одновременно на одной Unix - ма шине. При этом эти пользователи могут находиться в различных каталогах или все сразу в одном и том же каталоге, если они этого пожелают. Что будет с такими пользователями, если они все обратятся к одному каталогу? Как можно писать программы, которые будут выполнять навигационные действия по дереву каталогов? Какую роль играет ядро в создании такой древовидной модели?
1.4.4. Работа с файлами Каталоги играют роль, системной памяти для файлов. Пользователи имеют персональные файлы, которые хранятся в домашнем каталоге и в нижележащих каталогах. Система хра нит свои файлы в системных каталогах. Что может делать с файлами пользователь? Мы начинаем рассмотрение некоторых базовых действий. Команды для работы с файлами Имена файлов - краткое представление. Файлы имеют имена. В большинстве версий Unix имена файлов могут быть достаточно длинными - иметь до 250 символов.(Чаще всего указывают максимальную длину, равную 255 символов.- Примеч. пер.) Имена файлов могут быть составлены из любых символов, за исключением символа Символы могут быть набраны в верхнем и нижнем реги страх. В именах можно использовать знаки пунктуации, пробелы, знаки табуляции и даже символы перевода строки. cat, more, less, pg - команды для представления содержимого файлов.
Файл содержит данные. Для просмотра содержимого файла можно использовать команды cat, more или less. Команда cat служит для отображения содержимого всего файла целиком: $cat shopping-list soap cornflakes milk apples jam
$ Если файл имеет большее число строк, чем размер экрана, то можно использовать коман ду щоге для организации постраничного вывода содержимого файла на экран. $ more longfile
После вывода каждой очередной порции на экран вы должны нажать на клавишу “Про бел”, чтобы вывести следующую страницу, или нажать на клавишу Enter для смещения текущего вывода на одну строку или нажать на клавишу “q” для выхода из просмотра фай ла. На некоторых системах доступны для использования команды less и pg. Они работают аналогично команде more.
1.4. UNIX с позиций пользователя
35
ср - копирование файла.
Для выполнения копирования файла следует использовать команду ср. Например, при вы полнении команда $ ср shopping-list last.week.list
будет создан новый файл last.week.list, и в этот новый файл будет копироваться содержи мое файла shopping-list. rm - удаление файла.
Для удаления файла из каталога следует использовать команду rm. Например, после вы полнения команды $ rm old.data junk shopping.junel 992
будут удалены три файла. В Unix не поддерживается действие восстановления (undelete). В одно и то же время сис тему могут использовать сразу несколько пользователей. Когда вы удаляете файл, то система может немедленно выделить освободившееся место на диске для другого поль зователя. Дисковое пространство, в котором всего секунду назад находилась ваша курсо вая работа, может теперь содержать исходный код программы на С другого пользователя. mv - переименование или перемещение файла.
Для переименования файла или для перемещения файла в другой каталог следует исполь зовать команду mv. Например, после выполнения команды $ mv progl .с firstj>rogram.c
будет изменено имя файла progl.с: новым именем будет first_program.c. Можно теперь переместить эту программу в другой каталог, задавая имя каталога в качестве последнего аргумента при обращении к команде: $ mkdir mycode $ mv firstj>rogram.c mycode Ipr, Ip - распечатать содержимое файла.
Вы можете распечатать содержимое файла при помощи команды 1рг. В самом простом вари анте команда имеет вид: $ Ipr filename
На принтер по умолчанию будет передан для печати файл с указанным именем. На многих системах используют более одного принтера. Тогда команда Ipr применяется в более слож ном варианте, с тем чтобы выбрать для использования конкретный принтер. Пожалуйста, обратитесь к документации на вашей локальной системе, чтобы ознакомиться с деталями печати. На некоторых системах для печати используется команда 1р. Файловые команды: как они работают? Пользователи воспринимают файл как некое объединение информации, обычно в форме документа. Документ рассматривается как со вокупность страниц, состоящих из символьных строк. Как файлы хранятся на диске? Каким образом происходит копирование файлов? Как можно перемещать файл из одного каталога в другой? Как система производит переименование файлов? И вообще, как сис тема производит именование файлов? Вы, читатель, имеете имя; где оно хранится? В Unix все эти вопросы разрешены. Вам, как системному программисту, необходимо понимать, как это все работает.
36
Системное программирование в Unix. Общие представления
Атрибуты прав доступа к файлам У вас есть некоторые файлы, у других пользователей есть свои файлы. У тех, кто запуска ет систему, имеются свои системные файлы. Вы можете не предоставлять всем окру жающим право на изменение или даже право на чтение ваших файлов. Для тех лиц, которые будут запускать систему, требуется, чтобы пользователи не изменяли бы системные файлы или не вызвали бы беспорядок при работе с системными каталогами. Для контроля за доступом пользователей к их файлам в Unix для каждого файла устанав ливаются несколько атрибутов. Файл имеет собственника, и файл имеет атрибуты прав доступа к нему. Собственник файла является пользователем в системе. Вы становитесь собственником файла, когда его создаете. Другие пользователи становятся собственника ми при создании их собственных файлов. Каждый файл имеет три группы атрибутов прав доступа к файлу. Команда Is -1 показывает значения атрибутов файла: $ Is -I outline.01 -rwxr-x— 1 molay users 1064 Jun 29 00:39 outline.01
Это расширенный вариант вывода по команде Is. Символы -1 называются опцией в команд ной строке. Вы можете менять поведение Unix - команд с помощью указания значений этих опций при запуске команды. При расширенном варианте вывода команда Is выводит информацию о правах доступа, имя собственника файла, размер файла, дату и время по следней модификации файла. Подстрока в левой части строки вывода команды Is -1, состоящая из символов и знаков пунктира, отображает состояние разрядов прав доступа. Каждый файл имеет собственника и три группы атрибутов доступа к файлу: гwхгwх user group
г w х г: чтение, w: запись, х: исполнение other (собственник группа все.остальные).
Весь мир пользователей делится на три категории: пользователь, являющийся собствен ником файла, группа, к которой принадлежит пользователь, и все другие пользователи. Пользователям в каждой из этих трех категорий может быть предоставлено право на чте ние из файла, на запись в файл или на исполнение файла. Эти девять атрибутов являются независимыми. Вы можете, например, дать право на модификацию файла и не разрешить читать из файла всем пользователям из категории все_остальные. Вы даже себя можете лишить возможности читать собственные файлы. Права доступа к файлу: каким образом это все работает? Каково назначение разрядов прав доступа? Как установить указанные атрибуты прав доступа? Какие стратегйи при управлении правами поддерживаются в Unix? Где хранятся эти разряды прав доступа? Мы изучим эти темы в последующих главах.
1.5. Расширенное представление об UNIX 1.5.1 Взаимодействие (связь) между людьми и программами В предшествующем разделе мы рассмотрели, что делает Unix с позиций пользователя, и начали рассмотрение вопроса, как работает система. Пользователь входит в систему, запускает программы на исполнение, работает с файлами и каталогами и выходит из сис темы. Возможно, что в то же самое время в систему могут входить еще какие-то пользова тели, запускать на исполнение свои программы, работать с их файлами и каталогами и вы* ходить из системы. Пользователи могут работать с одними и теми же файлами и каталога ми, они могут посылать электронную почту или разовые сообщения друг другу. Каждый пользователь работает в собственном пространстве, но это пространство является частью большой системы.
1.5. Расширенное представление об UNIX
37
Мы изучим, как все это работает, и рассмотрим, как писать программы, которые работают в этой большой системе. Что представляет собой эта большая система? Большая система представляет собой систему, в которой работают более одного пользователя, исполняются более одной программы, работают более одного компьютера, производится взаимодейст вие (связь) между людьми, программами и компьютерами. Рассмотрим три примера, с тем чтобы обсудить некоторые идеи и вопросы, которые воз никают при программировании в этой большой системе.
1.5.2. Турниры по игре в бридж через Интернет Много людей играют в бридж через Интернет. Люди садятся за свои компьютеры, соеди няются с сайтом для игры в бридж и ищут игру. Как только игроки подсоединились к игре, возникает ситуация: четыре человека сидят за компьютерами в разных частях света. Каж дый из них видит на своем экране общий стол, каждый из них разделяет с другими игро ками одну и ту же колоду карт, и каждый может видеть, что делают другие игроки. Упро щенная картинка этой игры будет такой:
Четыре человека играют в бридж через Интернет На рисунке 1.9 изображены четверо игроков, каждый из которых работает со своим ком пьютером, каждый компьютер соединен через линию связи с Интернет. На этом рисунке не представлен стол для бриджа, который добавлен на рисунке 1.10.
Рисунок 1.10 Стол для бриджа н$ серверном компьютере Теперь мы имеем дело с сетью, в которой появляется пятый компонент. На столе для бриджа находятся карты, которые используются в игре. Стол представлен как поверх ность, на которой отображаются образы карт. Стол - это место, вокруг которого собирают ся люди, чтобы сыграть в игру. В реальной игре игроки могут передавать карты от одного игрока другому. Каким образом сделать то же самое при ведении виртуальной игры?
38
Системное программирование в Unix. Общие представления
Где располагаются карты? Как представить карты, которые находятся у вас на руках? Как программа может предотвратить 'использование двумя игроками одних и тех же карт? В реальном мире это не является проблемой. В виртуальном мире каждая карта не пред ставляет собой отдельную физическую целостность, что предотвращает возможность ее одновременного пребывания сразу в двух местах. На рисунке 1.11 изображены некоторые коммуникационные маршруты:
Рисунок 1.11 Отдельные программы посылают сообщения друг другу При рассмотрении примера с игрой в бридж возникли три новые темы, которые весьма важны в системном программировании в среде Unix. Коммуникации Каким образом один пользователь или процесс связывается с другим пользователем или процессом? Координация Одновременно два игрока не могут выбирать карты из колоды. Каким образом программа должна координировать действия между процессами, чтобы они правильно разделяли ре сурсы? Сетевой доступ В данном примере программы на каждом из компьютеров пользователей взаимодейст вуют через Интернет. Как программа может связаться с другой программой, используя Интернет? Каким образом обеспечивается программный доступ к Интернет?
1.5.3. Ьс: секреты настольного калькулятора в Unix В каждой версии Unix имеется программа Ьс, которая выполняет функции простого, тек стового калькулятора с двумя привлекательными характеристиками. Чтобы запустить программу на исполнение, нужно набрать: $Ьс В ответ не появится ни приглашения, ни указания номера версии, ни требования набрать пароль. Программа просто будет ждать возможности выполнить некие вычисления. Наберите арифметическое выражение и нажмите на клавишу Enter. 2+3*4+5*10
1.5. Расширенное представление об UNIX
39
Программа Ьс выведет на экран правильный результат. Программе известно, что в выра жении следует сначала выполнить умножение, а затем сложение. Для выхода из програм мы Ьс следует нажать на клавиши Ctrl-D. Одним из достоинств программы Ьс является возможность работать с очень большими целыми числами, такими, как: 99999999999999999999 * 88888888888888888888 8888888888888888888711111111111111111112
Для представления больших чисел можно использовать экспонентную форму записи чисел: 3333 Л 44 101 10061584495640995005898489182285794822405288498070703365111794769\ 4389041 1064925291 154381468890721948142209004688381870355409155411563\ 21805747562427309521
Для обычного представления числа 3333 с десятичным порядком 44 понадобилось две с половиной строки десятичных цифр. Поэкспериментируйте с Ьс, чтобы посмотреть, как программа работает с большими числами. Программа Ьс поддерживает также свой язык программирования, где используются переменные, циклы и С-образный синтаксис. Например, следующий ниже код (будет восприниматься для выполнения программой Ьс: if (х == 3){ у = х*3;
} У Вот еще одно интересное свойство программы Ьс. Программа Ьс не является' калькуля тором. Она не производит вычислений. Чтобы посмотреть, что делает программа, давайте попытаемся выполнить следующее: $Ьс 2+3 5 <-- Нажмите здесь Ctrl-Z Приостанов процесса $ps PID TTYS TIME CMD 25102 ttyp2T 0:00.02 be 27081 ttyp2T 0:00.01 dc-27560 ttyp2 1 0:00.59 -bash 27681 ttyp2T 0:00.00 be $ fg <- Нажмите здесь Ctrl-D
Программа ps выводит информацию о запущенных вами процессах. В данном листинге представлена информация о четырех процессах. Для одного из процессов указана в каче стве имени исполняемой программы строка в виде “-bash,” что говорит о том, что это ин формация о “входном shell” (log-in shell). В листинге представлены еще два процесса, в которых исполняется программа Ьс, и один процесс, в котором исполняется программа dc. Что представляет собой программа dc?
40
Системное программирование в Unix. Общие представления
В большинстве версий Unix есть электронный справочник. Для прочтения документации по команде dc следует набрать такую команду: $ man dc User Commands dc(1) NAME dc - desk calculator SYNOPSIS dc [ filename ]
DESCRIPTION dc is an arbitrary precision arithmetic package. Ordinarilyit operates on decimal integers, but one may specify an input base, output base, and a number of fractional digits to be maintained. The overall structure of dc is a stacking (reverse Polish) calculator. If an argument is given, input is taken from that file until its end, then from the standard input.
Это страничка из электронного справочника системы SunOS 5.8; в большинстве версий Unix описания команд представлены в аналогичном виде. В этом тексте из документации говорится о том, что dc - это команда, которая выполняет функции калькулятора. Более то го, здесь указывается, что эта команда работает как калькулятор с использованием стека и обратной польской записи. Предполагается, что она начинает работать после набора поль зователем чисел. Выполним сложение следующего вида: 2 +3.
2 3 + Р
5 Этот протокол означает, что сначала в стек заносится 2, затем 3, далее производится сло жение двух чисел, которые находятся в головной части стека, затем происходит выдача полученного результата, который будет размещен в голове стека. Возникает вопрос - если программа dc представляет собой калькулятор, причем калькулятор, использующий стеко вую память, то чем же тоща является программа 1?с и почему она тоже исполняется? Ответ мы получим после прочтения документации в электронном справочнике относитель но команды Ьс. В документации сказано о том, что команда Ьс является препроцессором от носительно команды dc1. Программа Ьс является синтаксическим анализатором. Она взаи модействует с процессом, где выполняется программа dc через коммуникационное средство, которое называется pipes (программные каналы)^ как показано на рисунке 1.12. Данные, которые вводит пользователь в формате “2 +2”, поступают на вход процесса Ьс. В этом процессе происходит преобразование данных в соответствии со стековым пред ставлением, после чего данные передаются процессу dc. Процесс dc выполняет необходи мые вычисления и отправляет полученный результат обратно процессу Ьс. Процесс Ьс про изводит форматирование этого результата для оконечного пользователя. Пользователь воспринимает программу Ьс как калькулятор. 2 2 + р ___________ _ 2 + 2
Рисунок 1.12 Программы посылают сообщения друг другу 1. В версии GNU команды Ьс вместо dc используется внутренний стековый калькулятор.
1.6. Могу ли я сделать то же самое?
41
В данном примере с программами bc/dc показано наличие программ, которые, аналогично случаю использования стола для бриджа в Интернете, включают в себя различные процес сы, а также некоторые разновидности средств коммуникации и кооперации. Отдельные программы при совместной работе образуют систему. Каждая часть системы выполняет свою задачу и достаточно наглядно выделена. Значимую часть системы составляют сред ства межпроцессных коммуникаций, которые используются отдельными программами. Сходство между примером с игрой в бридж через Интернет и примером с системой и bc/dc является фундаментальным принципом системного программирования в Unix. Поэтому изуче ние системного программирования в Unix сводится к изучению того, как нужно писать отдель ные программы и каким образом необходимо построить связи и обеспечить совместную работу программ.
1.5.4. От системы bc/dc к Web Систему из программ bc/dc отделяет от World Wide Web всего один небольшой шаг. В сис теме в паре bc/dc программа Ьс выполняет функции пользовательского интерфейса, а про грамма dc выступает в роли программы, выполняющей заданную работу. При рассмотре нии World Wide Web видно, что броузер выполняет функции пользовательского интерфей са, a Web-cepeep выполняет конкретную работу. Архитектура при этом одна и та же.
http://www.xyz.com/info
Рисунок 1.13 Отдельные программы посылают сообщения друг другу Пользователь взаимодействует с броузером. Броузер располагается не в том месте, где находятся Web-страницы. Эти страницы находятся на серверах. Совсем коротко можно сказать, что серверы поддерживают текстовый язык http, а программа dc разговаривает на текстовом языке, который называется rpn. Пользовательский агент (Ьс или броузер) транслирует пользовательский ввод (2+3 или нажатия кнопок мыши) в краткий текстовый язык и посылает требование на обслуживание (программа dc или Web-сервер). Пользова тельский агент (Ьс или броузер) принимает ответ от сервера и форматирует его для пользователя. Между моделью системы программ bc/dc и World Wide Web принципиаль ная разница отсутствует. Вероятно, не является неожиданным, что Web выросла благодаря Unix - системам2.
1.6. Могу ли я сделать то же самое? У нас теперь появилось представление о сути проблем, которые были обозначены ранее в двух первых вопросах. Мы рассмотрели несколько аспектов системы Unix, с тем чтобы ответить на вопрос “Что делает система?”. Перед нами стоял вопрос “Как выполняется эта работа?”
2. Между прочим, студент моего курса Ами Чусед был первым, кто отметил связь между системой bc/dc и клиент-серверным программированием на основе TCP/IP.
42
Системное программирование в Unix. Общие представления
По таким примерам, как система bc/dc, мы получили частично ответ на этот вопрос. Согласно нашему методу есть и третий вопрос: ’’Могу ли я сделать то же самое?” В этом разделе мы напишем версию Unix программы more.
Во-первых, определим, “Что делает команда more”? Команда more служит для поэкранного вывода содержимого файла. В большинстве Unix систем есть большой текстовый файл, который называется /etc/termcap и используется не которыми редакторами и видеоиграми. Если вы захотите постранично просмотреть содержимое этого файла, то вы должны будете набрать следующую команду: $ more /etc/termcap
В начале работы программы вы увидите первый экран текста файла. В нижней части экра на программа more будет выводить в инверсном режиме отображения процент про смотренного объема текста. Для просмотра следующей страницы следует нажать на кла вишу пробела, для смещения просматриваемого текста на одну строку необходимо нажать на клавишу Enter, для выхода из просмотра следует нажать на клавишу “q”, для получения текста помощи вы должны нажать на клавишу “h”. Заметим, что не следует нажимать на клавишу Enter после нажатия на клавишу пробела или на клавиши “q” или “h”. Программа сразу отвечает на нажатие указанных клавиш. Есть три варианта использования команды more на уровне командной строки: $ more filename $ command | more $ more < filename
В первом случае команда more будет отображать содержимое файла с указанным именем. Во втором случае запускается на исполнение программа с именем command и ее вывод постранично отображается на экране. В третьем случае команда more отображает тот текст, который эта программа читает из стандартного ввода. Вместо стандартного ввода был присоединен указанный файл.
Во-вторых, определим, “Как это все работает”? После неоднократного запуска команды more можно заметить, что ее логика работы впол не вероятно описывается следующей последовательностью шагов: +- — > вывод 24 строк из файла +- - > вывод сообщения [more?] | Нажатие на клавиши Enter, SPACE или q +-- если Enter вывод одной очередной строки +—- если SPACE если q ~> выход
Наша программа должна быть гибкой в части организации ввода и быть похожей в этом на реальную программу more. Это означает, что если пользователь указывает для нашей про граммы в командной строке имя файла, то программа должна читать этот файл. Если в ко мандной строке при обращении к программе имя файла не задано, то программа должна будет читать со стандартного ввода. Ниже представлен первый вариант нашей версии программы more: /* moreOI .с - версия 0.1 программы more * читает и выводит на экран 24 строки, затем следуют несколько * специальных команд */ #include <stdio.h>
£ Могу ли я сделать то же самое? «define PAGELEN 24 «define UNELEN 512 void do_more(FILE *); int see_more(); int main(int ac, char *av[])
{ RLE *fp; if (ac == 1) do_more(stdin); else while (--ac) if ((fp = fopen(*++av, Г)) != NULl)
{ do_more(fp); fclose(fp);
} else exit(1); return 0;
} void do more(FILE *fp)
Г * читает PAGELEN строк, затем вызывает see more() для получения дальнейших инструкций
7 { char line[UNELEN]; int num_of_lines = 0; int see_more(), reply; while (fgets(line, UNELEN, fp)){ /* ввод для more */ if*(num_of_lines == PAGELEN) {/* весь экран? */ reply = see_more(); /* у: ответ пользователя */ if (reply == 0) Г n: завершить*/ break; num of lines -= reply; /* переустановка счетчика */
}
if (fputs(line, stdout) == EOF) /* показать строку */ exit( 1); /* или закончить */ num of lines++; /* учесть очередную строку */
} } int see more()
/* * выдать сообщение, ожидать ответа, возвратить значение числа строк * q означает по, пробел означает yes, CR означает одну строку
7 { int с; printf("\033[7m more? \033[m"); Г реверс изображения для vt100 7 while((c=getchar()) 1= EOF) [* получение ответа 7
44
Системное программирование в Unix. Общие представления
If (с == e q e )/ * q->N 7 return 0; if (с ==” ) /* " => следующая страница */ return PAGELEN; /* сколько показывать */ if (с == в\пв) /* Требование на 1 строку */ return 1;
} return 0;
} Код программы состоит из трех функций. В функции main определяется, откуда произво дится ввод информации - из файла или со стандартного ввода. Выяснив вопрос относи тельно входного потока, функция main передает этот входной поток функции, которая на зывается do_inore и которая должна будет поэкранно отображать этот поток. В свою очередь функция do_more отображает экран текста и затем обращается к функции see_more, которая должна запросить у пользователя, что делать дальше. Для компиляции и запуска нашей программы следует выполнить: $ сс moreOI .с -о moreOt $ moreOI moreOI .с
Полученная программа работает достаточно хорошо. Программа выводит 24 строки тек ста и далее выводит заметное для плаза приглашение тоге? в реверсном изображении. При нажатии на клавишу Enter будет выведена следующая строка текста. Над этой про граммой необходимо будет еще поработать. В частности, после вывода сообщения more? оно остается на экране и смещается (скрол лируется вверх) вместе с текстом. Кроме того, если вы нажмете клавишу пробела или кла вишу “q”, ничего не произойдет, если вы не нажмете после этого на клавишу Enter. Это ре шение нельзя признать хорошим. Итак, на экране остается приглашение more?. Создание данной версии программы more иллюстрирует основополагающий фактор относительно программирования в Unix: Программирование в Unix не так трудно, как вы думали, но и не так просто, как можно судить по первому опыту. Программа выполняет четко заданную задачу. Логика этой задачи достаточно ясна. При * разработке алгоритма, который реализует действия, выполняемые задачей, не использова лись всяческие ухищрения. Мы отметили ряд тонкостей в работе программы. Как модифицировать программу, ко торая реагировала бы сразу же на нажатие клавиш без последующего нажатия на клавишу Enter? Как можно вычислить процент объема просмотренного текста из файла? Как уда лить с экрана текст приглашения more? после того, как будет нажата клавиша? Это не должно быть слишком сложным. Но прежде всего нам нужно закончить с другими характеристиками программы, которые должны быть сравнимы с оригинальной программой. Насколько хорошо наша программа справляется с управлением входными потоками? Функция main выполняет проверку числа аргументов в командной строке. Если в команд ной строке имена файлов отсутствуют, то программа будет производить чтение со стан дартного входа. Тем самым обеспечивается возможность помещать программу more в ко нец конвейера, как показано ниже: $who|more
1.6. Могу ли я сделать то же самое?
45
В этом конвейере запускается команда who, которая отображает список всех пользовате лей, работающих в текущий момент в системе, и посылает этот список пользователей команде more. Поскольку наша программа more отображает за раз 24 строки, то она будет полезна, если число пользователей будет превышать 24. Давайте проверим работу нашей программы, но при работе не с программой who, а при работе с командой Is: $ Is /bin | moreOI Мы предполагаем увидеть содержимое каталога /bin страницами по 24 строки. Когда вы запустите нашу программу, то увидите, что moreOI не приостанавливается после вывода 24 строк. Что привело к ошибке? Причина заключается в следующем. Наша про грамма moreOI читает и выводит по 24 строки из входного потока, который она получает от команды Is. Когда программа moreOI будет читать двадцать пятую строку, она выведет приглашение more? и будет ожидать ответа пользователя. Наша программа ждет, что поль зователь нажмет либо на клавишу пробела, либо на клавишу Enter, либо на клавишу “q”. Где в программе принимается информация от пользователя? В программе используется для чтения из стандартного ввода getchar. Но в таком представлении конвейера: $ Is /bin | moreOI происходит перенаправление стандартного вывода команды Is на стандартный ввод про граммы moreOI. Наша версия программы more пытается читать команды пользователя из того же потока, откуда поступают данные из файла. На следующем рисунке показана ситуацияГ'
Каким образом решается эта проблема в реальной программе more? То есть, как программа может читать данные со стандартного ввода и одновременно вводить информацию от пользователя с клавиатуры? Решением будет чтение данных непосредственно с клавиа туры. На рисунке 1.15 показано, как это делается в реальной версии. В каждой системе Unix есть специальный файл, который называется /dev/tty. Этот файл обеспечивает соединение с клавиатурой и экраном. Даже если пользователь с помощью символов < или > перенаправит в программе стандартный вход или стандартный вывод, программа остается связанной с терминалом, чтение и запись с которым производится через файл /dev/tty.
46
Системное программирование в Unix. Общие представления
На рисунке показано, что more имеет два источника ввода. Стандартный ввод програм мы подсоединен к выводу программы who. Но программа more также читает данные из файла /dev/tty. Программа читает строки файла и отображает их на экране. Когда необ ходимо запросить у пользователя, следует ли выводить более одной строки, более одной страницы или следует выйти из просмотра, программа читает ответ от пользова теля из файла /dev/tty. В соответствии с полученными новыми знаниями расширим вариант программы moreOI.с и создадим вариант more02.c: /* more02.c - версия 0.2 программы more * чтение и выдача 24 строк, затем следуют несколько * специальных команд * особенность версии 0.2: чтение команд из файла /dev/tty
*/ «include <stdio.h> «define PAGELEN 24 «define UNELEN 512 void do_more(FILE *); int see_more(FILE *); int main(int ac, char *av[])
{
FILE *fp; if {ac ===== 1) dojnore(stdin); else while (--ac) if ((fp = fopen(*++av, "r")) != NULL)
{ do_more(fp); fclose(fp);
} else exit(1); return 0;
}
void do_more(FILE *fp)
г
* utpump PAftFI PM rrnnir qqtpm butchr -qpp mпгрП л па Rkinnnupuuci ляпинршпму
1.6. May ли я сделать то же самое?
47
* команд
7
{
't'
char line[UNELEN]; int num_ofJines = 0; int see more(FILE *), reply; FILE *fp_tty; fp.tty = fopenC'/dev/tty", Y); /* НОВОЕ: команда потока 7 if (fp_tty == NULL) /* если открытие неудачно 7 exit(1); /* не используется при запуске программы no use in running7 while (fgets(line, UNELEN, fp)){ /* ввод для more */ if (num_of_lines == PAGELEN) {/* весь экран? */ reply = see_more(fp_tty); f* НОВОЕ: передача RLE * 7 if (reply == 0) Г n: завершить */ break; num of lines -= reply; f* переустановить счетчик 7
}
if (fputs(line, stdout) == EOF) /* показать строку17 exit( 1); /* или закончить вывод 7 num of lines++; /* учет выведенной строки */
) )
int see_more(RLE *cmd) /* НОВОЕ: прием аргументов */
Г
* выдать сообщение, ожидать ответа, возвратить значение числа строк
* q означает по, пробел означает yes, CR означает одну строку 7
{
int с; printf(”\033[7m more? \033[m"); /* реверсировать текст для vt100 */ while((c=getc(cmd)) != EOF) /* НОВОЕ: читать из tty */
{ if (с == 'q') Л Q •> N 7 return 0; if (с == ”) /*"=> следующая страница 7 return PAGELEN; /* сколько показывать */ if (с == ’\п’) /* Enter => 1 строка 7 return 1;
}
return 0;
} Компиляция и проверка этой версии производятся с помощью команд: $s сс -о more02 more02.c $ Is /bin | more02
Эта версия more02.c может читать данные со стандартного ввода, а команды - с клавиа туры. Заметим, что стремление написать стандартную Unix - программу привело нас к необходимости изучить файл /dev/tty и определить его роль в качестве связующего файла с пользовательским терминалом.
48
Системное программирование в Unix. Общие представления
И все же над нашей программой стоит еще поработать. Мы все еще должны нажимать на клавишу Enter для получения ответа.от программы. Итак, пусть на Экране появились сим волы “q” и пробел. Каким-то образом в реальной версии more при вводе указанных симво лов сразу же произойдет выход из программы. При этом не нужно нажимать на клавишу Enter. Если вы нажали на клавишу “q”, произойдет выход из программы, а вы этот символ просто не увидите на экране.
Непосредственный ввод: как это все работает? При установлении связи с терминалами можно производить настройки. Вы можете вы брать такую настройку соединения, что символы будут сразу же доступны в программе, а не после нажа(тия пользователем клавиши Enter после нажатия на какую-либо клавишу. Можно выбрать такую настройку, чтобы символы, которые пользователь набирает на кла виатуре, не отображались бы на экране. Вы в праве выбрать все варианты установок, ко торые управляют передачей данных вашей программе через терминал. Насколько мы углубились в проблему, детально показано на нашем рисунке. Теперь он бу дет представлен в таком виде:
Рисунок 1.16 Соединение с терминалом имеет настройки Новым составным элементом на этом рисунке стало управляющее устройство, которое было добавлено к соединению с /dev/tty. Это устройство позволяет программисту на страивать работу линии связи между терминалом и программой, что воспроизводит воз можные настройки между тюнером и громкоговорителем в радиоприемнике. Для написания полнофункциональной, хорошо работающей версии программы more нам по надобится изучить соединительное управляющее устройство (контроллер) и определить, как можно его программировать. Нам потребуется также ответить еще на ряд других вопросов. Как определить в процентах объем показанного текста? В реальной версии more процент вы вода пользователь может видеть на экране. Как можно добавить эту возможность в нашу про грамму? Операционная система знает размер файла. Нам нужно познакомиться с тем, как за просить у операционной системы эту информацию. Что такое реверсивный режим изображе ния? Что можно сказать о числе строк на экране? На разных дисплеях используют различные режимы для визуализации текста в инверсном режиме. Различные дисплеи имеют различное число строк на физическом экране. Использование размера в 24 строки и кода для реверсив-
/. 7. Еще несколько вопросов и маршрутная карта
49
ного изображения в стиле vtlOO представляется недостаточно гибким. Как можно написать версию программы more, которая работала бы с любым типом терминала и с любым числом строк на экране? Для ответа на эти вопросы нам кеобходимо будет изучить особенности управления экраном терминала и его атрибуты.
1.7. Еще несколько вопросов и маршрутная карта /. 7. /. О чем пойдет теперь речь? Мы оговорили целевое назначение этой книги. Unix - это операционная система, которая предоставляет нескольким пользователям возможность одновременной работы. Пользо ватели могут запускать программы на исполнение и работать с файлами и каталогами. Эти программы могут взаимодействовать между собой, с другим компьютером, а также взаи модействовать через сеть. Пользователи запускают программы для управления своими файлами, для обработки данных, для передачи и преобразования данных и для установле ния связи с другими пользователями. Что нужно сделать, чтобы все эти программы работали? Что делают программы? Что де лает операционная система? По мере изучения основных свойств системы мы ответили на многие вопросы. Давайте продолжать отвечать на вопросы. Наша разработка команды more продемон стрировала метод, который мы возьмем для последующего использования. Мы анализиру ем реальную программу, изучаем, что она делает, а затем пытаемся написать нашу собст венную версию такой программы. По мере разработки мы изучаем все более детально, как работает Unix, и учимся, как использовать ее принципы работы.
1.7.2. А теперь - карта Нам понадобится карта для нашего продвижения вперед. Вот она.
Рисунок 1.17 Диаграмма основной структуры системы Unix На этой диаграмме представлена основная структура любой системы Unix. Память разде ляется на системное пространство и на пользовательское пространство. В системном про странстве находится ядро и его структуры данных. Пользовательские процессы разме щаются в пользовательском пространстве. Некоторые пользователи взаимодействуют с системой через терминалы. Линии связи этих терминалов присоединены к ядру. Файлы
50
Системное программирование в Unix. Общие представления
хранятся в файловой системе на диске. К ядру подсоединяются различные типы устройств, которые становятся доступными пользовательским процессам. Наконец, есть средства для поддержки сетевых коммуникаций. Пользователи могут работать с систе мой, используя сетевые средства. В каждом разделе этой книги мы рассматриваем отдельные элементы этой диаграммы. Каждый компонент будет рассмотрен более детально, чтобы, изучить службы ядра, которые их поддерживают, и объяснить догику и структуры данных ядра, которые исполь зуются для обеспечения этих служб. В конце книги мы изучим каждую часть представленной диаграммы и рассмотрим все идеи и средства, которые необходимы для написания сложных системных программ для Unix (например, для разработки версии игры в бридж через Интернет).
1.7.3 Что такое Unix? История и диалекты В этой книге объясняются базовые идеи и структуры Unix и рассматривается, как следует писать программы, которые будут работать в системе Unix. Но что же представляет собой система Unix? Откуда она появилась? Что в этой части можно ожидать от этой книги? Прежде всего, откуда появилась система Unix? Система Unix была разработана в Bell Lab oratories в 1969 году несколькими компьютерными специалистами для решения специаль ных технических проблем и состояла из ядра и набора инструментальных средств. Систе ма Unix не была коммерческим продуктом. В самом деле, в течение семидесятых годов Bell Labs распространяла программное обеспечение Unix, включая исходный код, в шко лы и научные центры за номинальную цену. Специалисты из Bell Laboratories и многие другие компьютерщики потратили много времени на изучение системы, ее улучшение и добавление новых оригинальных программ. В восьмидесятых годах несколько компа ний лицензировали исходный код Unix и создали несколько версий Unix, ориентирован ных на потребителей. Двумя основными центрами по развитию системы стали AT&T и университет Беркли в штате Калифорния (UCB). AT&T разработала версию, которая была названа System V, а специалисты UCB разработали версию, которая была названа BSD. Большинство версий Unix были разработаны на основе одной из этих базовых сис тем или на основе использования той и другой системы. С годами менялась собственность на систему, прошла серия продаж системы от AT&T ряду компаний, коллектив UCB пере стал работать над Unix, появились различные группы, которые пытались выверять и стан дартизировать систему. Независимо от курсов и стандартов основной проект и принципы Unix распространяются через университетские и коммерческие компьютерные сферы. Развиваются различные диалекты и модели системы. В некоторые версии были включены специальные средства, например, обработка в режиме реального времени. При всех этих адаптациях и изменени ях в системе всегда оставались архитектура ядра и постоянный набор функций. Хотя точная внутренняя структура и набор инструментальных средств для версии Unix от AT&T времен восьмидесятых годов будут отличаться для варианта, .написанного в 1991 году в Хельсинки, но программы, которые были записаны для версии восьмидесятых, можно будет с минимальными изменениями откомпилировать и запустить на исполнение в среде финской версии. Что же, в конце концов, представляет собой Unix? Термин система Unix чаще всего используется при ссылке на системы, которые построены на основе ядерной модели и поддерживают определенные функции, которые являются общими для всех этих вариан тов систем. Некоторые системы работают и выглядят аналогично Unix, но построены они были не на основе кодов версий AT&T или UCB. Комбинация инструментальных средств
Заключение
51
и ядра вида GNU/Linux известна под названием Unix - подобная система. Одно из формальных определений системного интерфейса называется POSIX. Для понимания, чтения и написания Unix - программ вам понадобится знание более одного стандарта. Unix имеет длинную, многовариантную историю. Какое число систем Unix предполагает ся изучить в этой книге? Мы сосредоточим свое внимание на структуре, принципах и средствах, которые являются общими для всех систем Unix. Некоторые детали будут опу щены, некоторые операции дублируются, а все идеи рассматриваются с практических по зиций. Некоторые детали я не включаю в рассмотрение, и иногда я предлагаю вам обратиться к документации. Эта книга не является исчерпывающим руководством по любому аспекту относительно любой версии Unix. Существенная часть знаний об Unix должна быть по лучена по мере изучения и использования электронной документации на вашей системе. Иногда я описываю различные функции, которые производят одно и то же действие. Это обусловлено тем, что имеет место дублирование функций при децентрализованном про цессе развития Unix. Различные группы, подобные AT&T и UCB, иногда предлагают раз личные решения одной и той же проблемы. Другая причина дублирования функций объ ясняется обычными издержками роста. Когда разработчики заменяют в более развитой версии какую-либо службу в Unix, например, такую, как аварийные таймеры, они не хотят выкидывать существующие программы. Поэтому они редко удаляют старый, упрощенный интерфейс. Иногда я привожу одно решение, а иногда несколько. Если вы будете изучать программы Unix, то будете встречать такие варианты решений. Изучение различных ме тодов может помочь разобраться в фундаментальных идеях и помочь вам адаптироваться к локальным особенностям системы. Наконец, я представляю Unix в контексте действующих программных проектов. Unixэто система идей и средств, созданных людьми, которые искали решение реальных про блем. Мы начали рассмотрение с реальных проблем и наблюдали, как идеи приводят к на хождению решений. Unix воспринимается осмысленно, когда вы видите, что составные части работают совместно, как система.
Заключение •
• • •
Вычислительная система состоит из нескольких типов ресурсов, таких, как дисковая память, память, периферийные устройства и сетевые средства. Программы используют эти ресурсы для хранения, пересылки и обработки данных. Вычислительные системы, где одновременно работают несколько программ несколь ких пользователей, требуют наличия централизованной управляющей программы. Ядро Unix представляет собой программу, которая планирует исполнение программ и управляет доступом к ресурсам. Пользовательские программы обращаются к ядру за ресурсами. Некоторые Unix - программы состоят из отдельных программ, которые разделяют или обмениваются данными. Написание системных программ требует понимания структуры и использования служб ядра.
Глава 2 Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Цели Идеи и средства • • • • •
Роль и использование электронной документации. Файловый интерфейс Unix: open, read, write, lseek, close. Создание и чтение файлов, запись в файлы. Дескрипторы файлов. Буферирование: пользовательский уровень и уровень ядра. Режим ядра, пользовательский режим и назначение системных вызовов. Как в Unix представлено время, как форматировать изображения времени в Unix. Использование файла utmp для определения списка текущих пользователе. Обнаружение ошибок в системных вызовах,и оповещение об ошибках.
Системные вызовы и функции open, read, write, creat, lseek, close perror
Команды • • • •
man who cp login
2.1. Введение Кто же использует систему? Не много ли пользователей? Вошел ли в систему мой друг? В каждой многопользовательской вычислительной системе есть команда who. Команда сообщает, кто работает на компьютере. Как работает эта команда? В этой главе мы будем изучать работу команды в Unix. По мере изучения мы узнаем, как в Unix можно вести обработку файлов. Дополнительно к информации об Unix, которую мы получаем при изучении, рассмотрим, как использовать систему Unix в качестве справочника об этой системе.
2.2 Вопросы, относящиеся к команде who
53
2.2. Вопросы, относящиеся к команде who Обратимся снова к представлению системы Unix.
Рисунок 2.1 Пользователи, файлы, процессы и ядро Большой ящик н& рисунке представляет память компьютера. Он разделен на пользова тельское и системное пространство. Пользователи соединены с системой через термина лы. В этой системе есть два твердых диска, изображенное в виде больших цилиндров, и один принтер. В пользовательском пространстве исполняются различные программы. Они связываются с внешним миром через ядро. Эти коммуникационные каналы на рисун ке представлены в виде линий связи процессов с ядром. По нашему плану мы будем изучать команду who. Поэтому возникают вопросы: 1. Что делает команда who? 2. Как работает команда who? 3. Могу ли я написать программу who?
2.2.1. Программы состоят из команд Прежде чем начинать рассмотрение, важно отметить, что почти все команды Unix типа who и Is - это просто программы, которые были написаны некоторыми программистами, обычно на С. Когда вы набираете на клавиатуре Is, то обращаетесь к командному ин терпретатору shell, чтобы он запустил на исполнение программу с именем Is. Программа Is при исполнении выводит список файлов в каталоге. Если вы не удовлетворены тем, что делает команда Is, то можете написать собственную версию этой команды и использовать ее вместо исходной версии. Добавить новые команды в Unix очень просто. Вы пишете новую программу и должны поместить исполнимый файл для хранения в один из стандартных каталогов, таких, как /bin, /usr/bin, /usr/local/bin. Многие команды в Unix появились как программы, которые ктото написал для решения некоторой частной задачи. Другие пользователи сочли такие программы полезными. И тогда такие программы мож но встретить на каком-то числе Unix - машин. Поэтому у вашей версии программы who есть шанс стать когда-нибудь стандартной.
54
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
2.3. Вопрос 1: Что делает команда who? Если нам нужно узнать, кто в текущий момент работает в системе, то мы должны набрать команду who: $ who heckerl nlopez dgsulliv one.net) ackerman wwchen ■ barbier ramakris czhu bpsteven molay
ttypl ttyp2 ttyp3
Jul 2119:51 Jul 21 18:11 Jul 21 14:18
(tide75.surfcity.com) (roam163-141.student.ivy.edu) (h004005a8bd64.ne.media-
ttyp4 ttyp5 ttyp6 ttyp7 ttyp8 ttyp9 ttypa
Jul 15 22:40 Jul 21 19:57 Jul 8 13:08 Jul 13 08:51 Jul 21 12:47 Jul 21 18:26 Jul 21 20:00
(asd1-254.fas.state.edu) (circle.square.edu) (labpcl 8.elsie.special.edu) (roam157-97.student.ivy.edu) (spa.sailboat.edu) (207.178.203.99) (xyz73-200.harvard.edu)
'
$
Каждая строка этого протокола представляет одну сессию (одно вхождение в систему). В начале строки расположено пользовательское имя (username). Далее выводится имя терминала, через который пользователь вошел в систему. В следующей части строки выво дится информация о том, когда пользователь вошел в систему. Последняя часть строки пред назначена для обозначения, где находится пользователь, вошедший в систему. В некоторых версиях команды who информация об имени удаленного компьютера не выводится, если вы ее явно не затребовали. '
2.3.1. Обращение к справочнику При запуске команды who мы получаем определенную информацию о том, что эта команда делает. Для более подробного изучения вопроса о назначении команды можно обратиться к электронному справочнику. Каждая из систем Unix поступает с документацией обо всех командах. Иногда система Unix Поступает с печатным справочником, где для каж-дой команды представлена документация в одну или две страницы. Чаще всего теперь спра вочник расположен на диске. Команда для чтения информации из справочника - man1. Для получения описания команды who следует выполнить команду: $ man who who(1) NAME who - Identifies users currently logged in SYNOPSIS who [-a] |[-AbdhHlmMpqrstTu] [file] who ami who am I whoami
who(1)
The who command displays information about users and processes on the local system. 1. В некоторых версиях Unix реализована традиционная шап-документация со ссылками на основе использования системы info или справочник представлен как набор взаимосвязанных страниц HTML.
55
2.3. Вопрос 1: Что делает команда who?
STANDARDS Interfaces documented on this reference page conform to industry standards as follows: who: XPG4, XPG4-UNIX Refer to the standards(5) reference page for more information about industry standards and associated tags. OPTIONS -a Specifies all options; processes /var/adm/utmp or the named file with all options on. Equivalent to using the -b, -d, -I, -p, -r, -t, -T, and -u options, more (10%)
Все страницы руководства, которые часто называют manpages, имеют одинаковый базо вый формат. Заголовок служит для представления имени команды и обозначает раздел справочника, в котором находится данный документ. В данном примере это изображается как who (1), что обозначает команду who и раздел 1. В разделе 1 содержится документация обо всех пользовательских командах. Обратитесь к справочнику на вашей системе и по смотрите, что находится в других разделах справочника. Секция name на странице документации содержит имя команды и однострочное представ ление назначения команды. Секция synopsis представляет, как можно использовать команду. Здесь показано, что сле дует набирать при вызове команды, список аргументов и опций, которые возможно использовать при вызове команды. Каждая опция обычно начинается со знака дефиса, за которым следуют один или более символов. С помощью опций можно указывать вари ант исполнения команды. В тексте страницы справочника можно использовать квадратные скобки ([-а]), чтобы по казать, что данный элемент не является обязательным для команды, но может быть при не обходимости включен в текст командной строки при вызове команды. В примере страни цы документации для команды who показано, что вы можете обращаться к команде просто набором ее имени who, или можете набрать: who -а (произносится who минус а), или вы мо жете набрать who с последующим набором знака “минус” и некоторой комбинации симво лов, затем указать имя файла, которое вам понравится. На странице документации для команды who представлены еще три формы обращения к команде: who am i who am I whoami
Вы можете прочитать о назначении этих альтернативных форм вызова команды в спра вочнике или попытаться поработать с ними. В секции description находится описание того, что делает команда. Эти описания весьма сильно варьируются от команды к команде, от одной версии Unix к другой. Некоторые тексты описаний краткие, но точные. В некоторых описаниях представлено большее чис ло деталей и несколько примеров. В любом случае описания представляют все свойства команды и содержат надлежащие авторитетные ссылки. В секции options представлен список допустимых опций и описание, для чего предна значена каждая опция. В давние времена каждая команда в Unix была простой. Каждая вы полняла некоторое действие и имела одну или две опции. С годами многие команды были vcoRennieHCTRORaHw
за счет ввеления в
их
состав новых возможностей, каждая из котооых
56
Пользователи, файлы и справочник; Что рассматривать в первую очередь?
может быть активизирована с помощью опций при обращении к команде с уровня команд ной строки. Некоторые команды, подобные рассматриваемой версии команды who, имеют весьма много опций. В секции see also представлен список тем в справочнике, связанных с командой. В неко торых страницах справочника есть еще секция bugs.
2.4. Вопрос 2: Как работает команда who? Нами было установлено, что команда who отображает информацию о тех пользователях, которые к текущему моменту вошли в систему. На странице документации для команды who дано описание того, что может делать эта команда и каким образом заставить ее вы полнить допустимое для нее действие. Как работает команда who? Как она выполняет до пустимые для нее действия? Можно предположить, что системные программы, подобные who, используют специаль ные системные функции. В том числе, возможно, они включают расширенные привилегии администратора. Вам может потребоваться получить доступ к средствам системного разработчика, включая доступ к CD-ROM, толстым книгам и секретным кодам. Все это может потребовать каких-то расходов. Вся документация о функционировании команды who находится в самой системе. Вам только нужно знать, где следует искать документацию.
Изучение Unix из Unix Вы можете изучтъ принципы работы любой команды, используя для этого четыре воз можности: • Чтение справочника. ' • Поиск в справочнике.. • Чтение файлов с именами, имеющими расширение .h. • Использование ссылок из секции see also. Мы будем далее использовать эти возможности для изучения команды who. Чтение из справочника Для изучения команды who следует набрать $ man who
и обратиться к секции DESCRIPTION. В справочнике для SunOS текст этой секции будет иметь такой вид: . DESCRIPTION The who utility can list the user’s name, terminal line, login time, elapsed time since activity occurred on the line, and the process-ID of the command interpreter (shell) for each current UNIX system user. It examines the /var/adm/utmp file to obtain its information. If file is given, that file (which must be in utmp(4) format) is exam-ined. Usually, file will be /var/adm/wtmp, which contains a history of all the logins since the file was last created.
2.4 Вопрос 2: Как работает команда who?
57
Из данного документа мы получаем полную информацию о команде. Команда who про веряет файл /var/adm/utmp для извлечения для себя из него необходимой информации. Из текста описания следует, что список текущих пользователей хранится в этом файле. Команда читает файл. Что нам следует знать об этом файле? Для этого мы можем обра титься к справочнику и найти необходимую информацию.
Поиск в справочнике Команда man допускает возможность обращения к справочнику для организации поиска по ключевым словам. Для организации поиска следует использовать опцию -к. Чтобы получить информацию о ’utmp’, следует выполнить: $ man -k utmp endutent endutxent getutent getutid getutline getutmp getutmpx getutxent getutxid getutxline pututline pututxline setutent setutxent ttyslot
getutent (3c) getutxent (3c) getutent (3c) getutent (3c) getutent (3c) getutxent (3c) getutxent (3c) getutxent (3c) getutxent (3c) getutxent (3c) getutent (3c) getutxent (3c) getutent (3c) getutxent (3c) ttyslot (3c)
updwtmp updwtmpx utmp utmp2wtmp
getutxent (3c) getutxent (3c) utmp (4) acct(1m)
utmpd utmpname utmpx utmpxname wtmp wtmpx
utmpd (1m) getutent (3c) utmpx (4) getutxent (3c) utmp (4) utmpx (4)
-access utmp file entry -access utmpx file entry -access utmp file entry -access utmp file entry -access utmp file entry •access utmpx file entry -access utmpx file entry -access utmpx file entry -access utmpx file entry -access utmpx file entry -access utmp file entry -access utmpx file entry -access utmp file entry -access utmpx file entry -find the slot in the utmp file of the current user -access utmpx file entry -access utmpx file entry utmp and wtmp entry formats overview of accounting and miscellaneous accounting commands -utmp and utmpx monitoring daemon -access utmp file entry utmpx and wtmpx entry formats -access utmpx file entry utmp and wtmp entry formats utmpx and wtmpx entry formats
$ Полученный результат работы команды был получен на SunOS. Такой вывод будет пред ставлен в аналогичном виде и на других инсталляциях. Каждая строка в этом выводе содержит тему, название страницы справочника и краткое описание. Те строки, которые помечены метками utmp и wtmp, возможно, представляют то, что нам необходимо. Другие записи с похожими метками могут нам понадобиться позже. Нотация utmp (4) означает, что документация по utmp находится в разделе 4 справочника. Этот номер раздела следует использовать при обращении к команде man:
58
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
$ man 4 utmp utmp(4) utmp(4) ‘ NAME utmp, wtmp - Login records SYNOPSIS «include
DESCRIPTION The utmp file records information about who is currently using the system. The file is a sequence of utmp entries, as defined in struct utmp in the utmp.h file. The utmp structure gives the name of the special file associated with the user’s terminal, the user's login name, and the time of the loqin in the form of time(3). The ut_type field is the type of entry, which can specify several symbolic constant values. The symbolic constants are defined in the utmp.h file. The wtmp file records all logins and logouts. A null user name indicates a logout on the associated terminal. A terminal referenced with a tilde (-) indicates that the system was rebooted at the indicated time. The adjacent pair of entries with terminal names referenced by a vertical bar (j) or a right brace (}) indicate the system-maintained time just before and just after a dale command has changed the system’s time frame. The wtmp file is maintained by login(1) and init(8). Neither of these pro-grams creates the file, so, if it is removed, record keeping is turned off. See ac(8) for information on the file. FILES /usr/include/utmp.h /var/adm/utmp more (88%)
Мы достаточно быстро ответили на вопрос, как работает команда who. На первой страни це документации по команде who сказано, что команда читает файл utmp. Здесь сказано, что файл utmp представляет собой последовательность записей utmp, которые определе ны в структуре utmp в файле utmp.h. Где же находится этот файл utmp.h? Нам повезло. В разделе FILES на странице документации есть нужная информация. Там указано маршрутное имя файла /usr/include/utmp.h. Прежде чем перейти к рассмотрению следующей возможности для работы со справочни ком (чтение файлов с расширением имен .h ), обратимся еще к некоторой информации на данной странице документации. Речь идет о файле wtmp, куда происходит запись обо всех входах в систему и выходах из системы. Для работы с файлом указаны ссылки на команды login(l), init(8) и ас(8). Их рассмотрение будет интересно при изучении тем, которые будут представлены позже. Изучение Unix по справочнику аналогично поиску информации о каком-то объекте в Web. По мере чтения различных страниц справочника вы находите дополнительные ссылки, с помощью которых можете обратиться к интересующим вас и полезным для вас темам. Именно так, в соответствии с нашими задачами, мы подошли к изучению файла .
2.4. Вопрос 2: Как работает команда who?
59
Чтение файлов .h
В документации по utmp сказано, что структура записей в файле utmp описана в файле /usr/include/utmp.h. В большинстве Unix - машин заголовочные файлы для системной ин формации хранятся в каталоге, который называется /usr/indude. Когда С — компилятор обнаруживает в тексте программы строку вида: «include <stdio.h>
он предполагает, что этот файл находится в каталоге /usr/include. Используем команду more для прочтения содержимого этого файла: $ more /usr/include/utmp.h ' «define UTMP_FIUE "/var/adm/utmp" «define WTMP_FILE "/var/adm/wtmp" «include <sysAypes.h> /* for pid t, time t */
Г * Структуры файлов utmp и
*
*7 «define utjiame ut_user struct utmp { char ut_user[32]; char ut_id[14];
I* совместимость */ Г Пользовательское входное имя */ Г /etc/inittab id- IDENT.LEN в
* init */ . char ut_line[32]; short ut.type; pid_t ut_pid; struct exit_statys { short ejermination; short e_exit; } ut_exit;
Г имя устройства (console, Inxx) */ /* тип записи 7 Г идентификатор процесса 7 Г статус окончания процесса 7 Г статус процесса при выполнении exit */ Г Статус exit процесса, помеченного как
♦DEAD PROCESS. ' 7
time_t ut_time; char ut_host[64J;
Г временная отметка о сделанной записи */ I* имя хоста, такое же как
* MAXHOSTNAMELEN 7
}; Г Определения для ut_type */
utmp.h (60%)
В начале в данном выводе пропущен ряд сообщений и другой вводный материал. Далее мы обнаруживаем определение структуры. Оказывается, записи о вхождениях в систему состоят из восьми элементов. Поле ut_user предназначено для хранения пользовательско го имени. В массиве ut_line помещается информация об устройстве, что в данном случае будет означать терминал, через который пользователь соединен с системой. Через не сколько строк в структуре представлено поле ut_time, где хранится время вхождения в систему, а поле ut_host предназначено для хранения имени удаленного компьютера.
60
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
В рассматриваемой структуре есть еще и другие элементы. Они напрямую не используют ся для отображения информации командой who, но могут быть полезными в других ситуа циях. Структура записи utmp на вашей системе может отличаться от рассмотренной. Но файл ut mp.h на вашей системе будет описывать формат данных utmp для вашей системы. Имена полей обычно одинаковы для различных версий Unix, но наличие поля, которое имеет комментарий “совместимость”, показывает, что они иногда могут и отличаться. Заголо вочные файлы обычно снабжены хорошими комментариями, которые содержат полезную информацию.
2.4.1. Мы теперь знаем, как работает who При чтении электронной документации по темам who и utmp и просмотре заголовочного файла /usr/include/utmp.h, мы изучили, как работает команда who. Команда who читает структуры из файла. Файл содержит для каждой сессии по одной структуре. Мы изучили формат структуры. Поток информации изображен на рисунке 2.2.
Рисунок 2.2 Поток данных для команды who Файл - это массив, откуда who может читать записи и выводить требуемую информацию. По самой простой логике следовало бы читать и выводить записи по одной. Было бы это проще? Мы не рассматривали исходный код для версии команды who, но у нас была воз можность изучить все, что касается команды, из электронной документации. Из спра вочника мы узнали, что делает команда, и также рассмотрели, как используется структура данных в заголовочном файле. Единственная возможность проверить, действительно ли вам все понятно, — это попытаться сделать что-то самому.
2.5. Вопрос 3: Могу ли я написать who? В следующей части этой главы мы попытаемся создать программу, которая должна рабо тать аналогично стандартной команде who. Мы продолжим обучение, используя обраще ние к справочнику, и проверим нашу программу, сверяя ее вывод и вывод из версии коман ды who на нашей системе. Проведенный анализ программы who показал, что есть только две задачи, которые необходимо выполнять в программе: • Чтение структур из файла. • Отобоажение инсЬоомации. котооая хоанится в стоуктуое.
2.5Вопрос 3: Могу ли я написать who?
61
2.5.1. Вопрос: Как я буду читать структуры из файла? Для чтения символов и строк из файла вы можете использовать getc и f gets . Что пред ставляют собой структуры с позиций данных? Мы можем использовать getc для посим вольного чтения, но это довольно скучное занятие. Хотелось бы читать сразу всю струк туру с диска.
Давайте почитаем справочник! Нам необходимо найти страницы справочника, относящиеся к file и read. С помощью оп ции -к можно задавать только одно ключевое слово, поэтому мы укажем только одно из ключевых слов и выполним. $ man -k file
для просмотра предлагаемых тем. Нам будет выдан перечень тем, касающихся файлов. На моей системе по этой команде был получен результирующий вывод из 537 строк. Из этих строк нам нужно выбрать строки, где содержится слово “read”. В Unix есть коман да grep, \ которая будет выводить строки, где содержится заданный шаблон. Используем в конвейере команду grep следующим образом: $ man -k file | grep read Jlseek (2) - reposition read/write file offset fileevent (n) - Execute a script when a channel becomes readable or writable gftype(l) - translate a generic font file for humans to read lseek (2) - reposition read/write file offset macsave(l) - Save Mac files read from standard input read (2) - read from a file descriptor readprofile (1) - a tool to read kernel profiling information scr_dump, scr_restore, scrjnit, scr_set (3) - read (write) a curses screen from (to) a file tee (1) - read from standard input and write to standard output and files $
Наиболее значимую информацию среди этих строк содержит read(2). В других строках речь идет о других темах. Выберем страницу документации в разделе 2 относительно read: $ man 2 read READ(2) System calls READ(2) NAME read - read from a file descriptor SYNOPSIS «include ssize_t read(int fd, void *buf, size_t count); DESCRIPTION read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf. If count is zero, read() returns zero and has no other
62
Пользователи, файлы и справочник. Что рассматривать в первую очередь? results. If count is greater than SSIZE_MAX, the result is unspecified. RETURN VALUE On success, the number of bytes read is returned (zero indicates end of file), and the file position is advanced by this number. It is not an error if this number is smaller than the number of bytes requested; this may hap-pen for example because fewer bytes are actually available right now (maybe because we were close to end-of-file, or because we are reading from a pipe, or from a terminal), or because read() was interrupted by a signal. On error, -1 is returned, and errno is set appropriately. In this case it is left unspecified whether the file position (if any) changes.
С помощью этого системного вызова мы можем прочитать заданное число байт из файла в буфер. Нам необходимо счи“гать за один раз. одну структуру, поэтому мы можем исполь зовать sizeof (struct utmp) для определения того числа байтов, которое необходимо прочитать. В найденной документации сказано, что системный вызов read производит чтение из файлового дескриптора. Как мы можем получить один из них? При просмотре страницы документации относительно read мы обнаружим в последней ее части сле дующее: RELATED INFORMATION (called SEE ALSO in some versions) Functions: fcntl(2), creat(2), dup(2), ioctl(2), getmsg(2), lockf(3), lseek(2), mtio(7), open(2), pipe(2), poll(2), socket(2), socketpair(2), termios(4), streamio(7), opendir(3) lockf(3) Standards: standards(5)
Здесь мы обнаруживаем ссылку на ореп(2). Запускаем на исполнение команду: man 2 open,
чтобы прочитать, как работает open. Из этой страницы документации есть ссылка на close. Итак, при работе с электронным справочником мы нашли три части, которые необходимы нам для чтения структуры из файла.
2.5.2. Ответ: Использование open, read и dose Мы можем использовать эти три системных вызова для извлечения из файла utmp записей о вхождениях в систему. Страницы справочника, касающиеся этих тем, могут быть весьма краткими по содержанию. Эти системные вызовы имеют много опций и достаточно слож ны в своем поведении, когда они используются в отношении программных каналов, устройств и других источников данных. Основополагающие факторы выделяются и рас сматриваются далее.
Открытие файла: open Системный вызов open создает связь между процессом и файлом. Эта связь называется де скриптором файла и изображается на рисунке 2.3 в виде туннеля от процесса к ядру.
2.5Вопрос3:Могулиянаписатьwho?
63
Рисунок 2.3 Дескриптор файла - это соединение с файлом. Основные свойства системного вызова open:
Для открытия файла необходимо определить имя файла и тип желаемой связи. Сущест вуют три типа связей - соединение для чтения, соединение для записи и соединение для чтения и записи. В заголовочном файле /usr/include/fcntl.h находятся определения для макросов O.RDONLY, OWRONLY и O.RDWR. Открытие файлов - это служба ядра. Системный вызов open - это требование, которое выдает ваша программа ядру. Если ядро обнаружит ошибку при обращении к нему, то оно вернет код возврата, равный -1. Есть несколько видов ошибок. Может случиться, что указанный файл не существует. Файл может существовать, но у вас нет прав доступа на чтение из этого файла. Файл может находиться в каталоге, к которому у вас нет доступа. В странице документации по систем ному вызову open приведен список подобного рода ошибок. Способы обработки ошибок будут изучены далее в этой главе. Что происходит, если файл уже был открыт? То есть что будет в ситуации, когда другой процесс уже работает с файлом? В Unix не устанавливается запрета на одновременное от крытие несколькими процессами одного и того же файла. Если бы такое ограничение су ществовало, то двум различным процессам нельзя было бы запустить одновременно одну и ту же команду who. Если открытие происходит успешно, то ядро возвращает процессу небольшое по значению целое положительное число. Это число называют дескриптором файла, который является по смыслу идентификатором соединения процесса с файлом.
64
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Вы можете одновременно открыть несколько файлов. При этом для каждого соединения будет установлен уникальный дескриптор файла. Ваша программа даже может многократ но отрыть один и тот же файл. При этом для каждого соединения будет установлен свой дескриптор файла. Вы можете использовать дескриптор файла для всех операций с установленным соедине нием.
Чтение данных из файла: read Вы можете в процессе производить чтение данных, используя дескриптор файла: read НАЗНАЧЕНИЕ
Пересылка qty байт из файлового дескриптора fd в буфер
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ ssizeJ numread = read(int fd, void *buf, sizej qty) АРГУМЕНТЫ
fd - источник данных buf - место для сохранения данных qty - количество байт для передачи
КОДЫ ВОЗВРАТА
-1 - при ошибке Целое число - при успехе
С помощью системного вызова read происходит обращение к ядру для передачи qty байтов данных из файлового дескриптора fd в массив buf, который находится в пространстве памяти вызывающего процесса. Ядро выполняет действие по запросу и возвращает информацию о результате выполнения. Если требование не было выполнено, то код воз врата будет равным -1. В противном случае в качестве кода возврата будет число байтов, переданных при чтении. Почему можно получить в ответ меньшее число байтов, чем было запрошено? В файле мо жет не быть столько байтов, сколько вы указали при обращении к системному вызову. Например, если вы запросили 1000 байтов, а в файле содержится только 500 байтов, то по сле выполнения вызова вы увидите в качестве результата 500 байтов. При достижении конца файла системный вызов вырабатывает код возврата, равный нулю, поскольку нет данных для чтения. Какового сорта ошибки может фиксировать системный вызов read? Ответ можно найти на странице документации в вашей системе, где приведен перечень ошибок.
Закрытие файла: close Когда вы прочитали данные или записали данные через файловый дескриптор, то вы мо жете закрыть его. Системный вызов close представлен такими характеристиками: close НАЗНАЧЕНИЕ INCLUDE
Закрытие файла #indude
ИСПОЛЬЗОВАНИЕ
int result = close(intfd)
АРГУМЕНТЫ
fd - дескриптор данных buf - место для сохранения данных qty - количество байт для передачи
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
2.5 Вопрос 3: Могу ли я написать who?
65
Системный вызов close отключает соединение, которое было определено с помощью файло вого дескриптора fd. При обнаружении ошибки системный вызов close возвращает код воз врата ~1. Например, при попытке закрыть файловый дескриптор, который не ссылается на открытый файл, будет выработана ошибка. Другие виды ошибок описаны в справочнике.
2.5.3. Написание программы who 1. с Итак, мы почти у цели. Мы знаем суть работы команды who, мы знаем о существовании трех системных вызовов, необходимых для установления связи с файлом, выбора данных из файла и для закрытия файла. Ведущая часть кода программы будет выглядеть так: Г whol .с - первая версия программы who * выполнить open, прочитать файл UTMP и показать результаты 7 «include <stdio.h> «include «include «include «define SHOWHOST int main()
/* подключить удаленную машину для вывода 7
{ struct utmp current_record; f* считывать сюда данные 7 int utmpfd; /* читать из этого дескриптора */ int reclen = sizeof (cu rrent_record); if ((utmpfd = open(UTMP RLE. 0 RDONLY)) == -1){ perror(UTMP_FILE); /* UTMP.FILE - описание в utmp.h */ exit(1);
)-
while (readfutmpfd, ¤t_record, reclen) == reclen) show_info(¤t_record); close(utmpfd); return 0; /* все нормально 7
} В этой программе реализована логика, которая была рассмотрена выше в этой главе. В цикле while производится последовательное чтение записей из файлового дескриптора currentjecord. Функция showjnfo отображает информацию о вхождениях в систему. Про грамма работает в цикле до тех пор, пока системный вызов read в состоянии читать записи
из файла. Наконец, происходит закрытие файла и выход из программы. Системный вызов реггог является удобным средством для оповещения о наличии систем ных ошибок. Мы рассмотрим его далее в этой главе.
2.5.4. Отображение записей о вхождениях в систему Далее приведен код первого наброска функции show info, которая производит отображе ние информации из файла utmp. Г ж show info()
66
Пользователи, файлы и справочник. Что рассматривать в первую очередь? отображает содержимое структуры utmp в формате, удобном для восприятия * эти размеры аппаратно не зашиты 7 show info(struct utmp *utbufp)
{ printf("%-8.8s", utbufp->ut_name); printf(""); printf("%-8.8s", utbufp- >ut_line); printff"); printf("%10ld”, utbufp- >ut_time); printf(""); #ifdef SHOWHOST printf("(%s)", utbufp->ut_host); #endif printf("\n");
Г входное имя *1 Г пробел */ f терминал */ Г пробел*/ /* время вхождения */ /* пробел */ /* хост */ /* перевод на новую строку 7
} Мы выбрали в этой программе ширину полей для printf так, чтобы было соответствие с длинами строк вывода системной версии программы who. Программа выводит элемент uttime в формате long int. Значение time_t определено в заголовочном файле, но мы пока ничего об этом не знаем. Компилируем и запускаем программу на исполнение:
$ccwho1.c-owho1 $who1 system b 952601411 0 run-leve 952601411 () 952601416() 9526014160 952601417() 952601417 0 952601419 () 9526014190 952601423 () 952601566 0 LOGIN console 952601566 () ttypl 958240622 0 shpyrko ttyp2 964318862 (nasi -093.gas.swamp.org) acotton ttyp3 964319088 (math-guest04.williams.edu) ttyp4 964320298 0 spradlin ttyp5 963881486 (h002078c6adfb.ne.rusty.net) dkoh ttyp6 964314388(128.103.223.110) spradlin ttyp7 964058662 (h002078c6adfb.ne.rusty.net) king ttyp8 964279969 (blade-runner.mit.edu) berschba ttyp9 964188340 (dudley.leamed.edu) rserved ttypa 963538145 (gigue.eas.ivy.edu) rlahel ttvnh
1QARR I roam 10Я- 97 Qti irlpnt ctafp pHi
2.5 Вопрос В: Могу ли я написать who?
67
ttypc 964319645 0 rserved ttypd 963538287 (gigue.eas.ivy.edu) dkoh ttype 964298769(128.103.223.110) ttypf 964314510 0 964310621 (xyz73-200.harvard.edu) molay' ttyqO ttyql 964311665 0 964310757() ttyq2 964304284() ttyq3 ttyq4 964305014 0 ttyq5 964299803 () ttyq6 964219533 0 ttyq7 . 964215661 () cweiner ttyq8 964212019 (roaml 75-157.student.stats.edu) ttyqa 964277078 () ttyq9 964231347 ()
$ Давайте сравним вывод нашей программы с выводом системной версии команды who: $ who shpyrko ttyp2 Jul acotton ttyp3 Jul spradlin ttyp5 Jul dkoh ttyp6 Jul spradlin ttyp7 Jul king ttyp8 Jul berschba ttyp9 Jul rserved ttypa Jul dabel ttypb Jul rserved ttypd Jul dkoh ttype Jul molay ttyqO Jul cweiner ttyq8 Jul
22 22:21 22 22:24 17 20:51 22 21:06 19 22:04 2211:32 21 10:05 13 21:29 22 22:30 13 21:31 2216:46 22 20:03 21 16:40
(nasi -093.gas.swamp.edu) (math-guest04.williams.edu) (h002078c6adfb.ne.rusty.net) (128.103.223.110) (h002078c6adfb.ne.rusty.net) (blade-runner.mit.edu) (dudley.learned.edu) (gigue.eas.ivy.edu) (roam 193-27.student.state.edu) (gigue.eas.harvard.edu) (128.103.223.110) (xyz73-200.harvard.edu) (roam 175-157.student.stats.edu)
$ Наша версия выглядит как перспективная, но все еще не в полном виде. Есть еще шерохо ватости, которые следует ликвидировать. У нас выводятся те же пользовательские имена, как и в who. У нас выводятся Правильные имена терминалов, правильно указываются имена удаленных машин. Но есть две проблемы.
Что нам следует еще сделать: Подавить пустые записи. Получить корректное представление времени вхождения в систему.
2.5.5. Написание версии who2.c В версии 2 нашей программы who внимание уделяется двум проблемам, о которых шла речь в версии 1. И вновь мы будем решать эти проблемы, обращаясь к необходимым документам спра вочника и заголовочным файлам.
68
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Подавление пустых записей В реальной версии команды who выводится список пользовательских имен тех пользовате лей, которые входили в систему. В нашей версии программы выводится список из того, что программа находит в файле utmp. Файл utmp содержит записи, касающиеся всех терми налов, даже тех, которые не используются. Необходимо изменить нашу программу так, чтобы она не выводила записи о неиспользуемых терминальных линиях. Но как опреде лить, какая из utmp - записей не представляет активную сессию? Самое простое решение (которое не работает) - пропускать записи с пробелами в поле пользовательского имени. Это будет работать в большинстве случаев, но на экран не будет выводиться запись с полем LOGIN в строке, которая относится к консоли. Лучшим решением (которое работает) будет, если выбирать для вывода только те utmp - записи, которые соответствуют пользователем, вошедшим в систему. Обратимся к файлу /usr/include/utmp.h и мы обнаружим там следующее: Г Определения для ut_type 7 #define EMPTY #define RUN LVL #define BOOT TIME #define OLD flME #define NEWJIME #define INIT_PROCESS #defme LOGlN_PROCESS #define USER PROCESS #define DEAD PROCESS
О 1 2 3 4 5 /* Процесс был порожден процессом "init" 7 6 /* Процесс "getty" ждет login 7 7 /* Пользовательский процесс 7 8
Этот список весьма полезен. В каждой записи есть поле с именем utjype. Значения, ко торые могут находиться в этом поле, и их символические имена представлены в приведен ном выше списке. Тип 7 будет для нас счастливым номером. Если теперь мы сделаем ни жеследующие небольшие изменения в нашей функции showjnfo, то пробельные записи" должны исчезнуть: show info{struct utmp *utbufp)
{
if (utbufp->ut_type != USER_PROCESS) /* только пользователи! 7 return; printfryo-e.es”, utbufp->ut_name); Г имя пользователя 7
Отображение времени вхождения в систему в удобном для прочтения виде Теперь решим проблемы представления времени в формате, который воспринимаем людьми. Начнем поиск в справочнике и поиск заголовочных файлов. Страниц по теме “time” во всех версиях Unix весьма много, и они разнообразны. После набора $ man -k time получим много записей. На одной своей машине я получил 73 записи, а на другой машине получил 97. Вы можете просмотреть этот длинный список или можете отфильтровать по лученный вывод. Следующие ниже конвейеры прекрасно проведут фильтрацию: $ man -k time | grep transform $ man -к time j grep -i convert
2.5 Вопрос 3: Могу ли я написать who?
/
69
Через справочник выходим на необходимые заголовочные файлы. Файл /usr/include/time.h есть на ряде систем Unix. Проверьте вашу систему относительно информации, касающейся темы ■‘time”. Нам же нужно обсудить вопрос Как в Unix хранится значение времени: тип данных timej
В Unix значение времени представляется целым числом, которое измеряет в секундах ин тервал времени с полуночи первого января 1970 года по Гринвичу. Тип данных time_t это целочисленное представление времени в секундах. Этот формат в Unix используется во многих приложениях. Поле ut_time в utmp записях содержит время вхождения в сис тему, которое представлено числом секунд с начала Эпохи. Преобразование timej в читаемый формат: ctime
Есть функция ctime, которая преобразует значение времени в секундах от начала работы системы Unix в значение времени в читабельном формате. Функция описана в разделе 3 электронного справочника. $ man 3 ctime CTIME(3) Linux Programmer’s Manual CTIME(3) NAME asctime, dime, gmtime, localtime, mktime - transform binary date and time to ASCII SYNOPSIS «include char *asctime(const struct tm *timeptr); char *ctime(const time_t *timep); struct tm *gmtime(const timej *timep); struct tm *localtime(const timej *timep); timej mktime(struct tm *timeptr); extern char *tzname[2]; long int timezone; ' extern int daylight; DESCRIPTION The ctime(), gmtime() and localtime() functions all take an argument of data type timej which represents calendar time. When interpreted as an absolute time value, it rep-resents the number of seconds elapsed since 00:00:00 on January 1,1970, Coordinated Universal Time (UTC). The ctime() function converts the calendar time timep into a string of the form "Wed Jun 30 21:49:08 1993\n" The abbreviations for the days of the week are Sun, Mon, Tue, Wed, Thu, Fri, and Sat. The abbre-viations for the months are Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, and Dec. The return value points to a statically allocated string which might be overwritten by subsequent calls to any of the date and time functions. The function also
70
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Ш Вот это то, что нам необходимо. Мы имеем значение time_t в записях utmp . А нам тре буется строка в формате, подобном такому: Jun 30 21:49
Функция ctime(3) выбирает указатель на timej, а при окончании возвращает указатель на строку, которая будет выглядеть примерно так: Wed Jun 30 21:49:08 1993\п ЛЛЛЛЛЛАЛАЛЛЛ
Заметим, что строка, которая нам нужна для работы who, вставляется в строку возврата функции ctime. Это позволяет достаточно просто производить кодировку даты для who. Мы обращаемся к функции ctime и получаем после ее работы строку из 12 символов со смещением, равным 4. Это выполняется при выполнении оператора printf(“% 12.12s", ctime(&t) +4).
Одновременный вывод всего сразу Теперь мы знаем, как подавить пустые записи, и знаем, как отобразить значение ut_time в читабельном виде. Далее представлена окончательная версия программы who2.C: /* who2.c * *
- читает файл /etc/utmp и выводит список информации из него - подавляет пустые записи v - правильно форматирует время
7 «include <stdio.h> «include «include «include «include /* «define SHOWHOST 7 void showtime(long); void show_info(struct utmp *); int main()
{ struct utmp utbuf; int utmpfd;
/* сюда читается информация */ /* чтение происходит из этого дескриптора */
if((utmpfd = open(UTMP RLE, О RDONLY)) == -1){ perror(UTMP_RLE); exit(1);
} while(read(utmpfd, &utbuf, sizeof(utbuf)) == sizeof(utbuf)) show_info(&utbuf); close(utmpfd); return 0;
} Г * show info() * отображает содежимое структуры utmp
2.5 Вопрос 3: Могу ли я написать who?
71
* в удобном для восприятия виде * * ничего не отображает, если в записи нет имени пользователя*/ void show_info(struct utmp *utbufp)
{ if (utbufp->ut_type != USER.PROCESS) return; printf("%-8.8s", utbufp->ut_name); printff"); printf("%-8.8s", utbufp->ut_line); printff' ”); showtime(utbufp- >ut_time); tifdef SHOWHOST if (utbufp->ut_host[0] != ’\0') printff' (%s)", utbufp->ut_host); #endif printf( "\n");
/* входное имя */ /* пробел */ /* терминал */ /* пробел */ /* отображение времени */
/* хост */ /* перевод на новую строку */
} void showtime(long timeval)
Г * отображает время в формате, удобном для восприятия * использует функцию dime для формирования строки с изображением времени * Замечание: посредством формата %12.12s выводится строка из 12 символов, * при значении LIMITS, равно 12 символов.
*/ { char *ср; /* адрес со значением времени 7 ср = ctime(&timeval); Г преобразование значения времени в строку 7 /* строка должна иметь приблизительно такой вид */ Г Mon Feb 4 00:46:40 EST1991 */ Л 0123456789012345.*/ printf("%12.12s", ср+4); /* вывести 12 символов с позиции 4 */
}
Тестирование программы who2.c Рткомпилируем и запустим на исполнение программу who2.c. Для разнообразия вы ключим настройку SHOWHOST. Далее запустим на исполнение системную версию команды who и сравним полученные результаты: $ сс who2.c -о who2 $who2 rlscott acotton spradlin spradlin king
ttyp2 ttyp3 ttyp5 ttyp7 ttyp8
Jul 23 01:07 Jul 22 22:24 Jul 17 20:51 Jul 19 22:04 Jul 22 11:32
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
72 berschba ttyp9 rserved ttypa rserved ttypd molay ttyqO cweiner ttyq8 mnabavi ttyx2
Jul 21 10:05 Jul 1321:29 JuM 3 21:31 Jul 22 20:03 Jul 21 16:40 Apr 10 23:11
$ who rlscott ttyp2 acotton ttyp3 spradlin ttyp5 spradlin ttyp7 king ttyp8 berschba ttyp9 rserved ttypa rserved ttypd molay ttyqO cweiner ttyq8 mnabavi ttyx2
Jul 23 01:07 Jul 22 22:24 Jul 1720:51 Jul 19 22:04 Jul 22 11:32 Jul 21 10:05 Jul 13 21:29 Jul 13 21:31 Jul 22 20:03 Jul 21 16:40 Apr 10 23:11
$ Есть некоторое отличие в форматировании результатов. В различных версиях команды who используются различные по ширине колонки при выводе результатов. При изменении размеров колонок протокола вывода мы можем в точности добиться совпадения формата вывода по отношению к стандартному варианту. Можете заняться этим на своей системе. В некоторых версиях команды who производится вывод имени хоста для удаленной систе мы, если такая система была зафиксирована. В других же версиях такое имя не выводится. Программа выдает точный список пользователей, имена их терминальных линий и време на вхождения пользователей в систему. I
2.5.6. Взгляд назад и взгляд вперед Мы начали эту главу с постановки простого вопроса: “Как работает в Unix команда who?” Мы следовали в тексте трем сформулированным шагам. Во-первых, мы изучили, что де лает команда. Затем мы разобрались, посредством детального изучения технической доку ментации, как работает команда. Далее написали собственную версию программы, чтобы убедиться в том, что мы действительно понимаем, к^к работает команда. По мере нахождения решений на каждом из трех шагов мы научились использовать элект ронный справочник Unix и заголовочные файлы. Написание собственной версии програм мы привело к закреплению рассмотренного материала. Стала ясной структура файла utmp. Мы убедились в том, что каждое вхождение в систему приводит к появлению записи в журнале. Мы изучили, каким образом в Unix представляются временные величины. Это будет полезно при работе с другими частями Unix. Наконец, мы почитали документацию по надлежащим темам. На страницах справочника для файла utmp были найдены ссылки на файл wtmp. А со страниц справочника для функ ции ctime есть ссылки на другие функции, которые связаны со временем. Эти ссылки дают дополнительное представление о структуре системы.
2.6. Прова два: Разработка программы ср (чтение и запись)
73
2.6. Проект два: Разработка программы ср (чтение и запись) В программе who мы только читали из файла. А как можно будет записывать в файл? Для изучения возможности записи в файлы мы разработаем версию Unix команды ср.
2.6.1. Вопрос 1: Что делает команда ср? Команда ср выполняет копирование файла. Типичное обращение к команде будет таким: $ ср исходный _файл целевой_файл Если нет целевого файла, то команда ср создает его. Если целевой файл есть, то команда ср заменяет содержимое этого файла содержимым исходного файла.
2.6.2. Вопрос 2: Как команда ср создает файл и как пишет в него? Создание/транкатенация файла Один из способов создания файла или перезаписи файла является использование для это го системного вызова creat. Обобщенные характеристики системного вызова: creat НАЗНАЧЕНИЕ
Создание или уничтожение файла
INCLUDE
#include < fcntl.h >
ИСПОЛЬЗОВАНИЕ
int fd = creat(char ^filename, modej mode)
АРГУМЕНТЫ
filename: имя файла mode: права доступа
КОДЫ ВОЗВРАТА
-1 - при ошибке fd - при успехе
Системный вызов creat открывает файл с именем filename на запись. Если до этого не было файла с таким именем, то ядро создает файл. Если же есть файл с таким именем, то ядро уничтожает его содержимое, сокращая (транкатинируя) его размер до нуля. Если ядро создает файл, то оно устанавливает разряды прав доступа к файлу в соответст вии со значением второго аргумента2, который задается при обращении к системному вы зову. Например: fd = creatraddressbook”, 0644);
Будет создан или транкатенирован файл с именем addressbook. Если до этого файл не суще ствовал, права доступа будут такими: rw-r~r~: (Смотри детали в главе 3.) Если же файл с указанным именем существовал, то он становится пустым, а права доступа не меняются. В любом случае через файловый дескриптор fd файл будет открыт только на запись.
2. На самом деле разряды прав доступа модифицируются процессом с помощью системного вызова umask. См. главу 3.
74
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Запись в файл Передача данных в открытый файл производится с помощью системного вызова write: write НАЗНАЧЕНИЕ
Передача данных из памяти в файл
INCLUDE
#inc!ude < unistd.h >
ИСПОЛЬЗОВАНИЕ
ssizej result = writefint fd, void *buf, sizej amt)
АРГУМЕНТЫ
fd - файловый дескриптор buf - массив amt - количество байт для записи
КОДЫ ВОЗВРАТА
-1 - при ошибке Количество записанных байт - при успехе
Системный вызов write копирует данные из памяти процесса в файл. Если ядро не может ил и.не хочет копировать данные, то системный вызов write возвращает код -1. Если ядро переслало данные в файл, то системный вызов возвращает в качестве кода возврата ко личество байтов, переданных в файл. Почему может быть различие между количеством переданных байтов и тем значением, которое было заказано для передачи? Есть несколько обстоятельств, которые могут это прояснить. В системе может быть установлен предел на максимальный размер файла, который может создавать пользователь, или может быть недостаточно места на диске по отношению к затребованному значению. Если в системном вызове будет записано требо вание на размер, которое превышает предел или размер свободного пространства на дис ке, то системный вызов write запишет столько байтов, сколько он сможет, а затем остано вится. В вашей программе всегда необходимо сравнивать количество байтов, которое вы запрашиваете для пересылки в файл, с числом байтов, которое действительно туда было передано. Если эти значения оказываются разными, то программа должна предусматри вать реакцию на эту ситуацию.
2.6.3. Вопрос 3: Могу ли я написать программу ср? Проверим, насколько было все понятно, путем создания версии программы ср. Схема ра боты программы будет такой: открытие исходного файла для чтения открытие целевого файла на запись +- > чтение из исходного файла в буфер - • eof? |_ запись из буфера в файл закрыть исходный файл закрыть целевой файл
-+ |
<........... ..+
На рисунке 2.4 показаны потоки данных при копировании:
2.6. Проект два: Разработка программы ср (чтение и запись)
75
Рисунок 2.4. Копирование файлов посредством чтения и записи Файлы расположены на диске. Исходный файл находится слева на схематическом изобра жении диска, а целевой файл - справа. Буфер представляет собой область памяти в преде лах среды процесса. Процесс располагает двумя файловыми дескрипторами. Данные чи таются из исходного файла в буфер, а затем записываются из буфера в целевой файл. Наконец, программный код, который соответствует изображению на рисунке, будет та ким: Л* ср1.с * версия 1 программы ср - использует read и write при работе с буфером с * настраиваемым размером
*
* usage: ср1 src dest
*/ «include <stdio.h> . «include «include «define BUFFERSIZE 4096 «define COPYMODE 0644 void oops(char *, char *); main(int ac, char *av[])
{ int in_fd, out_fd, n_chars; char buf [BUFFERSIZE]; /* проверка аргументов */ if (ас != 3){ fprintf(stderr, "usage: %s source destination^", *av); exit(1);
} f открытие файлов */ if ((in_fd=open(av[1 ], O.RDONLY)) == -1) oopsC'Cannot open", av[1]);
76
Пользователи, фаты и справочник. Что рассматривать в первую очерщь? if {(out_fd=creat(av[2], COPYMODE)) ==-1) oops("Cannot creat", av[2]); Г копирование файлов */ while ((n_chars = read(in_fd, buf, BUFFERSIZE)) > 0) if (write(out_fd, buf, n_chars) != n_chars) oops("Write error to", av[2]); if (nchars == -1) oops("Read error from", av[1 ]); Г закрытие файлов */ if (close(in_fd) == -11| close(out_fd) == -1) oops("Error closing files","");
} void oops(char *s1, char *s2)
{ fprintf(stderr,"Error: %s", s1); perror(s2); exit(1);
} Откомпилируем и проверим работу программы: $ сс ср1 .с -о ср1 $ ср1 ср1 copy.of.cp1 $ Is -I ср1 copy.of.cp1 -rw-r--r--1 bruce bruce 37419 Jul 23 03:12 copy.of.cp1 • -rwxrwxr-x 1 bruce bruce 37419 Jul 23 03:08 cp1
$ cmp cp1 copy.of.cp1 $ С первого взгляда кажется, что все работает. Утилита cmp сравнивает два файла и при об наружении несовпадения по содержанию оповещает об этом. Поскольку разницы между указанными файлами нет, то нет и сообщения о несовпадении. А как наша программа будет реагировать на ошибочные ситуации? Сначала попытаемся снять копию с несуществующего файла, а затем записать копию в каталог. Получим такой результат: $ср1 ххх123 filel Error: Cannot open xxxl 23: No such file or directory
$ cp1 cp1 /tmp Error: Cannot creat Amp: Is a directory
Проверим другие ошибочные ситуации. Следует обратиться к документации по системно му вызову и посмотреть там, какие ошибки могут возникать при его выполнении. Затем нужно попытаться воспроизвести ошибочные ситуации. При этом проверяйте - не за тираете ли вы файлы, с которыми вам будет необходимо работать.
2.7. Увеличение эффективности файловых операций ввода/вывода: Буферирование
77
2.6\ 4. Программирование в Unix кажется достаточно простым Программа who - это программа, которая читает из файла и форматирует данные. Про грамма ср - это программа, которая читает один файл и производит запись в другой файл. Обе программы используют одни и те же базовые системные вызовы для установления связей с файлами и организации передачи данных в файлы и из файлов. Из справочника и из текстов заголовочных файлов мы будем извлекать всю информацию, которая будет необходима, чтобы понять, как писать такие программы. Программирование в Unix не выглядит слишком затруднительным. Следует ли пропус кать некоторые основополагающие вопросы? Давайте разберемся. Помимо трех вопросов, которые мы сформулировали в отношении Unix и программирования в Unix, есть еще один важный вопрос: что можно сделать, чтобы это работало лучше?
2.7. Увеличение эффективности файловых операций ввода/ вывода: Буферирование В программе ср1 содержится символьная константа BUFFERSIZE, которая задает размер мас сива в байтах, где содержатся данные по мере их передачи от исходного файла к целевому файлу. Это значение равно 4096. Возникает важный вопрос: какой размер буфера следует считать лучшим?
2.7.1. Какой размер буфера следует считать лучшим? Давайте порассуждаем. Если вы используете половник для разлива супа по тарелкам, то чем больше будет этот половник, тем меньше вам потребуется манипуляций с разливом и меньше времени. Рассмотрим файл длиной в 2500 байт. Можно выделить некоторые особенности при рабо те с ним: Ех: Размер файла = 2500 bytes Если buffer = 100 байт, тогда для копирования потребуется 25 системных вызовов read() и 25 write() Если buffer = 1000 байт, тогда для копирования потребуется 3 системных вызова read() и 3 write()
При изменении размера буфера со 100 байт до 1000 байт сокращается число системных вызовов read и write с 50 до 6. В следующей ниже таблице показано время выполнения программы ср1 при копировании файла размером в 5 Мбайт при различных значениях BUFFERSIZE.
Размер буфера 1 4 16 64 128 256 512 1024,
Время выполнения в секундах 50.29 12.81 3.28 0.96 0.56 0.37 0.27 0.22
78
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Размер буфера
Время выполнения в секундах
2048 4096 8192 16384
0.19 0.18 0.18 0.18
Системные вызовы требуют время для своего выполнения. Программа, где делается боль ше системных вызовов, работает медленнее и отнимает время у других пользователей, которые хотели бы работать в системе.
2.7.2 Почему на системные вызовы требуется тратить время? Чем определяются временные затраты при работе системных вызовов? На рисунке 2.5 схематично показан поток управления.
Рисунок 2.5 Поток управления при работе системных вызовов
На рисунке изображена память. Процесс развивается в пользовательской памяти, а ядро, располагается в системном пространстве. Диск доступен для ядра. Наша программа ср1 хочет читать данные, поэтому она обращается с системным вызовом read к ядру для чтения данных. Код, который производит фактическую передачу данных процессу с диска, явля ется частью ядра. Поэтому управление от вашего кода в пользовательском пространстве будет передано коду ядра, находящемуся в системном пространстве. После этого процес сор будет выполнять ту часть кода ядра, который организует передачу данных. На выпол нение кода по передаче данных требуется время. Но время требуется не только на передачу данных. Время требуется также и на переход в ядро и выход из ядра. Когда исполняется код ядра, процессор работает в супервизорном режиме со специальным стеком и памятью. При исполнении пользовательского кода про цессор работает в пользовательском режиме.
2.7. Увеличение эффективности файловых операций ввсщ/вывода: Буферирование
79
Функции ядра должны иметь доступ к диску, терминалам, принтерам и другим ресурсам. А вот пользовательские функции не должны иметь доступа к этим ресурсам. Поэтому и организуется работа компьютера в различных режимах. Когда компьютер работает в пользовательском режиме, он имеет ограничение на доступ к памяти - возможен доступ только к определенному сегменту памяти в пользовательском пространстве. Когда проис ходит работа в режиме ядра, компьютер имеет доступ ко всей памяти. Особенности смены режимов работы зависят от вашего процессора. У каждого процессора имеются собствен ные схемы по поддержке супервизорного и пользовательского режимов. Каждая версия Unix адаптируется к той модели поддержания супервизорного и пользовательских режи мов, которые обеспечиваются данным процессором. Рассмотрим Кларка Кента и Супермена. Когда происходит переход от пользовательского режима (Кларк Кент) в режим ядра (Супермен), то Кларк находит телефонную будку, там переодевается, снимает очки и меняет прическу. Далее Супермен выполняет определен ные действия, на что требуется время. И на переход обратно в пользовательский режим также требуется время. Чем чаще Кларк Кент выполняет такие смены образов (режимов), тем больше времени у него на это уходит и меньше времени остается на работу ре портером или на борьбу с преступностью. Ваша программа не является исключением. Чем больше времени процессор затрачивает на исполнение кода ядра и на вход в режим ядра и на выход из него, тем меньше времени у него остается для работы над вашим кодом или на обеспечение неких системных серви сов. Поскольку за время следует платить, то системные вызовы называют дорогими. Что приводит к удорожанию при чтении и записи данных в нашей версии программы who?
2.7.3. Означает ли, что наша программа who2.c неэффективна? Да! Выполнение для каждой записи utmp одного системного вызова выглядит также неэф фективно, как если бы мы покупали пиццу слоями или покупали бы яйца поштучно. Если вы собрались приготовить на завтрак яичницу из трех яиц, то вы должны будете поехать в магазин, купить одно яйцо, возвратиться обратно, поджарить яйцо, затем съесть его. После того как вы разделались с первым яйцом, должны будете поехать в магазин, купить другое яйцо, приехать обратно, поджарить яйцо и съесть его. Наконец, вы должны будете поехать и купить третье яйцо и понять, почему же яйца упаковывают в эти удобные коробки. Хорошая идея состоит в том, чтобы читать сразу несколько (связку) записей. Тогда (как в случае приобретения яиц в упаковке) связка записей помещается в локальную память. Упаковка с яйцами является по смыслу буфером. Далее показан псевдокод для метода getegg, где будет использована буферизация при покупке яиц. getegg(){ if (eggsJeft_in_carton == 0){ вновь упаковать коробку с яйцами в магазине if (eggs_at_store == 0) return EndOfEggs eggs left in carton = 12
} eggs.left_in_carton--; return one egg; i
80
Пользователи,файлыисправочник.Чторассматривал*впервуюочередь?
При каждом обращении к getegg выбирается одно яйцо, но не из магазина. Когда упаковка с яйцами опустеет, то по алгоритму функции следует ехать в магазин. Но какое отношение все это имеет к программированию в Unix? Ознакомьтесь с содержанием заголовочного файла /usr/include/stdio.h для getc. В некоторых версиях Unix функция getc, реализованная как макрос, использует ту же логику, что и функция getegg.
2.7.4. Добавление буферирования к программе who2.c Мы создадим версию программы who2.c, которая будет работать более эффективно за счет введения буферирования, что должно уменьшить число используемых системных вызо вов. Идея, которая была представлена на примере функции getegg, может быть представле на в программном виде. На рисунке 2.6 показано, как будет работать программа с буфери зацией.
Рисунок 2.6 Поток управления при работе системных вызовов Мы создаем массив, который может содержать 16 utmp структур. Этот массив именуется как буфер в нижней части схематического изображения процесса. Массив содержит по следовательность структур в пространстве процесса, что аналогично случаю с коробкой для яиц, в которой находились яйца у вас дома. Напишем функцию с именем utmp_next, которая будет извлекать записи из буфера. Модифицируем функцию main так, чтобы получать структуры из нашего буфера в пользо вательском пространстве. Это будет сводиться к вызову нашей собственной функции utmp_next в пользовательском пространстве. После того как будут обработаны все струк туры из буфера, функция utmp jiext обратится к системному вызову read, чтобы потребовать от ядра считать очередные 16 записей. Эта новая модель уменьшает число системных вы зовов read в 16 раз.
2.7. Увеличение эффективности файловых операций ввода/вывода; Буферирование
81
Такой буфер для размещения в нем 16 структур и функции для загрузки в буфер данных с диска и для извлечения из него структур для функции main помещены в файл utmplib.c.
Код utmplib.c Файл utmplib.c, содержимое которого здесь приведено, реализует алгоритм буферирования записей: /* utmplib.c - функции для чтения в буфер из файла utmp
★ * функции: * utmp_open (filename) - открытие файла * возвращает -1 при ошибке * utmp_next() - возвращает указатель на следующую структуру * возвращает NULL при достижении конца файла eof * utmp_close() - закрытие файла * при одной операции чтения происходит чтение NRECS записей и затем они * извлекаются из буфера
*/ «include <stdio.h> «include «include <sys/types.h> «include «define NRECS 16 «define NULLUT ((struct utmp *)NULL) «define UTSIZE (sizeof(struct utmp)) static char utmpbuf[NRECS * UTSIZE]; static int num_recs; static int cur_rec; static int fd_utmp = -1; utmp open(char ‘filename)
/* место хранения */ /* количество хранимых элементов 7
Г переход 7 /* чтение из */
{ fd_utmp = open(filename, 0_RD0NLY); curjec = numjecs = 0; return fd_utmp;
Г открытие */ Г пока нет записей Г сообщение */
} struct utmp *utmp next()
{ struct utmp *recp; if (fd_utmp ==-1) return NULLUT; if (cur rec==num_recs && utmp_reload()==0) /* еще? */ returnNULLUT; /*получить адрес следующей записи */ recp = (struct utmp *) &utmpbuf[cur_rec * UTSIZE]; cur_rec++; return recp;
} int utmn relnadO
Г ошибка? */
*/
82
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Г * Читать следующую последовательность записей в буфер 7
{ int amt_read; Г чтение записей в буфер */ amtjead = read(fd_utmp, utmpbuf, NRECS * UTSIZE); /* сколько было получено? */ num_recs = amtjead/UTSIZE; Г сброс указателя */ cur_rec = 0; . return num recs;
} utmp_close()
{ if (fd_utmp != -1) /* не закрывать, если не было */ close(fd utmp); /* открыто */
} utmplib.c содержит буфер, переменные и функции для управления потоком данных, который проходит через буфер. Значения переменных numrecs и cur rec определяют, сколько структур находится в буфере и сколько из них было использовано. Каждый раз при выборке записи функция utmp_next определяет с помощью проверки пере менной curjrec - не достиг ли этот счетчик значения, равного числу записей в буфере. Если не осталось неиспользованных записей, то функция utmp_next производит перезагрузку бу фера с диска. Прежде чем передать запись на использование, функция инкрементирует счетчик curjec. utmplib.c поддерживает ясный интерфейс в отношении вызываемых функций, скрывая внутренние детали расположения в памяти и формат utmp записей. Функция utmpjiext про сто возвращает указатели на структуры. Далее представлена модифицированная версия функции main: Г who3.c * * *
- who с буферируемым чтением - подавление пустых записей - форматирование времени - буферирование ввода (используя utmpiib)
*/ «include <stdio.h> «include <sys/types.h> «include «include «include «define SHOWHOST void show_info(struct utmp *); void showtime(time_t); int main()
{ stru utmD*utbufD.
t* казатель на следующую запись */
2.8.Буферизацияиядро
В данной версии вместо системных вызовов open, read, close будут вызываться эквивалент ные функции в модуле буферизации. Функции для отображения находятся в showjnfo.
2.8. Буферизация и ядро Буферизация - чрезвычайно полезная идея: читать данные в большие области памяти, ко торые находятся в вашем пространстве. Затем процесс выбирает из этих областей более мелкие части - те, которые ему необходимы.
2.8.1. Если буферизация столь хороша, то почему ее не использует ядро? Использует. При переходе в режим ядра и при возврате из него затрачивается время, но на передачу данных между твердым диском требуется несравненно больше времени. Для экономии времени ядро хранит копии блоков с диска в памяти. На рисунке 2.7 это проил люстрировано.
84
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Диск представляет собой объединение блоков данных аналогично тому, как файл utmp представляет собой объединение записей о входах в систему. Буферы ядра содержат копии некоторых блоков диска, точно так же, как наши utmp буферы содержат копии utmp записей. Ядро копирует блоки с диска в буферы ядра. Когда процессу становятся нужны данные из определенного файла, то ядро копирует данные из буфера ядра в буфер процесса. Ядро не копирует непосредственно с диска в пользовательское пространство памяти. Что происходит, если обнаруживается, что требуемой части данных нет в буфере ядра? Ядро приостанавливает процесс, который к нему обратился, и выставляет заявку на тре буемый блок в свой “список покупок”. Ядро затем находит другие процессы, которые готовы к выполнению некой работы и позволяет этим процессам развиваться. Черёз какое-то время ядро переместит требуемые данные с диска в буфер ядра. Теперь ядро может копировать данные в буфер пользовательского пространства, после чего раз будит спящий процесс. Понимание принципов буферирования в ядре изменяет наше восприятие системных вызо вов read и write. Системный вызов read копирует данные для процесса из буфера ядра, а сис темный вызов write копирует данные из процесса в буфер ядра. Передача данных между буферами ядра и диском происходит не так, как при работе системных вызовов read и write. Ядро может копировать данные на диск всякий раз, когда возникает в этом потребность. Возникает ситуация, аналогичная тому, как вы складываете почтовые конверты на столе в прихожей, чтобы потом их сразу переслать по почте. В нашем случае данные, которые обозначены процессом для записи, накапливаются в буферах ядра, ожидая момента, когда ядро скопирует их на диск. Если в системе внезапно произойдет отказ, который не позво ляет ядру скопировать блоки из буфера на диск, то изменения в файле или добавления дан ных в него произведены не будут. Последствия буферизации в ядре: •
Более быстрый “дисковый ” ввод/вывод. Оптимизированные записи на диск. Необходимость записи буферов на диск перед остановом системы.
2.9. Чтение файла и запись в файл Наша первая программа who читала из файла. Наша вторая программа ср читала из одного файла и писала в другой файл. А есть ли программы, которые читают из файла и пишут в тот же файл?
2.9.1. Выход из системы: Что происходит? Что происходит, когда вы выходите из системы? Прежде всего, в системе происходит из менение записи в файле utmp. По полученному результату работы whol можно было заме тить, что в файле utmp могут содержаться записи для неиспользуемых терминальных ли ний. Поэкспериментируйте и попытайтесь выполнить следующее: 1. Войдите в систему дважды, используя для этого два окна telnet к одной машине. 2. Используйте программу whol, которую мы написали, чтобы посмотреть содержимое файла utmp. Посмотрите, какие терминальные линии используются. 3. Выполните однократный выход из ваших сессий. 4. Запустите повторно на исполнение whol, чтобы посмотреть, что произошло с нашими двумя utmp записями.
2.9. Чтение файла и запись в файл
85
Вы увидите, что одна из записей, которая содержит ваше входное имя, изменилась. Обра тите внимание, что изменилось значение поля irttime. Какое будет новое значение времени в этом поле? На некоторых системах поле с входным именем очищается. Будут ли еще какие-то изменения в записи? Что произойдет с именем удаленной машины?
2.9.2. Выход из системы: Как это происходит Давайте обратимся к простому примеру. Программа, которая удаляет ваше имя из журна ла, должна выполнять следующие действия: 1. Открыть файл utmp. 2. Читать файл utmp до обнаружения записи о вашем терминале.. 3. Сместить модифицированную запись utmp на ее место. 4. Закрыть файл utmp. Рассмотрим эти четыре шага, один за другим.
Шаг 1: Открытие файла utmp Программа выхода читает из файла utmp (она способна найти запись о вашем терминале), а также производит запись в файл utmp (чтобы заменить запись). Поэтому программа вы хода должна открыть файл utmp на чтение и запись: fd = open(UTMP_FILE, O.RDWR);
Шаг 2: Поиск записи о вашем терминале Все происходит просто. В цикле while будут по одной читаться utmp записи (или будет использовано буферирование), будет производиться сравнение значения utjine с именем вашего терминала. Это может происходить так: while(read(fd, rec, utmplen) == utmplen) if (strcmp(rec.utjine, myiine) == 0) revise_entry();
/* получить следующую запись7 /* это моя линия? */ /* удалить мое имя 7
ШагЗ: Запись модифицированной записи на место Программа выхода модифицирует запись и помещает эту запись обратно в файл. Про грамма изменяет значение USER_PROCESS, которое находится в utjype, на значение DEAD_PROCESS. В некоторых версиях программ выхода может производиться очистка поля с входным именем пользователя и поля с именем хост-машины, а значение, которое было в поле utjime заменяется на время выхода. Описанные действия легко запрограммировать. Теперь возникает такой большой вопрос: как же мы запишем модифицированную запись обратно в файл? Если просто вызвать системный вызов write, то произойдет модификация следующей записи. Это произойдет потому, что ядро поддерживает понятие текущей по зиции в файле и смещает текущую позицию после каждого прочтения некого числа байтов или при записи в файл. При организации поиска utmp записи о нашем терминале текущая позиция была выставлена на следующую запись. Тогда возникает важный вопрос. Вопрос: Как программа может изменить текущий указатель чтения-записи в файле? Ответ: С помощью системного вызова lseek. Мы рассмотрим lseek в следующем разделе.
Шаг 4: Закрытие файла Следует вызвать close(fd).
86
Пользователи; файлы и справочник. Что рассматривать в первую очередь?
2.9.3. Смещение текущего указателя: lseek Unix управляет текущим указателем в каждом открытом файле, как это показано на ри сунке 2.8. Каждый раз, когда вы читаете байты из файла, ядро будет читать данные с текущей пози ции и затем смещать текущий указатель на то число байтов, которое было прочитано. Ука затель используется и при записи данных в файл. Каждый раз, когда вы производите запись байтов в файл, ядро помещает их в файл, начиная с текущей позиции, а затем корректирует значение текущей позиции - увеличивает ее на число записанных байтов.
Текущий указатель позиции привязан к соединению с файлом, а не к самому файлу. На пример, если две программы открыли один и тот же файл, то после открытия для каждого соединения будет поддерживаться собственный указатель позиции. Программы могут чи тать или записывать в разных местах файла. Системный вызов lseek дает вам возможность изменять текущую позицию в открытом файле и имеет такие характеристики:
2.9. Чтение файла и запись в файл
87
lseek устанавливает текущий указатель через дескриптор открытого файла fd в то место в файле, которое задается парой значений - dist и base. Значением base (база) можно зада вать начало файла (0), текущую позицию в файле (1) или конец файла (2). Смещение - это число байтов относительно базы. Напдимер, при таком обращении к системному вызову: lseek(fd, -(sizeof(structutmp)), SEEK_CUR); произойдет смещение текущего указателя на sizeof(struct utmp) байтов относительно теку щей позиции. При обращении вида: lseek(fd, 10 * sizeof(struct utmp), SEEKSET); текущий указатель будет установлен на начало одиннадцатой utmp записи в файле. А при обращении вида: lseek(fd, 0, SEEKEND); write(fd, "hello", strlen(MhelloH)); текущий указатель будет установлен в конец файла и там будет записана текстовая строка. Наконец, нотация вида: lseek(fd, 0, SEEKCUR) означает возврат в текущую позицию.
2.9.4. Кодирование выхода из системы через терминал Теперь у нас есть все, что необходимо для написания функции, которая будет делать от метку в файле utmp при выходе из системы:
г ’ logout_tty{char *line) . * производит отметки в utmp - записи при выходе из системы * не затирает имени пользователя и удаленной машины * возвращает -1 - при ошибке, 0 - при успехе 7 int logout tty(char *line)
{ int fd; struct utmp rec; int len = sizeof(struct utmp); int retval = -1; /* пессимизм */ if ((fd = open(UTMP_FILE,0_RDWR)) == • 1) /* открытие файла 7 return -1; Г поиск и замена */ while (read(fd, &rec, len) == len) if (stmcmp(rec.ut_line, line, sizeof(rec.ut line)) == 0)
{ rec.ut_type = DEAD_PROCESS; /* установка типа 7 if (time(&rec.ut_time) != -1) /* и времени 7 if (lseek(fd, -len, SEEK_CUR)!= -1) /* откат 7 if (write(fd, &rec, len) == len) /* модификация7 retval = 0; /* успех! 7
88
Пользователи, файлы и справочник. Что рассматривав в первую очередь? break;
} Г закрытие файла */ if (close(fd) ==-1) retval = -1; return retval;
} В этом программном коде производится проверка возникновения ошибок для каждого системного вызова. В ваших системных программах следует всегда проверять наличие ошибок при каждом системном вызове. Такие программы в состоянии модифицировать файлы и данные, от которых зависит работа системы. Могут возникнуть серьезные по следствия, если оставлять файлы в некотором противоречивом состоянии или оставлять их, не закончив с ними работу. С другой стороны, в ряде простых программ в данном тек сте были опущены проверки на наличие ошибок. Это сделано было для того, чтобы оста вить наглядной и ясной логику работу самих системных вызовов. Упомянув об ошибках, теперь посмотрим, как управлять ими и как оповещать об их на личии.
2.10. Что делать с ошибками системных вызоврв? Если системный вызов open не может открыть файл, то он возвращает -1. Если системный вызов read не может прочитать данные, то он возвращает -1. Если системный вызов lseek не может отыскать нужную позицию, то он возвращает-1. Системные вызовы возвращают-1, когда что-то выполняется неправильно. Ваши программы должны проверять код возврата каждого системного вызова, которые делаются в вашей программе, и предусматривать вы полнение необходимых действий в случае возникновения ошибок. Что следует считать неправильным? Для каждого системного вызова установлен собст венный перечень ошибок. Рассмотрим системный вызов open. Файл может не существо вать, вы можете не иметь прав на открытие файла или у^ке открыли слишком много фай лов. Как же ваша программа сообщит вам, какая и^з нескольких возможных ошибок воз никла?
Как идентифицировать ошибочную ситуацию: errno Ядро оповещает вашу программу при возникновении ошибки с помощью определенного кода ошибки, который ядро записывает в глобальную переменную errno. Каждая програм ма имеет доступ к этой переменной. На странице документации еггпо(З) и в заголовочном файле <errno.h> находятся символь ное и числовое представление кодов ошибок. Вот несколько примеров: «define «define «define «define «define
EPERM ENOENT ESRCH EINTR EIO
1 2 3 4 5
/* Отсутствие прав на действие */ Г Нет такого файла или каталога */ Г Нет такого процесса */ Г Прерываемый системный вызов */ /* Ошибка ввода/вывода */
Различные ответы на различные ошибки Вы можете использовать эти символические коды ошибок в вашей программе, когда буде те программировать распознавание ошибочных ситуаций и действий, которые нужно предпринимать при ошибках. Это иллюстрируется в следующем программном коде:
2.10. Что делать с ошибками системных вызовов?
89
«include <ermo.h> extern int errno; ' int sample()
{ intfd;
fd = openf'file", О RDONLY); if (fd == -1)
{ printf("Cannot open file:"); if (errno ==ENOENT) printf("There is no such file.”); else if (errno == EINTR) printf("Interrupted while opening file."); else if (errno == EACCESS) printf(”You do not have permission to open file."); Действия вашей программы будут зависеть от того, что вы будете считать ошибочным действием. Например, если системный вызов open заканчивается неуспешно, поскольку указанный файл не существует, вы можете запросить у пользователя другое имя файла. С другой стороны, если программа открыла слишком много файлов (EMFILE), то можно закрыть некоторые из них и вновь попытаться открыть нужный файл. В данном случае пользователю нет необходимости знать о возникновении такой ошибки и выполненных действиях при ее возникновении. Сообщения об ошибках: реггог(3) Если вы хотите выдать сообщение, которое описывает ошибку, то требуется проверить значение переменной errno и вывести то или иное сообщение в зависимости от значения переменной. В функции sample, которая была приведена выше, это. выполняется. Вместе с тем вместо указанных действий представляется удобным использовать библиотечную функцию perror(string). Функция perror(string) выбирает код ошибки и выводит в соответст вии с возникшей стандартной ошибкой строку, которую вы ей передаете, вместе с кратким сообщением об ошибке. В модифицированной версии программы sample используется реггог: int sample()
{ int fd; fd = openC'file", 0 RDONLY); if (fd ==-1)
{ perrorf’Cannot open file"); return;
} Если возникает ошибка при работе системного вызова open, то вы увидите такого рода сообщения: Cannot open file: No such file or directory Cannot ODen file: Interrupted svstem call
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
90
В первой части диагностического вывода находится строка, которую вы передаете при обращении к функции реггог, а во второй части вывода находится текст, который соответ ствует коду ошибки в переменной errno.
Заключение Основные идеи • Команда who выводит список текущих пользователей после чтения его из системного журнала. • В системах Unix данные хранятся в файлах. Unix - программы организуют передачу дан ных в файлы и из файлов с помощью шести системных вызовов: open(filename, how) creat(filename, mode) read(fd, buffer, amt) write(fd, buffer, amt) lseek(fd, distance, base) close(fd)
Процесс читает данные и записывает их с помощью файловых дескрипторов. Файловый дескриптор определяет соединение между процессом и файлом. Каждый раз, когда программа выполняет системный вызов, компьютер переключается из пользовательского режима в режим ядра. Далее исполняется некоторый код в ядре. Программы будут работать более эффективно, если в них будет произведена минимизация числа системных вызовов. Программы, которые читают и пишут данные, могут сократить число системных вызовов, размещая данные в буферах и обращаясь к ядру, когда необходимо записать заполненный буфер или поместить данные в буфер при его опустошении. Ядро Unix использует буферы, которые располагаются в памяти ядра для того, чтобы сократить время на пересылку данных между системой и диском. В Unix значение времени хранится в форме целого числа секунд, прошедших с момента начала работы Unix. Когда в Unix системный вызов обнаруживает ошибку, система устанавливает определенное значение в глобальной переменной errno и возвращает код возврата, равный -1. Системные программы могут использовать значение errno для диагностирования ошибок и выполнения необходимых действий при возникновении ошибок. Большая часть информации, которая была представлена в этом разделе, доступна в системе. Расширенные тексты документации представляют описание команд, что они делают, а в ряде случаев описывают, и как они работают. В заголовочных файлах содержатся определения структур данных, значения символических констант, прототипы функций, используемые для создания системных средств.
Заключение
91
Исследования 2.1 Команда w. В Unix есть команда, которая называется w и которая имеет отношение к команде who. Попытайтесь выполнить команду и прочитайте документацию для нее. Какие действия поддерживаются в команде w и не поддерживаются в команде who? Какая при этом используется информация в файле utmp? Каково назначение дополни тельной информации? Попытайтесь найти источники, объясняющие смысл дополни тельной информации. 2.2 Авариии utmp. Когда вы входите в систему, то ваше входное имя, имя терминала, время, имя вашего удаленного хоста записываются в файл utmp. Когда вы выходите из систе мы, то запись зачищается. Что происходит, если в системе произойдет некая авария? Очевидно, что в файле utmp останется список пользователей, которые были в системе во время аварии. Когда система вновь стартует, то информация в файле utmp не будет верной. Что в системе Unix делается с файлом utmp, когда система стартует? Создают ся ли записи для всех доступных терминальных линий? Может быть, создается пус той файл, в котором будут накапливаться записи о терминальных линиях? Для ответа на эти вопросы обратитесь к справочнику, заголовочным файлам и стартовым скрип там. Вы можете поэкспериментировать на собственных машинах. 2.3 Проверьте, как работает программа ср1, копируя некий файл на /dev/tty: Ср1 ср 1 .с /dev/tty. Здесь целевым файлом является терминал. Наша программа будет от крывать терминал, производить запись и закрывать терминал, используя для этого те же системные вызовы, которые она использовала при посылке данных в файл на дис ке. Далее скопируйте данные с терминала в дисковый файл, используя такую нота цию: ср1 /dev/tty filyl. Теперь ваша клавиатура становится входным файлом. Следут от метить, что после набора строк вы должны нажимать на клавишу Enter, а в конце сле дует набрать Ctrl-D. 2.4 Стандартные С - функции для работы с файлами fopen, getc, fclose, fgets представляют собой часть системы буферированного ввода и вывода файлов. Эти функции исполь зуют структуру типа FILE, которая является промежуточным уровнем и подобна по на значению модулю utmpiib. Найдите определение FILE в заголовочных файлах, описание структур и сравните их с переменными в utmplib.c. 2.5 Запись в буферы ядра. Как убедиться в том, что данные, которые вы пишете на диск, действительно туда были записаны? Мы отмечали, что ядро будет копировать дан ные, когда оно в них нуждается. Изучите справочный материал, где говорится, как системные вызовы и программы отслеживают состояние буферов при копировании на диск. 2.6
Многократное открытие одного и того же файла. В Unix допускается открытие одного файла несколькими процессами. В Unix возможно, что один и тот же процесс может многократно открывать один и тот же файл. Поэкспериментируйте с много кратным открытием одного и того же файла, предварительно создав файл с неко торым произвольным текстом. Затем напишите программу, которая выполняет сле дующие действия: (a) Открывает файл для чтения. (b) Еще раз открывает этот же файл на запись. (c) Еще раз открывает этот же файл на чтение.
92
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Вы должны получить три файловых дескриптора. После этого программа должна: (d) Прочитать 20 байтов, используя для этого первый дескриптор fd, и вывести на экран то, что прочитали. (e) Записать строку -"testing 1 2 3 ”, используя для этого второй дескриптор fd. (0 Прочитать 20 байтов, используя для этого третий дескриптор fd и вывести на экран то, что прочитали. 2.7 Изучение электронного справочника. Команда man предоставляет вам информацию о
командах Unix, системных вызовах, системных устройствах и информацию по дру гим темам. Какую команду следует использовать, чтобы изучить свойства самой ко манды man? Сколько разделов содержится в электронном справочнике в вашей версии Unix? Для чего они предназначены? 2.8 Изучение файла utmp. Файл utmp, о котором шла речь в предшествующих эксперимен
тах, содержит записи, которые соотнесены текущим сессиям. Какие еще виды запи сей содержатся в этом файле? Для чего они предназначены? 2.9 Переход в конец файла. Системный вызов lseek позволяет вам выставить текущий ука
затель в файле на позицию, которая будет располагаться за концом файла. Например, системный вызов: lseek(fd, 100.SEEKEND)
установит текущий указатель в позицию, которая смещена на 100 байтов от конца файла. Что произойдет, если после этого вы попытаетесь читать данные сразу за концом фай ла? Что произойдет, если после этого вы попытаетесь писать данные сразу за концом файла? Попытайтесь записать некоторую строку, типа “hello”, со смещением на боль шую величину относительно конца файла (например, 20 000 байтов). Проверьте, каков будет размер файла с помощью команды Is -I и с помощью команды Is -s. Что в результате получили?
Программные упражнения 2.10 Идентификация личности. В документации для команды who упоминается о возмож ности использования для этой команды такой нотации: who am i. Кроме того, допустим и вариант: whoami. Модифицируйте программу who2.c так, чтобы она поддерживала вариант вызова команды who am i. Поэкспериментируйте с командой whoami и почитай те документацию по ней. Чем этот вариант отличается от варианта who am i? Напишите программу, которая работала так же, как и whoami. 2.11 Что сделает стандартная программа ср, если вы попытаетесь с ее помощью копиро
вать из файла в этот же файл? Например: cpfilel filel. Насколько это корректно? Моди фицируйте программу ср1 .с, которая управляла бы данной ситуацией. 2.12 Файлы и API. Мы создали utmplib.c, чтобы увеличить эффективность, но при этом дос
тигается еще и дополнительный эффект. Этот дополнительный эффект заключается в том, что была произведена замена файла данных на набор функций, который пред ставляет собой программный интерфейс (API). С помощью этого API программа по
Заключение
93
лучает все структуры utmp, даже те, которые не представляют входы пользователей. Программе who необходимо только получить для рассмотрения те записи, которые представляют активные сессии. Модифицируйте utmplib.c так, чтобы она возвращала только записи об активных сессиях. Как такие изменения подействуют на остаток кода who3.c? Хороша ли эта идея с изменениями? Почему да или почему нет? 2.13 Буферирование и поиск. Функция logoutjty, которая была приведена ранее, использует системный вызов lseek. Она производит откат на одну запись и может быть применена для перезаписи. Заметим, что функция logout_tty не использует буферирования для чте ния файла utmp. Программа могла более эффективно работать, если бы использова
лось буферирование. (a) Рассмотрите те проблемы, которые могут возникнуть, если мы соединим вместе системный вызов и обращения к функциям в utmplib.c. (b) Добавьте новую функцию в utmplib.c, которая должна будет вызываться так: utmp_seek(record_offset,base) и которая изменяет текущий указатель для utmp_next таким же образом, как это делает
lseek
при изменении текущего указателя при вызове read. Эта новая функция должна пере двигать текущий указатель на record offset записей относительно базы base, где значе ниями base могут быть seek_set, seek_cur или seek_end. Заметьте, что значение аргу мента представляется в количестве записей, а не в количестве байтов. (c) Модифицируйте logoutjty так, чтобы в этой версии можно было использовать utm plib.c. 2.14 Эксперименты с utmp. Программа whol пролистывает каждую запись в файле utmp.
Хотя это и не входило в наши намерения, но программа тем самым предоставляет удобное средство для проверки содержимого файла utmp. Сделайте программу whol еще более полезной, добавив некий код к ней, который будет выводить все другие по ля в структуре. В частности, полезно поле iitjype. Модифицируйте программу так, чтобы она позволяла пользователю заменить файл utmp (файл по умолчанию)на файл, имя которого задается в командной строке. Теперь можно будет использовать это средство для проверки файла wtmp. 2.15 Предотвращение разрушений файлов. В стандартной версии команды ср безусловно
происходит перезаписывание существующих файлов. То есть, если у вас есть файл file2, и вы будете выполнять команду: $ ср filel file2
то вы уничтожите оригинальное содержание файла file2. В стандартной версии ср под держивается опция -i, которая заставляет команду запрашивать у пользователя под тверждение на перезапись файла. Добавьте это свойство в программу cpl.c.
Проекты Взяв за основу материал этой главы, вы можете изучить и написать собственные версии следующих команд Unix: ас, last, cat, head, tail, od, dd
94
Пользователи, файлы и справочник. Что рассматривать в первую очередь?
Последняя трудная задача: Команда tail Мы многое узнали при ознакомлении с рядом команд. Рассмотрите еще одну команду. У нас нет возможности ее изучать, поэтому вы должны будете сделать это самостоятельно. Системный вызов lseek позволяет вам передвигать текущий указатель по файлу. Вызов Iseek(fdASEEKEND)
переместит текущий указатель в конец файла. Команда tail позволяет отобразить последние десять строк файла. Попытайтесь ее выпол нить. Команда tail будет выводить не от конца файла и вперед, а десять строк, которые рас полагаются перед концом файла. Заметим, что аргумент distance в системном вызове lseek измеряется в количестве символов. Как работает команда tail? Разработайте собственную версию. Подумайте о буферирова нии, чтобы ваша программа работала эффективнее. Изучите документацию и познакомь тесь со всеми опциями, которые поддерживаются в команде tail. Как они работают? Разра ботка такой программы в отношении программ who и ср выглядит гораздо более простым проектом. Исходный код двух версий tail доступен на Web-сайте книги. Одна версия - это gnu версия, другая - версия bsd. В них используются различные средства. Прежде чем обратиться к этим решениям, попытайтесь написать одну из версий самостоятельно.
Глава 3 Свойства каталогов и файлов при просмотре с помощью команды-Is
Цели Идеи и средства • • • • • •
Каталог - это список файлов. Как прочитать каталог. Типы файлов и как определять тип файла. Свойства файлов и как определять свойства файл. Битовые наборы и биты маскирования. Идентификаторы пользователя, идентификаторы группы и база данных passwd.
Системные вызовы и функции • •
opendir, readdir, closedir, seekdir stat chmod, chown, utime
•
rename
Команды •
Is
3.1. Введение Мы знаем, как проч1?гать содержимое файла и как записать данные в файл. Помимо со держания, файл имеет еще ряд атрибутов. Файл имеет собственника, у файла есть время его последней модификации, размер, тип и другие атрибуты. Как мы можем посмотреть имена файлов и определять свойства файлов? С помощью команды Is можно получать списки имен файлов в каталоге и информацию о файлах. Мы изучим команду Is, чтобы больше узнать о каталогах и типах файлов, узнать о свойствах файлов.
96
Свойства каталогов и файлов при просмотре с помощью команды Is
3.2. Вопрос 1: Что делает команда is? 3.2.1. Команда Is выводит список имен файлов и оповещает об атрибутах файлов Наберите команду Is, чтобы посмотреть, что она делает. JHs Makefile docs Is2.c s.tar statdemo.c tail! .c chap03 Is1 .c old_src statl x tail!
$ Действие команды Is по умолчанию - вывод списка имен файлов в текущем каталоге. При выводе имена файлов сортируются командой Is в алфавитном порядке. В одних версиях ко манда располагает список имен поколонно, в других версиях такой вывод выполняется с по мощью опции (используя опцию -С). Помимо имен файлов, команда Is может выводить еще дополнительную информацию о файлах. Если при обращении к команде задается опция Ч то команда представляет информацию о каждом файле, используя длинный формат: $ls-l total 108 -rw-rw-r--rw-rw-r-drwxrwxr-x -rw-r--r--fW-r-rdrwxrwxr-x -rw-rw-r--rw-r--r--rw-r--r--rwxrwxr-x -rw-r~r--
2 bruce 1 bruce 2 bruce 1 bruce 1 bruce 2 bruce 1 bruce 1 bruce 1 bruce 1 bruce 1 bruce
users users users users users users users support support users users
345 Jul 29 11:05 Makefile 27521 Aug 1 12:14 chap03 1024 Aug 1 12:15 docs 723 Feb 9 1998 Is1.с 3045 Feb 15 03:51 Is2.c 1024 Aug 1 12:14 old src 30720 Aug 1 12:05 s.tar 946 Feb 18 17:15 statl. с 191 Feb 9 1998 statdemo.c 37351 Aug 1 12:13 taih 1416 Aug 1 12:05 taill.с
$ Каждая строка вывода представляет один файл и содержит несколько атрибутов для каж дого файла.
3.2.2. Получение листинга о других каталогах, получение информации о других файлах В системе Unix находится много каталогов, в каждом из которых собраны собственные на боры файлов. Как быть, если вам понадобится информация о других каталогах и о файлах в этих каталогах? Можно обратиться к команде Is для получения информации о файлах из других каталогов, указывая имена каталогов и имена файлов в командной строке: Запрос на получение информации по Is из других каталогов и об их файлах Пример
Действие
ls/tmp
Список имен файлов в каталоге Дтр
Is-I docs
Показать атрибуты файлов в каталоге docs
Is -l./Makefite
Показать атрибуты. /Makefile
Is'.с
Список файлов, имена которых удовлетворяют шаблону '.с
97
33. Краткий обзор дерева файловой системы
Если в качестве аргумента задан каталог, то команда Is выводит в виде списка его содержи мое. Если в качестве аргумента задан файл, то команда Is выводит его имя и, возможно, атрибуты. На то, что может выполнить Is и как будет выглядеть вывод команды Is, указы вают опции, которые задаются при обращении к команде.
3.2.3. Наиболееупотребимые опции В документации команды Is приводится большой список опций этой команды. Наиболее популярные представлены в таблице. Команда
Действие
Is-а Is -III Is -s Is -t
Сортировка по времени
Is -F
Показать типы файлов
Показать скрытые файлы Показать время последнего чтения Показать размер в блоках
Ремарка относительно имен файлов с начальной точкой Опция -а требует пояснения, если вы новичок в Unix. Unix реализует концепцию скрытых файлов на основе использования простого соглашения. Соглашение заключается в том, что команда is не включает в список вывода имена файлов, если они начинаются с точки. Нечто в операционной системе (а именно ядро) знает и поддерживает концепцию скрытых файлов. Это соглашение, которому следуют команда Is и пользователи. Некоторые программы используют имена с начальной точкой для файлов в пользователь ском домашнем каталоге, чтобы указать неопределенные пользовательские предпочтения. Такие конфигурационные файлы легко редактировать. Но их имена в листинге о содержа нии каталога чаще всего не выводятся.
3.2.4. Первый ответ: Итоговые замечания В результате проведения экспериментов с командой Is и после изучения соответствующей документации мы обнаружили, что команда Is выполняет две функции: Выводит в виде списка содержимое каталогов. Отображает информацию о файлах. Отметим, что команда Is выполняет различную обработку каталогов и файлов. При обра щении команда Is определяет, что задано в качестве аргумента - файл или каталог. Как это делается? Если мы будем писать версию программы Is, то нам потребуется ответить на три вопроса: • Как вывести в форме списка содержимое каталога? • Как получить и отобразить свойства файла? • Как различить имя файла и имя каталога?
3.3. Краткий обзор дерева файловой системы Прежде чем отвечать на сформулированные вопросы, давайте рассмотрим картину рас пределения файлов на диске, которая поддерживается в Unix. Информация на диске представлена как дерево каталогов, каждый из которых содержит файлы и/или каталоги. На рисунке 3.1 небольшие прямоугольники обозначают файлы,
98
Свойства каталогов и файлов при просмотре с помощью команды Is
находящиеся в каталогах, а линиями обозначается, каким образом каталог соединяется с вышележащим и нижележащим каталогами.
Рисунок 3.1 Дерево каталогов В Unix каждый файл расположен в некотором месте единственного дерева каталогов. Отсутствуют такие понятия, как устройства или тома. Напротив, каталоги на отдельных физических дисках и разделы рассматриваются как составные части одного дерева. Даже гибкие диски, диски CD-ROM и другие заменяемые носители будут рассматриваться в какой-то момент как подкаталоги единого дерева. Все это значительно упрощает написание программы Is. Мы будем иметь в виду только ка талоги и файлы и не думать о разделах и томах.
3.4. Вопрос 2: Как работает команда Is? Команда Is выдает список имен файлов. Как его сформировать? Первый набросок дейст вий будет такой: открытие каталога +- > читать запись - конец каталога? •+ |_ отобразить информацию о файле | закрытие каталога <............ .........+
Эта схема напоминает логику команды who. Главное отличие заключается в том, что команда who производит открытие и читает из файла, а команда Is открывает и читает данные из ката лога. Насколько отлично чтение из каталога от чтения из файла? В конечном счете, что такое каталог?
3.4.1. Что же такое каталог, в конце концов? Ответ. Каталог представляет собой особый вид файла, в котором содержится список имен файлов и подкаталогов. Каталог по ряду признаков подобен файлу utmp (см. главу 2). В нем содержится последовательность записей, а каждая запись четко определена по на значению и имеет полностью документированную структуру. Каждая запись в каталоге служит для представления одной сущности - одного файла или одного каталога. В от личие от обычных файлов каталог никогда не бывает пустым. В каждом каталоге есть две специальные записи, которые обозначаются так: . {точка) и .. (точка_точка); точка -это имя текущего каталога, а точкаjno4m - это имя вышележащего каталога.
3.4. Вопрос 2: Как работает команда Is?
99
3.4.2. Работают ли системные вызовы open, read и dose в отношении каталогов? Ответ 1. В Olden Days ®#Что это такое? нет упоминаний о таких возможностях. Порабо тайте с такими командами на вашей системе: $ cat / as\a,asa..a,bw.tagsb’c{ quota.userc'{ quota.group,esbetce"sbtmp,,,sbdevM sbmnt ' wcsbin ’2 sbopt2
•8 sbusr8 •9 sbvar9 (many lines of hard-to-read data omitted)
$ more /tmp Amp is a directory
$ od -c /dev 0000000 0000020 0000040 0000060 0000100 0000120 0000140 0000160 0000200 0000220 0000240
360 001 002 М 362 362 к 364 364 к 366
001 200 \о А 001 001 с 001 001 m 001
\0 \о \о К \0 \0 0 \0 \0 е \о
024 \о 001 002 \о \0 001 200 \0 D V Е 030 \о 004 001 200 \о \о \о \0 030 \0 \а 001 200 \о \о \о \о \о 024 \о 003
\о \о \о Е \о \о п \о \о m
\о \о \о \о \о \о \о \о \о \о \о
\о \о 024 \о 002 361 001 \о 361 001 \о к I 0 363 001 \о 363 001 \о i к b 365 001 \о 365 001 \о m е m
\о 360 001 \0 \о \0 \о \о \о 030 \0 \а \о \о 001 200 \о \о 0 \0 \0 \о 9 030 \о \0 004 \о 001 200 \0 \о \о п I 0 9 \о \о 030 \0 004 \0 \о 001 200 \о \0 \о 366 001 \о \0
Из этих примеров следует ряд интересных результатов. Во-первых, команды cat и od могут читать каталоги так же, как они читают обыкновенные файлы. В этих командах исполь зуются стандартные системные вызовы для работы с файлами: open, read и close. Поэтому каталоги можно читать как обыкновенные файлы. Во-вторых, команда more отказывается показывать вам содержимое каталога. Она распо знает, что в качестве аргумента задан каталог, и не будет отображать его содержимое. Команда more смогла бы отображать содержимое каталога, но не думаю, что вам захоте лось бы его рассматривать. Некоторые версии команды cat, подобно команде more, распо знают каталог при обращении и не отображают его содержимое. Наконец, примеры показывают, что каталоги не содержат однородного текста. Каталог со стоит из последовательности структур. Ответ 2. Использование системных вызовов open, read и close для получения списка содержимого каталогов является плохой идеей. В Unix поддерживается много типов ката логов. Допускается читать диски, используя форматы Apple HFS, IS09660, VFAT. Можно читать каталоги NFS и различные обыкновенные каталоги. При использовании системно го вызова read потребуется знание формата записей для обработки каждого типа каталога.
Свойства каталогов и файлов при просмотре с помощью команды Is
100
3.4.3. Хорошо, хорошо. Но как же мне прочитать каталог? Обратимся к электронному справочнику. Поищем информацию по ключевому слову direct: $ man-k direct В одной из систем по этому слову была найдена 81 запись. Отфильтруем этот вывод по слову read: $ man -к direct | grep read DXmHelpSystemDisplay (ЗХ) - Displays a topic or directory of the help file in Bookreader. opendir, readdir, readdirj, telldir, seekdir, rewinddir, closedir (3) Performs operations on directories
$ Первый экран документации будет выглядеть так: $ man 3 readdir opendir(3) opendir(3) NAME opendir, readdir, readdirj, telldir, seekdir, rewinddir, closedir - Performs operations on directories LIBRARY Standard С Library (libc.a) SYNOPSIS #include <sys/types.h> «include DIR ’opendir ( const char *dir_name); struct dirent *readdir ( DIR *dir_pointer); int readdirj ( DIR *dir_pointer, struct dirent *entry, struct dirent **result); long telldir ( DIR ’dir_pointer); void seekdir ( DIR *dir_pointer, long location); void rewinddir ( DIR *dir_pointer); int closedir ( DIR *dir_pointer);
[more] (11%)
3.4. Вопрос 2: Как работает команда Is?
101
Согласно этой странице документации мы убеждаемся в том, что данные из каталога по лучают аналогично тому, как получают данные из файла. Сначала с помощью opendir открывается соединение с каталогом, а далее readdir возвращает указатель на следующий элемент в каталоге. Наконец, closedir разрывает соединение. Системные вызовы: seekdir, telldir и rewinddir по назначению подобны lseek. На рисунке 3.2 показано, как происходит чтение.
Рисунок 3.2 Чтение записей из каталога
Чтение содержимого каталога Каталог - это список файлов, а более точно, это последовательность записей, каждая из которых есть запись о каталоге. Мы читаем записи с помощью вызова readdir. После рабо ты каждого вызова readdir возвращается указатель на очередную запись типа struct direntn. Компоненты структуры описаны в соответствующей документации и в заголовочном фай ле /usr/include/dirent/h. Например, начало документации по dirent, которая была взята в сис теме Sun OS, будет таким: File Formats NAME dirent - file system independent directory entry SYNOPSIS tinclude DESCRIPTION Different file system types may have different directory entries. The dirent structure defines a file system independent directory entry, which contains information com-mon to directory entries in different file system types. A set of these structures is returned by the getdents(2) sys-tem call. The dirent structure is defined: struct dirent { ino_t d_ino; off_td_off; unsigned short djeclen; chard_name[1];
dirent(4)
102
Свойства каталогов и файлов при просмотре с помощью команды Is
Каждая структура dirent содержит элемент с именем d__name. Это элемент для хранения имени файла. Заметьте, что длина массива d_name в этой системе равна 1. Что означают та кие установки? Один символ char задает пространство для сохранения в поле одиночного терминального нулевого символа.
3.5. Вопрос 3: Могу ли я написать Is? Логика получения содержимого каталога будет такой: main() opendir while (readdir) print d_name closedir
Полный код программы Isl.c будет таким: Г Is1 .с ** цель - вывод списка содержимого каталога или каталогов ** при отсутствии аргументов используется., в противном случае ” используется список имен файлов через список аргументов
•ktcj
«include <stdio.h> «include <sys/types.h> «include void do_ls(char []); main(int ac, char *av[])
{ if (ac == 1) do_ls("."); else while (--ac){ printf("%s:\n", *++av); do ls(*av);
} } void do !s(char dirnamef])
/* list files in directory called dirname
*/ { DIR *dir_ptr; /* каталог */ struct dirent *direntp; /* каждая запись */ if ((dir_ptr = opendir( dirname)) == NULL) fprintf(stderr,”ls1: cannot open %s\n", dirname); else
{ while ((direntp = readdir(dir_ptr)) != NULL) printf("%s n", direntp->dname);
3.5. Вопрос 3: Могу ли я написать Is?
103
closedir(dir ptr);
} } Откомпилируем и запустим этот код, а затем сравним полученный результат с выводом коман' ды Is, которая работает на вашей системе: $ СС -О Is1 Is1 .с $ls1
.
s.tar taiM Makefile Isl.c Is2.c chap03 old_src docs Is1 statl.с statdemo.c tail 1.с
$ Is Makefile chap03
docs Is1
Isl.c Is2.c
old_src s.tar
statl .c statdemo.c
taill taill.с
$
3.5.1. Что еще нужно делать? Неплохо для первой попытки. Эта версия 1.0 Is выводит список файлов в каталоге, но в дан ной версии не поддерживаются следующие возможности: (a) Нет сортировки вывода. Наш список имен файлов не отсортирован в алфавитном порядке. Устранение. Мы можем считать все имена файлов в массив, а затем использовать команду qsort для сортировки этого массива. (b) Нет поколонного вывода. Стандартная версия команды Is поддерживает возможность вывода списка имен файлов поколонно. В некоторых версиях расположение имен поколонно происходит сверху вниз, слева направо, а в других системах - слева направо, сверху вниз. (c) Вывод файлов с именами с лидирующей точкой. В этой версии отображаются имена файлов с точкой. В стандартной версии команды Is имена файлов с лидирующей точкой отображаются, только если используется опция -а. Устранение: Подавить вывод имен с лидирующей точкой достаточно просто и обеспечить их вывод по опции -а. (d) Не работает опция -I.
104
Свойства каталогов и файлов при просмотре с помощью команды Is
В стандартной версии Is производится вывод статусной информации о файле, если поль зователь задает при обращении к команде опцию -1. В нашем варианте такой возможности нет. Устранение: Добавить отработку опции -1 непросто. В структуре dirent, которая опре делена в заголовочном файле , есть только несколько необходимых элементов. В структуре dirent отсутствует информация о размере файла, о собственнике, а также дан ные о других характеристиках файла. Если этой информации нет в каталоге, то где же она хранится?
3.6. Проект 2: Написание версии Is -I Мы уже заметили, что команда Is выполняет два вида действий: выводит список содержи мого каталогов, а также отображает статусную информацию о файлах. Далее мы увидели, что эти два аспекта не связаны между собой. В каталоге содержатся не только имена фай лов. Нахождение и отображение статусной информации о файлах - это отдельный слож ный проект. Мы будем его реализовывать, отвечая натри стандартных вопроса.
3.6.1. Вопрос 1: Что делает ts -I? Рассмотрим вывод команды:
$ Is -I total 108 -rw-rw-r-- 2 bruce users 345 Jul 29 11.05 Makefile -rw-rw-r--1 bruce users 27521 Aug 1 12:14 chap03 drwxrwxr-x 2 bruce users 1024 Aug 1 12:15 docs -rw-r--r--1 bruce users 723 Feb 9 1 998 Is1 .c -rw-r--r--1 bruce users 3045 Feb 15 03:51 Is2.c drwxrwxr-x 2 bruce users 1024 Aug 1 12:14 old.src -rw-rw-r--1 bruce users 30720 Aug 1 12:05 s.tar -rw-r-r- 1 bruce support 946 Feb 18 17:15 statl .c -rw-r-r- 1 bruce support 191 Feb 9 1998 statdemo.c -rwxrwxr-x 1 bruce users 37351 Aug 1 12:13 tail 1 -rw-r-r- 1 bruce users 1416 Aug 1 12:05 taill .c -rw-r-r- 1 cse215 cscie215 574 Feb 9 1998 writable.c
$ В каждой строке содержатся следующие семь полей: Режим. Первый символ в каждой строке предназначен для обозначения типа файла. Символ показывает, что это обычный файл, а символ “d” показывает, что это каталог. Есть еще и другие типы файлов. Вы должны еще немного изучить свойства и возможности Unix с тем, чтобы было понятно назначение других типов файлов. Последующие девять символов в первой колонке предназначены для обозначения прав доступа. Могут быть установлены или сброшены права на чтение, запись, исполнение в отношении файла для трех категорий пользователей: собственник, группа, все осталь ные. В предшествующем примере вывода все файлы и каталоги были доступны для чте ния в каждом из классов пользователей, но файлы были доступны на запись только собст веннику файлов. Откомпилированный файл taill доступен на исполнение для всех кате горий пользователей. Ссылки. Ссылки указывают на файл. Эта тема будет обсуждаться в следующей главе.
3.6. Проект2: Написание версии Is -/
105
Собственник. Каждый файл принадлежит пользователю собственнику. В данной колонке указывается пользовательское имя собственника. Группа. Каждый файл принадлежит также группе пользователей. В ряде версий команды Is в колонке указывается имя группы. Размер. В пятой колонке находится целое число, которое обозначает число байтов в фай ле. Заметим, что в этой колонке каталоги в нашем примере имеют один и тот же размер. Память под каталоги выделяется блоками, поэтому размер каталога всегда кратен 512. (Это зависит от конкретной версии Unix. Так, в HP UX под каталог выделяются блоки раз мером 1024 байта. - Примеч.ред.) Для обычных файлов размер указывается в количестве байтов данных, которые хранятся в этом файле. Время последней модификации. Следующее поле состоит из трех подстрок, где размеща ется время последней модификации. Для сравнительно новых файлов в подстроки зано сится месяц, день и время. Для более старых файлов заносится месяц, день и год. Почему такие отметки будут полезны в системе? Насколько должен быть “старым” файл, чтобы выводить в колонку год, а не время? Имя. В этой колонке изображается имя файла.
3.6.2. Вопрос 2: Как работает Is -/? Как мы можем получить информацию о файле? Давайте обратимся к электронному спра вочнику. При таком обращении: $ man -k file | grep -i information должна быть найденаполезная информация о файле, но она по-разному называется в раз личных версиях Unix. Многие версии вместо термина информация о файле используют термин статусная информация о файле, или свойства файла. Для извлечения статусной информации о файле используется системный вызов stat.
3.63Ответ: Системный вызов stat получает информацию о файле На рисунке 3.3 изображено, как работает системный вызов stat.
Рисунок 3.3 Чтение статусной информации о файле с помощью stat Файл хранится на диске. Файл имеет содержимое и набор атрибутов: размер, идентифика тор собственника и т. д. Процессу необходимо получить статусную информацию о файле. Процесс должен определить место, куда будет помещена статусная информация о файле.
106
Свойства каталогов и файлов при просмотре с помощью команды Is
Поэтому он определяет буфер типа struct stat, а затем процесс обращается к ядру с требова нием скопировать статусную информацию с диска в этот буфер. stat НАЗНАЧЕНИЕ
Получение статусной информации о файле
INCLUDE
#include < sys/stat.h >
ИСПОЛЬЗОВАНИЕ
int result = statfchar *fname, struct stat *bufp)
АРГУМЕНТЫ
fname - имя файла bufp - указатель на буфер
КОДЫ ВОЗВРАТА
-1-при ошибке 0 -при успехе
Системный вызов stat копирует статусную информацию о файле с именем fname в струк туру, на которую выставлен указатель bufp. В следующем ниже примере показывается, как используется системный вызов stat для получения размера файла. Г filesize.c - выводит размер файла passwd */ «include <stdio.h> «include <sys/stat.h> int main()
{ struct stat infobuf; /* место хранения статусной информации */ if (statfyetc/passwd”, jJnfobuf) == -1) /* получить информацию */ perror('7etc/passwd"); else printf(" The size of /etc/passwd is %d\n", infobuf.st size);
} Системный вызов stat копирует статусную информацию о файле в структуру infobuf, после чего программа читает размер файла из поля st_size в этой структуре.
3.6.4. Какую еще информацию можно получить с помощью системного вызова stat? Документация для stat и заголовочный файл /usr/include/sys/stat.h представляют описание перечня полей в структуре struct stat. stjnode st_uid st_gid st_size stjilink st_mtime st_atime st_ctime -
тип и права доступа идентификатор собственника идентификатор группы количество байтов в файле число ссылок на файл время последней модификации содержимого файла время последнего доступа время последнего изменения статусной информации
В структуре содержатся еще и другие поля. Но именно указанные поля отображаются при работе команды Is -1. Следующая далее простая программа fileinfo.c извлекает и выводит эти атрибуты.
3.6. Проект2: Написание версии Is •/
107
/* fileinfo.c - использует stat() для получения и вывода статусной информации о файле - некоторые поля просто содержат числа...
7 «include <stdio.h> «include < sys/ty pes. h «include <sys/stat.h> int main(int ac, char *av[])
{ struct stat info; if (ас>1) if(stat(av[1], &info) !=-1){ show_stat_info(av[1], &info); return 0;
/* буфер для статусной информации 7
} else perror(av[1 ]);
/* сообщения об ошибках stat()7
return 1;
} show_stat info(char *fname, struct stat *buf)
Г * отображение информации из stat в формате a name=value
7 { printff mode: %o\n", buf->st_mode); printff links: %d\n", buf->st_nlink); printff user: %d\n", buf->st_uid); printff group: %d\n", buf->st_gid); printff size: %d\n", buf->st_size); printffmodtime: %d\n", buf->st_mtime); printff name: %s\n”, fname);
/* тип + доступ 7 /* количество ссылок 7 /* id пользователя */ /* id группы */ /* размер файла 7 Г время модификации */ /* имя файла 7
} Откомпилируем и запустим на исполнение программу fileinfo, а затем сравним получен ный вывод с выводом, который получается при работе стандартной версии Is -I: $ сс -о fileinfo fileinfo.c $./fileinfo fileinfo.c mode: 100664 links: 1 user: 500 group: 120 size: 1106 modtime: 965158604 name: fileinfo.c
$ Is -I fileinfo.c -rw-rw-r--1 bruce users 1106 Aug 1 15:36 fileinfo.c
108
Свойства каталогов и файлов при просмотре с помощью команды Is
3.6.5. Чего мы достигли? Мы достигли того, что правильно отображаются такие атрибуты, как ссылки, размер, имя. Вывод значения времени модификации представлен в формате timej. Мы можем исполь зовать ctime, чтобы конвертировать это значение в строку, где будет содержаться месяц, день, время или год. В поле mode в нашем выводе выводится значение режима в числовом виде, а при работе Is вывод будет символьным: -rw-rw-r-Вывод в полях user и group представлен в числовом виде, а в команде Is в этих полях вы водятся символьные имена собственника и имя группы. Для окончания работы над нашим вариантом по написанию Is -I нам необходимо еще ознакомиться, как конвертировать числовые значения полей mode, user и group в символьные представления значений.
3.6.6. Преобразование числового значения поля mode в символьное значение Каким образом представлены разряды, соотнесенные типу файла и правам доступа, в поле st mode? Как нам выбрать эти атрибуты и представить их как последовательность из 10 символов? Какая связь между восьмеричным числом 100664 и строкой rw-rw-r-? Ответ: поле st mode шестнадцатиразрядное. Отдельные атрибуты закодированы в соот ветствующих подстроках в этом 16-разрядном поле. На рисунке 3.4 показано назначение пяти таких подстрок. Тип
^ i? Собственник Группа Остальные
w)сл -а СЛэся U
д
S
г
W X
г
W X
г
W X
Рисунок 3.4 Представление кодов типа файла и прав доступа Подстрока из первых четырех разрядов предназначена для представления типа файла. В четырехразрядном поле можно хранить 16 возможных комбинаций из 1 и 0. Каждый из этих двоичных кодов может служить для представления отдельного типа файла. В настоя щее время используется семь типов файлов. Следующая подстрока из трех разрядов предназначена для хранения специальных атрибу тов файла. Каждый разряд в этой подстроке соответствует специальному атрибуту. Если любой разряд установлен в Ч ’, то соответствующий ему атрибут установлен. Если разряд установлен в ‘0’, то соответствующий ему атрибут не установлен. Эти специальные атри буты называются set-user-ID, set-group-ID и sticky bits. Они будут рассмотрены позже. Наконец, далее расположены три последовательности трехразрядных подстрок для пред ставления прав доступа к файлу. Первая подстрока - для хранения прав доступа собствен ника, вторая подстрока - для хранения прав доступа группы и последняя подстрока - для хранения прав доступа всех остальных пользователей. Для каждого класса пользователей в подстроке из трех разрядов можно задать наличие или отсутствие прав на чтение, запись и исполнение. Значение какого-либо разряда в любой из подстрок, равное 4 Г, означает, что соответствующий вид доступа разрешен. Значение какого-либо разряда в любой из подстрок, равное ‘0’, означает, что соответствующий вид доступа запрещен.
3.6. Проект2: Написание версии Is -I
109
Секреты кодировки подполей Весьма распространенным приемом является упаковка специальных значений в подполя больших строк. Эта идея иллюстрируется на таких примерах: Примеры кодирования подстрок 617-495-4204 Область, коммутатор, линия 027-93-1111 Личный социальный номер 128.103.33.100 IP-адрес
Как читать подполя: Маскирование Как можно определить - принадлежит ли телефонный номер 212-333-4444 кодовой облас ти 212? Очень просто. Вы берете три первых числа из номера и сравниваете их подстрокой 212. Другой подход будет заключаться в том, что вы обнуляете все цифры в телефонном номере, кроме первых трех, и затем сравниваете результат с 212-000-0000. Техника обнуления указанных подполей называется маскированием. Подход напоминает о маске на лице, которая все скрывает, за исключением ваших глаз и, возможно, ушей и рта. Мы можем использовать набор масок для преобразования значения поля st_mode в символьную строку, которая выводится стандартной командой Is -1. Кодирование подполей является общим и важным методом системного программирова ния. Вам будет необходимо помнить о четырех моментах для понимания кодирования и маскирования подполей. Первый момент: Концепция маскирования Маскирование значения - это обнуление установленных значений разрядов в числе при условии, что остальные разряды остаются неизменяемыми. Второй момент: Целое число - это битовая строка Целые числа хранятся в компьютере как последовательность двоичных разрядов. На ри сунке 3.5 показано, как десятичное значение числа 215 выражается как последователь ность единиц и нулей, используя двоичную нотацию (основание 2). Каково будет деся тичное значение, которое соответствует двоичному значению 00011010? 100’s 10’s Is
\I
128
64
32
16
8
4
2
1
1
1
0
1
0
1
1
1
128
64
32
16
8
4
2
1
0
0
0
1
1
0
1
0
/
215 =
?=
0
0
Рисунок 3.5 Преобразования десятичного представления в двоичное Третий момент: Техника маскирования Операция поразрядного “И”(т. е. &) дает возможность маскировать одно значение с по мощью другого значения. На рисунке 3.6 показано восьмеричное значение 100664 (осно вание 8), которое маскируется кодом, составленным пользователем. Отметьте, как неко торые единичные разряды в исходном числе будут преобразованы в 0 с помощью опреде ленных разрядов маски.
Свойства каталогов и файлов при просмотре с помощью команды Is
110
&0
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
1
0
1
1
1
1
0
1
0
1
0
0
1
0
1
0
0
0
0
0
0
0
Рисунок 3.6 Использование двоичной маски Четвертый момент: Использование восьмеричного основания Использование масок в двоичном формате является достаточно утомительным, особенно для слов длиной в 16 или 32 разряда. Поэтому произведем группировку больших деся тичных чисел в трехсимвольные “связки” (например, 23,234,456,022) для более простого прочтения значения числа, а также сгруппируем двоичные представления больших чисел в трехсимвольные “связки” и преобразуем каждую “связку” в одно восьмеричное число (значение от 0 до 7). Например, мы можем произвести группировку по связкам в двоичном числе 1000000110110100 и получить такое представление: 1,000,000,110,110,100. После чего пре образуем каждую связку и получим такое представление числа: 0100664, которое легче воспринимается. Использование маскирования для декодирования значения типа файла Тип файла кодируется в первом четырехразрядном поле mode. Для декодирования ин формации в этом поле мы можем использовать маскирование. Прежде всего, мы исполь зуем маску для обнуления всех разрядов, кроме первых четырех разрядов. Затем сравним полученный результат с кодами для каждого из типов: Определения этих кодов находятся в заголовочном файле <sys/stat.h>: «define «define «define «define «define «define «define «define
S IFMT 0170000 S IFREG 0100000 SJFDIR 0040000 SJFBLK 0060000 S IFCHR 0020000 S IFIFO 0010000 S IFLNK 0120000 S IFSOCK 0140000
/* тип файла */ /* обычный */ /* каталог */ Г специальный блочный */ /* специальный символьный */ Г программный канал fifo */ /* символическая ссылка */ Г сокет */
Символьная константа S_IFMT - это маска, с помощью которой выбираются первые че тыре разряда. Значением маски является число 0170000. Убедитесь в том, что эта маска вы бирает правильный набор разрядов с помощью обратного преобразования каждого вось меричного представления цифры в трехразрядный двоичный эквивалент. Код типа для обычного файла (S__IFREG) равен 0100000. Значение кода типа для каталога равно 0040000. Например, во фрагменте кода: if ((info.stjnode & 0170000) = 0040000) printff'this is a directory.”);
будет проводиться проверка на тип каталога, что делается с помощью маскирования всех полей, кроме поля типа, и последующего сравнения результата с кодом типа каталога.
3.6. Проект 2: Написание версии Is -I
Если вы пожелаете написать код для маскирования и проверки, вы можете использовать при этом макросы из заголовочного файла <sys/stat.h>: /*
★
Макросы для типов файла
7 «define «define «define «define «define
S ISFIFO(m) (((m)&(0170000)) == (0010000)) SJSDIR(m) (((m)&(0170000)) == (0040000)) SJSCHR(m) (((m)&(0170000)) == (0020000)) S ISBLK(m) (((m)&(0170000)) == (0060000)) S ISREG(m) (((m)&(0170000)) == (0100000))
С помощью этих макросов можно так написать наш код: if (S_ISDIR(info.st_mode)) printff'this is a directory.");
Использование маскирования для декодирования разрядов прав доступа Последние девять разрядов в mode предназначены для представления прав доступа на вы полнение операций чтения, записи и исполнения с файлом для каждого класса пользова телей. В стандартной версии команды Is производится преобразование этих девяти дво ичных разрядов, каждый из которых установлен в 1 или 0, в строку, которая состоит из по следовательности символов и прочерков. Назначение каждого разряда маски можно посмотреть в файле <sys/stat.h>. Следующая программа представляет собой простое, читабельное приложение, которое проверяет отдельно каждый разряд: Л * В этой функции извлекается значение mode и формируется символьный массив. * В символьный массив помещается значение типа файла и * девять символов для представления прав доступа. * ЗАМЕЧАНИЕ: Коды setuid, setgid sticky * не рассматриваются
7 void mode_to_letters(int mode, char str[]) strcpy(str,........... —"); if (S_!SDI R( mode)) str[0] = 'd'; if (SJSCHR(mode)) str[0] = 'c'; if (S.ISBLK(mode)) str[0] = ’b’; if (mode & S IRUSR) str[1] = ‘r’; if (mode & SJWUSR) str[2] = ’w’; if (mode & S.IXUSR) str[3] = 'x'; if (mode & SJRGRP) str[4] = V; if (mode &S IWGRP) str[5] = ’w’; if (mode &S IXGRP) str[6] = Y; if (mode &S IROTH) str[7] ='r';
/* no умолчанию - отсутствие всех прав 7 /* каталог */ Г символьные устройства 7 Г блочное устройство 7 /* 3 разряда для собственника 7
Г 3 разряда для группы */
[* 3 разряда для всех остальных 7
112
Свойства каталогов и файлов при просмотре с помощью команды Is if (mode & SJWOTH) str[8] = 'w'; if (mode &S IXOTH) str[9] Y;
} Декодирование разрядов и написание версии Is
У нас накопилось достаточно знаний для написания версии команды Is, которая может правильно работать с длинным форматом вывода. Мы можем правильно выводить значе ния таких атрибутов файла, как размер, ссылки и имя файла. Мы имеем возможность взять значение поля mode и преобразовать его значение в стандартную последовательность из символов и прочерков. Можно преобразовать с помощью ctime значение времени из формата time_t в строковый формат. А каковы соображения по строчному представлению имен собственника и группы?
3.6.7. Преобразования числового представления идентификаторов собственника/группы в строковое представление В нашем варианте в выводе для представления собственника и группы выдаются числа. В стандартном выводе команды Is выводится символьное пользовательское имя и имя группы. Какая связь между числовым идентификатором пользователя июи пользователь ским именем? При обращении к документации для поиска по ключевым словам username, w/d и group бу дет получен весьма различный по составу результат поиска, который будет зависеть от версии Unix. Посмотрим, что можно найти. Есть несколько интересующих нас факторов. Фактор первый: Файл /etc/passwd содержит список пользователей
Как производится ваш вход в Unix - машину? Сначала система запросит у вас входное пользовательское имя, а затем пароль. Далее система определяет, верны ли указанные значения входного пользовательского имени и пароля. Как она узнает об их правильно сти? Традиционная система для учета пользовательских имен и паролей состоит из файла /etc/ passwd. В этом поле находится список всех пользователей данной системы. Содержимое файла выглядит так: root:WPMd10wUxypE:0:0:root:/root:/bin/bash bin:*: 1:1 :bin:/bin: daemon:*:2:2:daemon:/sbin: smith:x1mEPcp4TNokc:9768:3073:James Q Smith:/home/s/smith:/shellsAcsh fred:mSuVNOF4CRTmE:20359:550:Fred:/homeAAred:/shellsAcsh diane:7oUS8f1PsrccY:20555:550:Diane Abramov:/home/d/diane:/shellsAcsh ajr:WitmEBWylar1 w:3607:3034:Ann Reuter:/home/a/ajr:/shells/bash
Этот последовательный текст представляет собой список пользователей и информации о каждом из пользователей. Каждая строка в файле представляет одного пользователя. Поля в каждой строке разделяются знаком двоеточия. Первое поле предназначено для хра нения пользовательского имени. Второе поле содержит зашифрованный пароль. Третье поле хранит значение пользовательского идентификатора, четвертое поле предназначено для хранения идентификатора группы, членом которой является пользователь. Сле дующие поля: поля для представления фактического имени пользователя, поле для указа ния домашнего каталога пользователя, поле для хранения маршрутного имени програм мы, которую пользователь использует в качестве shell. (Речь идет о произвольной про
3.6. Проект 2: Написание версии Is -/
113
грамме, которая запускается в начале сессии пользователя. - Примеч. пер.) Файл passwd доступен для чтения для всех категорий пользователей. Для более детального ознакомле ния с файлом обратитесь к электронному справочнику с аргументом passwd. Все выглядит вполне оптимистично. Достаточно найти в файле запись, которая содержит не обходимый идентификатор пользователя, а далее необходимо прочитать первое поле в выбранной строке. Но этот метод не перспективен и вот почему: поиск в файле /etc/passwd достаточно скучное занятие, кроме того метод не работает во многих сетевых системах. Фактор второй: В файле /etc/passwd не всегда содержится полный список пользователей
В каждой системе Unix есть файл /etc/passwd, но во многих системах Unix в этот файл включаются не все пользователи. В сетевых реализациях систем предполагается регистрация пользователей на любой машине в сети с одним и тем же именем пользователя и паролем. Чтобы достигнуть такой возможности, используют файл /etc/passwd1. Системный администра тор должен будет добавить в этот файл на каждой машине в сети одно и то же пользователь ское имя и текущий пароль. Когда пользователь захочет изменить на какой-то машине пароль, то это изменение должно быть сделано в каждом файле /etc/passwd сети. Если одна из машин будет недоступна, то это может привести к нарушению процесса синхронизации в отношении оставшихся машин. Одно из решений - инсталлировать минимально файл /etc/passwd на каждой машине для автономных действий, но поддерживать полный список пользователей в базе данных, которая доступна в сети. Все новые пользователи и изменения паролей записываются в эту центральную базу данных. Все программы, которым необходима информация о пользователе, будут обращаться к центральной базе данных. Система с централизован ной сетевой информацией называется nis. В электронном справочнике можно получить дополнительную информацию по этому поводу. Фактор третий: Доступ к полному списку пользователей обеспечивает функция getpwuid
Библиотечная функция getpwuid предоставляет доступ к пользовательской информации в базе данных. Если в вашей системе используется файл passwd, то функция будет работать с этим файлом. А если используется центральная база данных, то функция getpwuid будет работать с этой базой. При использовании функции getpwuid в качестве аргумента задается идентификатор пользователя, а в результате функция возвращает указатель на структуру struct passwd, которая описана в файле /usr/include/pwd.h так: Г Структура passwd. */ struct passwd
{ char *pw_name; /* Пользовательское имя. */ char *pw_passwd; /* Пароль. */ _uidj pw_uid; /* Пользовательский ID. */ __gid_t pw_gid; /* Групповой ID. */ char *pw_gecos; Л Реальное имя. */ char *pw_dir; /* Домашний каталог. */ char *pw_shell; /* Программа Shell. 7
};
1. Во многих системах пароли хранятся в зашифрованном виде в файле shadow, чтобы увеличить степень безо пасности системы.
/14
Свойства каталогов и файлов при просмотре с помощью команды Is
Эта функция и описание этой структуры дают нам возможность организовать вывод поля с пользовательским именем в длинном формате. Вот таким может быть простое решение: Л * возвращается пользовательское имя, соотнесенное uid * ЗАМЕЧАНИЕ: код не работает, если нет пользовательского имени
7 char *uid to name(uid t uid)
{ return getpwuid(uid)->pw name;
} Эта функция проста, но ненадежна. Если значению uid не найдено соответствующее поль зовательское имя, то функция getpwuid возвращает указатель NULL. В этом случае нечего разыменовывать в pw_name. Как это может произойти? В стандартной версии команды Is приводится решение этой проблемы. Фактор четертый: Для некоторых UID нет входных имен
Скажем, что вы зарегистрированы на некоторой Unix-машине и вам присвоено пользова тельское входное имя pat, значение идентификатора пользователя равно 2000. Когда вы создаете файлы, то будете собственником этих файлов. То есть системный вызов stat будет возвращать в качестве результата структуры для ваших файлов, где в поле st_uid будет на ходиться 2000. Это число является атрибутом файла. Далее вы уехали в другой город. Системный администратор удалит учетную запись о вас из файла passwd. Тем самым удаляется связь между числом 2000 и пользовательским име нем pat. Если программа будет передавать число 2000 при обращении к системному вызо ву getpwuid, то системный вызов будет возвращать null. В стандартной версии Is происходит обработка данной ситуации - будет выводиться uid, если нет соответствующего пользовательского имени. Что произойдет, если в системе будет зарегистрирован новый пользователь и ему будет присвоено значение старого UID? В системе могли остаться файлы, у которых теперь соб ственником становится этот новый пользователь. Этот пользователь имеет права на чте ние, запись и удаление этих файлов. И наконец, как мы можем преобразовать идентификатор группы в имя группы? Что такое группа? Что такое идентификатор группы? Фактор пятый: Файл /etc/group содержит список групп
Рассмотрим Unix-машину, которая используется в сфере бизнеса. В этой области все работники сгруппированы по отделам и отдельным проектам. Может быть группа людей, которые занимаются продажами, группа менеджмента и т. д. Рассмотрим школу. Весь состав людей в школе можно представить так: учителя, школьни ки, администрация. Людей можно сгруппировать и по другим признакам - по принадлеж ности студентов к одному и тому же курсу, по месту работы в одном и том же отделе. В Unix имеется система для регистрации групп и введения пользователей в состав групп. Имеется файл /etc/group, который является обыкновенным текстовым файлом и который выглядит примерно так:
3.6. Проект 2: Написание версии Is -/
115
root::0:root other:: 1: bin:: 2: root, bin, daemon sys::3:root,bin,sys,adm adm::4:root,adm, daemon uucp::5:root,uucp mail::6:root tty::7:root,tty,adm lp::8:root,lp,adm
Первое поле предназначено для хранения имени группы, во второе поле записывается пароль группы (редко используется на практике), в третье поле записывается идентифика тор группы, в четвертом поле хранится список пользовательских имен. Элементы списка разделяются запятыми. Эти пользователи составляют группы. Фактор шестой: Пользователь может быть членом более чем одной группы
В файле passwd для каждого пользователя заведены поля uid и gid. Идентификатор группы в файле passwd указывает первичную группу для пользователя, но пользователь может быть также зарегистрирован в составе других групп. В примере, приведенном выше, вы можете заметить, что пользователь adm находится в группах с именами sys, adm, tty, 1р. Этот список используется при работе с разрядами прав доступа для группы. Например, если файл принадлежит группе с именем 1р и для группы установлены права на запись, тогда пользователь adm может модифицировать этот файл. Фактор седьмой: Системный вызов getgrgid предоставляет доступ к списку групп
В сетевом варианте системы данные, которые размещаются в файле /etc/group, также мож но переместить в центральную базу данных. Аналогично работе со статусной информаци ей для файлов в Unix есть возможность получать доступ к списку групп независимо от реализации системы. В документации на getgrgid приведены детали и необходимая информация. Для наших целей будем использовать код, подобный приведенному ниже. Г * возвращает имя группы, которое соотнесено указанному gid * ЗАМЕЧАНИЕ: не работает, если нет имени группы
*/ char 'gid_to name(gid t uid)
{ return getgrgid(gid)->gr name;
}
3.6.8. Объединение всего вместе: Is2.c Мы проверили каждый компонент в выводе Is -1. Для каждого из них мы знаем, что означа ет каждое поле и как можно преобразовать значение поля в форму, наиболее понятную для пользовательского восприятия. В результате программа Is2.c будет такой:
Г IS2.C * цель - вывод списка содержимого каталога или каталогов ’ при отсутствии аргументов используется., в противном случае
6
Свойства каталогов и файлов при просмотре с помощью команд^ * используется список имен файлов через список аргументов * замечание - использует stat, pwd.h и grp.h * BUG: попробуйте Is2 Дтр
*1 «include <stdio.h> «include <sys/types.h> «include «include <sys/stat.h> void do_ls(char[]); void dostat(char *); void show_file_info(char *, struct stat *); void mode_toJetters(int, char []); char *uid_to_name(uid_t); char xgid_to_name(gid_t); main(intac, char*av[])
{ if (ac == 1) do_ls("."); else while (--ac){ printf("%s:\n", *++av); do ls(*av);
} } void do_ls(char dirnameQ)
Г * перечисляет файлы в каталоге с именем dirname
7 { DIR *dir_ptr; struct dirent ‘direntp; if ((dir_ptr = opendir(dirname)) == NULL) fprintf(stderr,"ls1: cannot open %s\n", dirname); else
/* каталог */ /* какая запись */
{ while ((direntp = readdir(dir_ptr)) != NULL) dostat(direntp- >d .name); closedir(dir ptr);
} } void dostat(char ‘filename)
{ struct stat info; if (stat(filename, &info) == -1) perror(filename); else
I* неудача у stat */ Г посмотреть почему */ /* иначе показать информацию */
?. Проект 2: Написание версии Is -/ show file info(filename, &info);
} void show_file_info(char ‘filename, struct stat *info_p)
Г * выводит информацию о 'filename'. Эта информация записана в структуре *info_p 7
{ char *uid_to_name(), *ctime(), *gid_to_name(), *filemode(); void mode_to_letters(); char modestr[11]; mode_to_letters(info_p- >st_mode, modestr); printf("%s", modestr); printf("%4d", (int) info_p->st_nlink); printf("%-8s", uid_to_name(info_p->st_uid)); printf("%-8s", gid_to_name(info_p->st_gid)); printf("%8ld", (long) i nfo_p - >st_size); printf("%.12s", 4+ctime(&info_p->st_mtime)); printf("%s\n", filename);
} Г * utility functions 7
Г * В этой функции извлекается значение mode и формируется символьный масси * В символьный массив помещается значение типа файла и * девять символов для представления прав доступа. ’ ЗАМЕЧАНИЕ: Коды setuid, setgid sticky ’ не рассматриваются 7 void mode_toJetters(int mode, char str[])
{ strcpy(str,"-------- "); if (SJSDIR(mode)) str[0] = 'd'; if (S_ISCHR(mode)) str[0] = 'c'; if(S_ISBLK(mode))str[0] = 'b'; if (mode & SJRUSR) str[1] = 'r'; if (mode & SJWUSR) str[2] = 'w'; if (mode & SJXUSR) str[3] = 'x'; if (mode & SJRGRP) str[4] = 'r'; if (mode &S IWGRP) str[5] = V; if (mode &S IXGRP) str[6] ='x'; if (mode & SJROTH) str[7] = 'r'; if (mode & SJWOTH) str[8] = 'w'; if (mode & S_IXOTH) str[9] = 'x';
/* по умолчанию отсутствие прав */ Г каталог? 7 /* символьные устройства 7 /* блочное устройство 7 /* 3 разряда для собственника 7
Г 3 разряда для группы 7
/* 3 разряда для остальных */
118
Свойства каталогов и файлов при просмотре с помощью команды Is
} «include char *uid to name(uid t uid)
Г * возвращается указатель на пользовательское имя, соотнесенное * идентификатору uid, используется getpw()
7 { struct passwd *getpwuid(), *pw_ptr; static char numstr[ 10]; if {(pw_ptr = getpwuid(uid)) == NULL){ sprintf(numstr,"% d", uid); return numstr;
} else return pw ptr->pw name;
} «include char *gid to name(gid t gid)
Г * возвращается указатель на имя группы, используется getgrgid(3)
7 { struct group *getgrgid(), *grp_ptr; static char numstr[10]; if ((grp.ptr = getgrgid(gid)) == NUI1){ sprintf(numstr,"% d", gid); return numstr;
} else return grp ptr- >gr name;
} И вот теперь запустим нашу программу и получим также для сравнения стандартный вывод:
$ls2 drwxrwxr-x drwxrwxr-x -tw-rw-r--rwxrwxr-x - rw-rw-r*.rw-r-r-rw-r—Г--rw-rw-r-drwxrwxr-x drwxrwxr-x
4 bruce 5 bruce 1 bruce 1 bruce 2 bruce 1 bruce 1 bruce 1 bruce 2 bruce 2 bruce
bruce bruce users users users users users users users users
1024 Aug 1024 Aug 30720 Aug 37351 Aug 345 Jul 723 Aug 3045 Feb 27521 Aug 1024 Aug 1024 Aug
218:18. 218:14.. 1 12:05 s.tar 1 12:13 taill 2911:05 Makefile 1 14:26 Isl.c 1503:51 Is2.c 1 12:14 chap03 1 12:14 old_src 1 12:15docs
3.7. Три специальных разряда -rwxrwxr-x -rw-r--r-- rwxrwxr-x -rw-r-r--rw-r-r--
119
1 bruce 1 bruce 2 bruce 1 bruce 1 bruce
bruce support bruce support users
37048 Aug 946 Feb 42295 Aug 191 Feb 1416 Aug
1 14:26 Is1 18 17:15 statl.с 2 18:18 Is2 9 21:01 statdemo.c 1 12:05 taill.с
2 bruce 1 bruce 2 bruce 1 bruce 1 bruce 2 bruce 1 bruce 2 bruce 1 bruce 1 bruce 1 bruce 1 bruce 1 bruce
users users users bruce users bruce users users users support support users users
345 Jul 27521 Aug 1024 Aug 37048 Aug 723 Aug 42295 Aug 3045 Feb 1024 Aug 30720 Aug 946 Feb 191 Feb 37351 Aug 1416 Aug
29 11:05 Makefile 1 12:14 chap03 1 12:15 docs 1 14:26 Is1 1 14:26 Isl.c 2 18:18 Is2 15 03:51 Is2.c 1 12:14 old_src 1 12:05 s.tar 18 17:15 statl.с 9 1998 statdemo.c 1 12:13 taill 1 12:05 taill.с
$ Is -1 total 189 -rw-rw-r--rw-rw-r-drwxrwxr-x -rwxrwxr-x -rw-r--r- rwxrwxr-x -rw-r-rdrwxrwxr-x -tw-rw-r-rw-r--r-rw-r-r--rwxrwxr-x -rw-r-r--
$ Чего мы достигли? Программа ls2 отображает информацию о файлах в стандарте вывода команды Is -1. Вывод выглядит хорошо. Он происходит поколонно, производится преобразование из внутренне го представления разрядов доступа и числовых значений идентификатора в читабельные строки. Но программа все же нуждается в доработке. В реальной версии в самой первой строке вывода печатается строка total. Зачем нужна эта строка? Кроме того, в нашей программе все еще нет сортировки имен файлов, не работает опция - а, не производится упорядоче ния имен файлов по колонкам, программа рассматривает каждый аргумент при обраще нии к ней в качестве имени каталога. В программе ls2 есть еще более серьезные проблемы. Она не будет корректно выдавать ин формацию о файлах, которые находятся в других каталогах. Для рассмотрения проблемы попытайтесь выполнить команду ls2 /tmp. Следует решить эту проблему, что вы должны сделать в качестве упражнения.
3.7. Три специальных разряда Поле st_mode в структуре stat содержит шестнадцать разрядов. Четыре разряда исполь зуются для хранения типа файла, девять - для хранения прав доступа. Три оставшихся разряда используются для организации действий со специальными атрибутами файла.
3.7.1. Разряд Set-User-ID Первый из трех специальных разрядов называется set-user-lD. Он используется для реше ния важного вопроса:
120
Свойства каталогов и файлов при просмотре с помощью команды Is
Как может обычный пользователь изменить его или ее пароль? Это легко сделать, используя команду passwd. Но как работает команда passwd? Заметьте кто является собственником и каковы права доступа к файлу паролей.
$ Is -I /etc/passwd -rw-r-r- 1 root root 894 Jun
2019:17/etc/passwd
Изменение вашего пароля означает изменение вашей учетной записи в этом файле, но вы не имеете прав доступа на запись в этот файл. Права на запись имеет только пользова тель с именем root. Как добиться при использовании программы passwd, чтобы вы получи ли бы право на изменение файла, который не имеете права изменять? Почувствовали про блему? Решением будет предоставление прав на запись программе, но не вам. Вы используете программу /usr/bin/passwd или /bin/passwd для изменения вашего пароля, собственником ко торой является root и для которой установлен разряд set-user-lD. Права доступа будут вы глядеть так:
$ Is -I /usr/bin/passwd -r-sr-xr-x 1 root bin 15725 Oct 31 1997 /usr/bin/passwd
Установленный разряд suid сообщает ядру о необходимости запускать программу так, что предполагается, что программу запустили не вы, а собственник этой программы. Назва ние разряда set-user-Ю (Буквальный перевод - установить идентификатор пользователя. Но в русскоязычной литературе название этого разряда не переводится. - Примеч. пер.) подчеркивает тот факт, что этот разряд требует у ядра присвоения эффективному пользо вательскому идентификатору значения пользовательского идентификатора собственни ка программы. Пользователь root является собственником файла/etc/passwd. Поэтому про грамма, которая запускается в статусе root, может модифицировать учетный файл. Не означает ли это, что я могу изменять пароли других пользователей? Нет. Программа passwd знает, кто вы такой. Она использует системный вызов getuid, чтобы узнать с помощью ядра - какой был у вас UID, когда вы вошли в систему. Программе pass wd предоставлена возможность перезаписывать любые записи в учетном файле, но она бу дет изменять только запись, которая принадлежит пользователю, который запустил про грамму passwd. Другие случаи использования разряда Set-User-ID Разряд suid может быть использован некой программой, которая должна контролировать доступ к файлу или к каким-то другим ресурсам. Рассмотрим систему печати с буфериза цией (систему спулинга). Для многих пользователей возникает необходимость распеча тать свои файлы, но принтер может печатать в каждый момент времени только один файл. В Unix есть команда Ipr. (В HP-UX утверждается, что Ipr - это команда печати для Linux, а для Unix - 1р. - Примеч. ред.) Команда копирует ваш файл в каталог, где он будет ждать, когда он будет распечатан. Но было бы рискованным разрешить всем пользователям ко пировать свои файлы в этот каталог для спулинга и разрешать им модифицировать списки имен файлов, которые ждут в очереди на печать. Реально все происходит так. У програм мы Ipr собственником является root или Ipr, и для нее установлен разряд set-uid. Когда вы, обычный пользователь, используете команду Ipr, то программа будет запущена с установ ленным значением root или Ipr для эффективного UID и может теперь модифицировать содержание каталога для спулинга и соответствующих файлов в нем. Программы для удаления заданий на печать из очереди на печать также имеют установленные разряды set-uid.
121
3.7. Три специальных разряда
Компьютерные игры, которые модифицируют базы данных со счетчиками для игроков или читают файлы с секретными планами, будут маркироваться так, что они будут запус каться пользователем и работать в статусе собственника базы данных или секретных фай лов. Любой пользователь может играть, но только программа игры может модифициро вать списки со счетом или читать секретные планы. Маска для определения значения разряда suid Программа может проверить, установлен ли разряд set-user-ID для файла, с помощью мас ки, которая определена в заголовочном файле: <sys/stat.h>. Определение такое: #define SJSUID 0004000 /* set user id на исполнение */
Вы можете убедиться, что маска выбирает первый из трех специальных битов.
3.7.2 Разряд Set-Group-ID Второй специальный разряд используется для установки эффективного группового иден тификатора программы. Если программа принадлежит группе g и установлен разряд set - group ID, то программа будет запускаться так, если бы она запускалась на исполне ние членом группы g. Этот бит предоставляет программе права доступа, которые припи саны членам группы. Программист может проверить значение данного разрядах помощью такой маски: #define SJSGID 0002000
Г установить group id на исполнение */
3.7.3 Разряд Sticky Bit Этот разряд имеет две различные области использования - для работы с каталогами и для работы с файлами. Поговорим сначала о файлах. В Olden Days ®#.Что это такое? Unix разрешала проблему одновременного исполнения нескольких программ с помощью тех ники, которая называется своппированием. Рассмотрим следующую ситуацию. На вашем компьютере есть 1 мегабайт пользовательского пространства памяти, и вы запускаете на исполнение три программы, каждая из которых использует 0,5 мегабайта памяти. Очевид но, что только две программы из них могут одновременно находиться в памяти. Куда ядро поместит те программы, которые в текущий момент не могут быть исполнены? В Olden Days ®. Что это такое? было решено, что ядро может размещать сразу всю программу в раз деле твердого диска, который резервируется специально для своппирования. В некоторый момент эта программа может быть повторно запущена на исполнение. Тогда ядро выгру жает эту программу из области своппирования, а помещает туда одну из исполняемых до этого момента программ. Загрузка программы, которая хранилась на устройстве для своппинга, будет происходить более быстро, чем загрузка программы из обычного раздела диска. При хранении про граммы в обычном разделе на диске текст программы может быть фрагментирован, т. е. разбит на много малых секций, которые разбросаны по диску. При хранении программы на устройстве для своппирования текст программы не фрагментирован. Рассмотрим теперь такие программы, которые интенсивно используются. Это редакторы, компиляторы или компьютерные игры. Если копии таких программ поместить для хране ния на устройстве для своппирования, то ядро будет загружать эти программы быстрее. Установленный разряд sticky bit (Название этого бита принято не переводить. - Примеч. пер.) для какой-либо программы говорит ядру о необходимости хранить эту программу на устройстве для своппинга, даже если никто эту программу в текущий момент времени не
122
Свойства каталогов и файлов при просмотре с помощью команды Is
вызывает. Название разряда (sticke — приклеивать) обусловлено тем фактом, что програм ма ‘‘приклеивается ” к устройству для своппирования так же, как жевательная резинка приклеивается к вашему ботинку. К настоящему времени признано, что своппировать полностью тексты программ (туда и обратно) больше нет необходимости. Теперь используется механизм виртуальной памя ти, который позволяет ядру выгружать и загружать программы в память небольшими час тями, которые называются страницами. У ядра отпадает необходимость загружать полно стью блок кода, чтобы запустить программу на исполнение. Разряд sticky bit имеет другой смысл, если он установлен для каталога. Это значение так же относится к проблеме “приклеивания”. Некоторые каталоги создаются для хранения в них временных файлов. Эти временные каталоги, и прежде всего /tmp, доступны всем для записи, что дает возможность любому пользователю создавать и удалять любые фай лы в таком каталоге. Sticky bit, который может быть установлен для каталога, аннулирует возможность доступа на запись для всех в этом каталоге. В таком случае файлы в каталоге могут удалять только их собственники.
3.7.4. Специальные разряды и Is -/ Как мы убедились, каждый файл имеет атрибут типа и 12 разрядов для других атрибутов, но команда Is резервирует для вывода только девять знакомест для изображения в них этих 12 атрибутов. Как происходит отображение этих значений? В документации команды Is приведены детали. Пример -rwsr-sr-t 1 root
root
2345 Jun 12 14:02 sample
показывает, что символ s используется в тех же местах, где может быть символ х для поль зователя и группы. Символ s показывает, что произошла замена символа х на символы s, для обозначения установленных разрядов set-user and set-group-ID. Символ t свидетельст вует об установленном разряде sticky bit.
3.8. Итоги для команды is Мы теперь имеем работающую версию команды Is, которая выводит список файлов в ка талоге и отображает статусную информацию об этих файлах. По мере того как мы рас сматривали возможности команды Is, рассматривали, как работает эта команда и при напи сании нашей собственной версии программы, у нас сложилось, в некотором смысле, опре деленное представление об Unix. Далее следует список основных тем.
Каталоги и файлы В Unix данные хранятся в файлах. Каталог - это специальный тип файла. В каталоге имеется список имен файлов. В каталоге содержится также его собственное имя. В Unix есть набор функций, которые позволяют открывать, читать, искать и закрывать каталоги. Функции для записи в каталог отсутствуют.
Пользователи и группы Каждому, кто использует систему, присваивается имя пользователя и числовое значение идентификатора пользователя. Пользовательские имена используются людьми для вхож дения в систему и установления связей с другими людьми. Система использует значения UID для идентификации собственника файла. Люди принадлежат различным группам. Каждая группа имеет имя и числовой идентификатор группы.
3.9. Установка и модификация свойств файла
123
Атрибуты файла Каждый файл имеет набор свойств. Программа может получить список свойств файла с помощью системного вызова stat.
Собственник файла У каждого файла есть собственник. UID собственника в Unix записывается в качестве свойства файла. Файл принадлежит группе. GID группы в Unix записывается в качестве свойства файла.
Права доступа Пользователи могут читать файлы, писать в файлы и исполнять файлы. Каждый файл име ет набор разрядов, которые определяют, какие пользователи могут выполнять эти опера ции. Права на чтение, запись и исполнение могут контролироваться на трех уровнях: соб ственник, группа, остальные.
3.9. Установка и модификация свойств файла Команда Is -I отображает несколько свойств файла. Как можно устанавливать эти свойст ва? Можно ли изменять их значения? Если да, то как это делать? Если нет, то почему? Про верим установленные значения свойств в выводе в длинном формате: -rw-r-r- 1 bruce users 3045 Feb 15 03:51 Is2.c
Рассмотрим слева направо каждый из атрибутов.
3.9.1. Тип файла Файл имеет тип. Могут быть обычные файлы, каталоги, файлы устройств, сокеты, симво лические ссылки и именованные программные каналы. Установка типа файла. Тип файла устанавливается при создании файла. Например, с помощью системного вызова creat создается обычный файл. Для создания каталогов, файлов устройств и других типов файлов используются другие системные вызовы. Изменение типа файла. Тип файла изменить невозможно. В сказках тыквы превращают ся в кареты, но никто не объясняет, куда девать семечки и мякоть тыкв.
3.9.2. Разряды прав доступа и специальные разряды Каждый файл имеет девять разрядов прав доступа и три специальных разряда. Эти разря ды устанавливаются при создании файла и могут быть модифицированы с помощью сис темного вызова chmod. Установка режима файла. Второй аргумент, который задается в системном вызове creat, служит для задания значений прав доступа, которые будут установлены при создании файла. Например: fd = creat(MnewfileM, 0744);
Будет создан файл newfile, для которого требуется установить начальный набор таких прав доступа: rwxr— г—. Второй аргумент в creat - это требование на установку доступа. Ядро будет выбирать это требуемое значение и далее накладывать на него маску. В результате получается дво ичный код, который и будет окончательным для установки прав доступа. Маска называет ся маска на создание файлов и определяет, какие разряды в исходном требовании на права доступа должны быть сброшены. Например, если вы хотите запретить программам созда
Свойства каталогов и файлов при просмотре с помощью команды Is
124
вать файлы в системе, которые можно было бы модифицировать группе и остальным пользователям, то вы должны будете сбросить разряды: —w--w-, что соответствует вось меричному коду 022. Системный вызов umask в таком варианте: umask(022);
установит маску на создание файлов, по значению которой будет происходить сброс этих двух разрядов. В общем случае маски используются для включения и выбора разрядов. В данном случае маска определяет, какие разряды следует сбросить. Да, такая вот обрат ная трактовка смысла. Изменение режима файла. Программа может модифицировать значения прав доступа и значения специальных разрядов с помощью системного вызова chmod. Два примера: chmod(7tmp/myfile,\ 04764);
и chmod(7tmp/myfile,,l SJSUID | SJRWXU | SJRGRP|SJWGRP | SJR0TH);
имеют один и тот же результат. В первом случае указывается новый двоичный код, выра женный в восьмеричном формате, а во втором случае указываются маски, которые опре делены в файле <sys/stat.h>, комбинируются в один двоичный набор или оператор. Во втором случае можно изменять значение разрядов доступа в будущей работе, не прерывая для этого вашу программу. Количество существующих программ, которые используют точное восьмеричное представление, таково, что можно говорить о меньшей привлекательности этого варианта для тех, кто собирается менять значения разрядов дос тупа. Значение маски на создание файлов не влияет на значение режима, которое задается при обращении к системному вызову chmod. В заключение суммируем свойства в данной таблице:
chmod НАЗНАЧЕНИЕ
Изменение прав доступа и специальных разрядов для файла
INCLUDE
#include < sys/types.h > #include <sys/stat.h>
ИСПОЛЬЗОВАНИЕ
int result = chmod(char *path, mode_t mode);
АРГУМЕНТЫ
path - путь к файлу mode - новое значение режима
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Команда Shell для изменения прав доступа и специальных разрядов. Для модификации прав доступа и специальных разрядов используется обычная Unix-команда chmod. Команда chmod допускает возможность для пользователя задавать двоичный код режима в восьмеричном представлении (например, 04764) или в символьной нотации (например, и -rws g =rw о =r).
3.9.3. Число ссылок на файл Назначение этого атрибута будет рассмотрено в следующей главе. Если говорить кратко, то число ссылок соответствует числу обращений к файлу в разных каталогах. Если файл оказывается представлен в трех местах, в различных каталогах, то число ссылок будет равно 3.
3.9. Установка и модификация свойств файла
125
Для увеличения значения счетчика ссылок нужно создать новые ссылки. (Вы можете ис пользовать для этого системный вызов link.) Для уменьшения значения счетчика ссылок, необходимо удалить какое-то число ссылок. (Вы можете использовать для этого систем ный вызов unlink.)
3.9.4. Собственник и группа для файла У каждого файла есть собственник. Для внутреннего представления в Unix собственник представлен числовым значением UID, а группа, использующая файл, представлена числовым значением GID. Установление собственника файла. В самом простом толковании собственник файла это пользователь, который создал файл. Но файлы создают не люди, а ядро. Ядро создает файл, когда в процессе выполняется системный вызов creat. Когда ядро создает файл, оно устанавливает в качестве собственника файла эффективный UID процесса, который вы полнял вызов creat. Значение эффективного UID процесса обычно равно значению UID того, кто породил процесс. Если в программе процесса был установлен разряд set-user-ID, то эффективный UID будет равен значению того пользователя, кто является собственни ком этой программы. Все ясно? Установление группы для файла. Обычно в качестве группы для файла устанавливается эффективный GID процесса, который создает файл. Но иногда значением GID для файла становится значение GID родительского каталога. Ну, как? Такие действия напоминают процедуру, как если бы национальность устанавли валась по месту вашего рождения и не принимались во внимание ваши родители, которые вас и создали. В системе делается нечто напоминающее эту процедуру. Изменение собственника и группы для файла. Программа может изменять собственника и группу для файла с помощью системного вызова chown. Например: chownC'filel", 200,40);
Здесь происходит изменение пользовательского ID на 200, значение группового ID заме няется на 40 для файла с именем filel. Если какой-либо аргумент будет иметь значение -1, то этот атрибут не модифицируется. Обычно пользователи не меняют собственника фай ла. Суперпользователь может установить в любой момент и для любого файла требуемое значение пользовательского ID и группового ID. Этот вызов обычно используется для установки и управления пользовательскими входами в систему. Собственник файла может изменить групповой ID файла в любой группе, к которой он принадлежит. В итоге мы имеем следующую таблицу с характеристиками вызова: chown НАЗНАЧЕНИЕ
Изменение собственника или группового ID для файла
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
int chown(char *path, uidj owner, gid J group)
АРГУМЕНТЫ
path - путь к файлу owner - пользовательский ID для файла
КОДЫ ВОЗВРАТА
group - групповой ID для файла -1 - при ошибке 0 - при успехе
126
Свойства каталогов и файлов при просмотре с помощью команды Is
Команды Shell для изменения идентификаторов пользователя и группы для файлов В shell есть обычные команды chown и chgrp, с помощью которых программы могут моди фицировать пользовательский ID и групповой ID для файлов. В одной команде с помощью этих команд можно изменять UID и GID для нескольких файлов. В документации изложе ны все детали. В командах chown и chgrp пользователи могут задавать идентификаторы или в числовом варианте, или в символьном - как имена пользователей и имена групп.
3.9.5. Размер файла Размер файла, каталога и именованного программного канала представляется в выводах числом хранимых байтов в таких файлах. Программы могут увеличить размер файла до бавлением в него данных. Программы могут обнулить размер файла с помощью систем ного вызова creat. Программы не могут сокращать размер файла до некоторой ненулевой длины. (Утверждение слишком категорично. Для сокращения размера можно использо вать системный вызов truncate. - Примеч. ред.)
3.9.6. Время последней модификации и доступа Каждый файл имеет три временных отметки: время последней модификации файла, время последнего чтения из файла и время последней модификации статусной информации фай ла (таки,е как идентификатор собственника или права доступа). Ядро автоматически мо дифицирует значения этих времен, когда программы пишут в файлы или читают из файла. Это может показаться странным, но вы можете написать программы, которые устанавли вали бы произвольные значения для времени последней модификации и времени послед него доступа.
Изменение значений времен последней модификации и последнего доступа к файлу С помощью системного вызова utime можно устанавливать время последней модификации и время последнего доступа к файлу. Для того чтобы использовать системный вызов utime, создается структура, в которой находятся два элемента time_t, один для хранения времени доступа, а другой - для времени модификации. Затем происходит вызов utime, где задается имя файла и указатель на эту структуру. Ядро устанавливает в этой структуре значения времени доступа и времени модификации для этого файла. В итоге сведем свойства вызова в таблицу: utime НАЗНАЧЕНИЕ
Изменение времени модификации и доступа к файлу
INCLUDE
#include < sys/time.h > #include
ИСПОЛЬЗОВАНИЕ
#include <sys/types.h> int utime(char *path, struct utimbuf *newtimes)
АРГУМЕНТЫ
path - путь к файлу newtimes - указатель на структуру utimbuf См. более детально в utime. h
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Почему у вас может появиться желание изменить время последней модификации или по следнего доступа? Использование системного вызова utime будет полезно, в частности, когда вы извлекаете файлы из копий (backups) и архивов. Рассмотрим набор файлов, который был сброшен в backup. При х анении этого набо а на диске или на ленте эти
127
Заключение
файлы будут иметь свои первоначальные значения времен модификации. Когда програм ма восстанавливает файлы из backup, то она гарантирует, что получит файл назад с пра вильным временем модификации. Программа, которая копирует файлы из места хранения backup, выполняет два действия. Во-первых, она копирует данные в новый файл. Затем она изменяет время модификации и время доступа так, чтобы они были равны значениям для оригинальных файлов, которые остались в backup на диске. Таким образом, ваши восстановленные файлы имеют то же содержимое и те же свойства, что и оригинальные файлы. Команды Shell для изменения времени модификации и времени доступа. Обычная Unix-команда touch выполняет установку значений времени модификации и времени доступа к файлам. В документации приведены подробности об этой команде.
3.9.7. Имя файла Когда вы создаете файл, вы присваиваете ему имя. С помощью команды mv можно изменять имя файла. Кроме того, команда mv может перемещать файл из одного каталога в другой. Установление имени файла. Системный вызов creat устанавливает имя и начальный режим для файла. Изменение имени файла. Системный вызов rename изменяет имя файла. При обращении к вызову задаются два аргумента, старое и новое имя:
rename НАЗНАЧЕНИЕ
Изменение имени и/или перемещение файла
INCLUDE
#include < stdio.h >
ИСПОЛЬЗОВАНИЕ
int result = renamefchar *okJ, char *new)
АРГУМЕНТЫ
old - старое имя файла или каталога new - новое маршрутное имя для файла или каталога
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Заключение Основные идеи •
•
•
•
На диске находятся каталоги и файлы. Файлы имеют содержимое и свойства. В файле могут содержаться данные некоторого типа. Каталог может содержать только список имен файлов. Имена в каталогах соотнесены файлам и другим каталогам. В ядре есть системные вызовы для чтения содержимого каталогов, для чтения свойств (атрибутов) файлов и для модификации большинства атрибутов файлов. Тип файла, права доступа и три специальных атрибута хранятся как двоичный код некоторого целого числа. Для выборки определенных разрядов используется техника поразрядного маскирования. Собственник и группа файла хранятся в числовом представлении. Соответствие меж ду этими числами и символьными именами собственника и группы устанавливается через базы данных passwd и group.
128
Свойства каталогов и файлов при просмотре с помощью команды Is
Что дальше? Каталог - это список файлов и каталогов. Каталоги связаны между собой в древовидную структуру. Как происходит работа с этим деревом? Как связываются между собой катало ги? В следующей главе мы рассмотрим внутреннюю структуру дерева.
Визуальнй итог
Исследования 3.1 Длина d_name[ ]. В определении структуры
struct dirent длина символьного массива d_name[] была указана равной 1 для некоторых систем и может быть 255 символов для других систем. Какова будет фактическая длина? Почему приведены такие стран ные числа? Почему не использована нотация вида: char *?
3.2 Защита файлов от самого себя. Режим доступа вида:---------- rwx, который вы можете установить с помощью команды chmod 007 filename, вполне допустим, но явно весь ма эксцентричен. Он гарантирует все остальным пользователям право на чтение, запись и исполнение в отношении файла filename, но не предоставляет таких прав ни собственнику, ни группе. Установите такой режим для некоторого файла. Смогли ли вы прочитать такой файл? Поэкспериментируйте с различными комбинациями, чтобы представить себе логику ядра, которая используется при определении прав доступа при работе системного вызова open. Если у вас есть возможность доступа к исходным
Заключение
129
кодам ядра, найдите ту часть кода, которая отвечает за права доступа, и проверьте правильны ли ваши догадки относительно логики работы. 3.3 Идентификаторы и пользовательские имена. Каждый пользователь имеет пользова тельское имя и каждому имени сопоставлен пользовательский числовой идентифика тор. Возможен ли случай, когда двум различным пользовательским именам сопостав лен один и тот же числовой идентификатор пользователя? Возможен ли случай, когда одному и тому же пользовательскому имени сопоставлены два различных числовых идентификатора пользователя? Если у вас есть права доступа root на машине, то по пробуйте поэкспериментировать с различными комбинациями. Заведите учетные записи на двух различных пользователей, для которых укажите один и тот же UID, но для каждого пользователя должны быть установлены свое пользовательское имя и свой пароль. Могут ли каждый из пользователей модифицировать файлы у другого? Что должна показать при выводе команда who? Что покажет команда Is -I? Что пока жет команда id? А как быть с электронной почтой? Что дал вам эксперимент для по нимания того, как работают эти команды? Можете ли придумать ситуации, где было бы полезным использовать множество пользовательских имен при одном и том же пользовательском идентификаторе? 3-4 Специальные разряды и каталоги. Каталоги, как и все файлы в Unix, имеют полный набор разрядов для определения прав доступа, включая разряды set-user-ID, setgroup-ID. Возникает вот какая задача. Если вы установите в отношении каталога разряд set-group-ID, то повлияет ли он на работу с каталогом? Если да, то как и поче му? Если нет, то подумайте, как можно будет использовать этот разряд? 3.5 Исполнимый код и права на исполнение. У каждого файла есть три разряда для уста новки права на исполнения для собственника, группы и для всех остальных пользова телей. Вы можете установить право на исполнение для любого файла, для обыкновен ных текстовых файлов, даже для тех, которые содержат перечень товаров в продо вольственном магазине. Но, с другой стороны, может быть файл, который содержит исполнимый код, напри мер, a.out, и который получен после компиляции С-программы. Для этого файла может быть сброшен разряд на исполнение. Объясните разницу между идеями испол нимого кода и правами на исполнение для файла. Связаны ли эти идеи между собой? Почитайте документацию по команде file. 3.6 Входные имена и пользовательские ID. Каждый пользователь имеет символьное поль зовательское имя и числовой идентификатор — UID. Зачем? Не было бы более про стым указывать в качестве владельца файла символьное имя пользователя? Почему бы для каждого пользователя не завести только один числовой идентификатор? В чем проблемы с двумя системами идентификации? Какие преимущества предоставляют две системы идентификации? Если бы вы проектировали собственную операционную систему, то как бы вы поступили в отношении этого вопроса? 3.7 В документации на dirent, текст которой был приведен, была сделана ссылка на систем ный вызов getdents(2). Что делает этот вызов и какое отношение он имеет к readdir? 3.8 Обычно в листинге команды Is -I каталоги представлены с такими правами доступа: drwxr-xr-x.
130
Свойства каталогов и файлов при просмотре с помощью команды Is При посимвольном разборе этого поля слева направо обнаруживаем: тип файла каталог, собственник каталога может выполнять операции чтения, записи и исполне ния в отношении каталога, члены группы и все остальные пользователи могут выпол нять только операции чтения и исполнения в отношении каталога. Что означает право на “исполнение” в отношении каталога? Файл может содержать код, составленный из команд конкретной машины, или код, составленный на “скрип товом” языке. Имеет смысл маркировать такой файл как “исполняемый”, поскольку компьютер может непосредственно исполнять программу на машинном языке или за пустить интерпретатор для исполнения скрипта. Каталог же - это просто список имен файлов, и он не может быть запущен на исполнение. Так что же означает разряд, дающий право на исполнение каталога? Почему он полезен? Поэкспериментируйте с командной chmod и выключите разряд на исполнение для каталога и посмотрите, что получится в данной ситуации.
3.9 Работа с вашим терминалом. Пользователи связываются с системой с помощью терми налов или терминальных эмуляционных программ. Каждый терминал представлен как файл в каталоге /dev. Выполните следующие команды: Is -I /dev/tty* j more. В результате получим вывод списка всех терминальных устройств и их свойств. Фай лы, которые представляют терминалы, как и обычные файлы, имеют собственника. Собственником терминала будет тот пользователь, который вошел в систему через этот терминал. Терминальные устройства не используют root в качестве собственни ка. Собственник терминала изменяется программой login. Обратитесь к исходному * коду программы login и отыщите в ней код, где происходит изменение собственника. Что изменяет программа, когда собственность возвращается опять в root, когда вы будете выходить из системы?
Программные упражнения 3.10 Добавьте возможность многоколонного вывода в версии программы Isl .с. Поэкспери ментируйте со стандартной версией команды Is с тем, чтобы посмотреть, что она де лает. Вы можете увидеть, что она выводит поколонно в зависимости от длины текуще го списка имен файлов. Колонки по возможности строятся в выводе равной длины. Наконец, ширина для отображения при выводе. Как команда Is конструирует такой вывод? 3.11 Модификация ls2. Следует модифицировать ls2.c так, чтобы она правильно работала, когда имя каталога задается как аргумент при обращении к программе. 3.12 Модифицируйте программу ls2.c так, чтобы ею можно было корректно управлять с помощью разрядов suid, sgid, sticky bit. Почитайте документацию, чтобы убедиться, что вы учли все возможные комбинации. 3.13 Стандартная утилита ср допускает использование имени каталога в качестве второго аргумента. В таком случае файл будет копироваться в этот каталог и получать имя оригинала. То есть, команда:
$ ср filel /tmp
Заключение будет работать так же, как и команда: $ср filel /tmp/file1 Модифицируйте программу cpl .с в главе 2, чтобы реализовать такой вариант обраще ния. 3.14 Иногда вам необходимо скопировать в некоторый каталог сразу все файлы. Просто вам требуется сделать исброс” (backup) всех файлов. Модифицируйте программу cpl .с в главе 2 так, чтобы при задании двух имен каталогов в качестве аргументов про грамма копировала бы все файлы из первого каталога во второй, присваивая каждой копии имя оригинала. 3.15 Модифицируйте программу Isl.c так, чтобы она производила бы сортировку списка имен файлов. В стандартной версии команды Is поддерживается опция -г, с помощью которой можно выдавать список в обратном порядке. Добавьте возможность отработ ки этой опции. В некоторых версиях команды Is поддерживается опция a -q для “quick” (быстрого) вывода. При использовании такой опции команда Is не производит сортировки списка имен файлов. Эта опция полезная, когда каталог содержит слишком большое число файлов. Их так много, что даже при быстрой сортировке на вывод тратится значитель ное время. 3.16 Нарисуйте строку, содержащую 16 знакомест, и заполните требуемые знакоместа еди ницами и нулями с тем, чтобы они соответствовали такому праву доступа к каталогу: rwxr-x—х. 3.17 Блокировка сарая. Если вы выключили право чтения на файл, то вы не сможете открыть файл на чтение. Что будет, если вы открыли файл на чтение, а затем с другого терминала сбросили право на чтение для этого файла? Выполнится ли успешно сле дующий системный вызов read? Напишите программу, которая открывает файл на чтение, далее читает из него несколько байтов, затем выполняет системный вызов sleep(20) и находится в состоянии ожидания 20 секунд, а далее опять пытается прочи тать из файла. В течение этих 20 секунд сбросьте для файла право на чтение. Что про изойдет далее? Объясните истинный смысл прав доступа на чтение. 3.18 Рекурсия для Is. Стандартный вариант команды Is поддерживает опцию -R. Эта опция позволяет получить содержимое каталога и содержимое всех подкаталогов под ним. Попытайтесь выполнить такую команду. Модифицируйте вариант программы ls2.c, чтобы работала опция -R. 3.19 Вывод времени последнего доступа. В стандартном варианте команды Is поддержива ется опция -и, с помощью которой можно отображать в длинном листинге время последнего доступа вместо времени последней модификации. Что произойдет, если будет использована опция -и без использования опции -1? Намек: почитайте докумен тацию относительно опции -1. 3.20 Напищите простую версию программы chown, которая при обращении должна вос принимать из командной строки пользовательское имя или пользовательский ID и произвольное имя файла. Как вы будете преобразовывать пользовательское имя в пользовательский идентификатор? Что будет, если не будет обнаружен пользова тель с указанным пользовательским именем? Замечание. Чтобы проверить работу ва шей программы, вам понадобится стать суперпользователем.
132
Свойства каталогов и файлов при просмотре с помощью команды Is
3.21 Время последнего доступа к файлу - это полезная информация. Получение backup для файлов - это хорошая идея. Но когда происходит восстановление (чтение) файлов из backup, то изменяется время последнего доступа для каждого восстановленного фай ла. Было бы очень хорошо, если бы программа для восстановления из backup при вос становлении файла оставляла для него неизменяемым время последнего доступа. Кроме того, не менее приятно было бы, если бы копия файла имела такие же времена последней модификации и последнего доступа, как у оригинала. Напишите версию программы ср, которая выполняла эти два действия. 3.22 Ваше терминальное устройство - это файл, который программа использует, чтобы получать из него данные для вас и посылать от вас данные на него. Когда программа читает с терминального устройства, то она получает данные с вашей клавиатуры. Когда программа выдает данные на терминальное устройство, то они передаются на экран. Для установления времени последней модификации в терминальном файле ис пользуется stjritime. Напишите программу с именем lastdata, которая выводит список всех текущих пользователей и отображает для каждого пользователя время, когда бы ла последняя модификация терминала. Используйте тот же формат, как в команде who.
Проекты Основываясь на материале этой главы, вы можете изучить справочный материал и напи сать версии следующих Unix программ: chmod, file, chown, chgrp, finger, touch .
Глава 4 Изучение файловых систем. Разработка версии pwd
Цели Идеи и средства • • • • • •
Пользовательское восприятие дерева файловой системы в Unix. Внутренняя структура файловой системы Unix: структуры inodes и блоки данных. Как связаны между собой каталоги. Твердые ссылки, символические ссылки: идеи и системные вызовы. Как работает команда pwd. Монтирование файловых систем.
Системные вызовы и функции •
mkdir, rmdir, chdir
•
link, unlink, rename, symlink
Команды •
pwd
4.1. Введение Файлы содержат данные. В каталогах содержится список имен файлов. Каталоги органи зованы в древовидную структуру. В них могут содержаться имена других каталогов. Что для файла значит “быть в каталоге”? Когда вы сходите в Unix-машину, то вы попадае те в ваш “домашний каталог”. Что означает для пользователя выражение “находиться в каталоге”? Древовидная структура является иллюзией. Твердый диск-это набор металлических пла стин, на каждой из которых нанесено магнитное покрытие. Как сделать, чтобы этот набор связанных металлических пластин представлялся бы нам как дерево файлов, свойств и ка талогов?
134
Изучение файловых систем. Разработка версии pwd
Чтобы найти ответ на вопрос, напишем версию команды pwd. Эта команда сообщает о вашем текущем расположении в дереве каталогов. Последовательность каталогов и под каталогов от корня дерева до места вашего расположения называют путем к вашему рабочему каталогу. Чтобы написать версию pwd, нам следует понять, как организованы и хранятся файлы и каталоги. Мы будем изучать файловую систему, начиная с рассмотре ния ее с позиций пользователя. Далее рассмотрим ее внутреннюю структуру. И наконец, изучим системные вызовы и способы их использования.
4.2. Пользовательский взгляд на файловую систему 4.2.1. Ка талоги и файлы Пользователи воспринимают диск в Unix как дерево каталогов. В каждом каталоге могут содержаться файлы и другие каталоги. На рисунке 4.1 представлена схема небольшой час ти дерева.
Начнем со строительства этой древовидной структуры и по мере строительства введем в использование ряд Unix-команд для управления такими деревьями файлов и каталогов.
4.2.2. Команды для работы с каталогами Дерево можно создать с помощью последовательности таких команд: $ mkdir demodir $ cd demodir $ pwd /home/yourname/experiments/demodir $mkdir boops $ mv b с $ rmdir oops
$cdc $ mkdir d1 d2 $ cd../.. $ mkdir demodir/a
Мы будем использовать много других команд. Та последовательность команд, что была только что приведена, преднамеренно усложнена и включает в себя несколько методов.
4.2. Пользовательский взгляд на файловую систему
135
Прочтите текст этого примера и нарисуйте дерево таким, как оно вам представляется. Пример использует несколько базовых команд. Команда mkdir создает новый каталог или каталоги с заданными именами. Что произойдет, если вы попытаетесь создать каталог и укажете при этом имя, которое уже присвоено файлу или каталогу? Команда rmdir удаля ет каталог или каталоги. Что произойдет, если вы попытаетесь удалить каталог, в котором содержатся подкаталоги? Команда mv переименовывает каталог. С ее помощью можно также перемещать каталог с одного места в другое. Команда cd несколько отличается. Она ничего не делает с каталогом. Эта команда влияет на вас, на пользователя. Команда cd перемещает вас из одного каталога в другой, как если бы вы переходили из одной комнаты в другую. Команда pwd выводит путь к текущему каталогу. В примере мы начали работу в подкаталоге с именем demodir. Это подкаталог для каталога experiments, который в свою очередь находится в каталоге yourname. Каталог yourname расположен под каталогом home, а он в свою очередь находится под корневым катало гом, который обозначается символом слеша (/).
4.2.3. Команды для работы с файлами Теперь мы создадим несколько файлов в дереве каталогов:
$ cd demodir $ ср /etc/group х $catx root::0:
bin:: 1:bin,daemon users: :200:
$ cp x copy.of.x $ mv copy.of.x у $ mvxa $cdc $ cp../a/x d2/xcopy $ln../a/xd1/xlink $ Is > d 1 /xlink $cpd1/xlinkz $ rm../../demodir/c/d2/../z $cd../.. $ cat demodir/a/x (что здесь произойдет?)
Эта последовательность команд для работы с файлами также намеренно усложнена, чтобы показать различные команды и проиллюстрировать их действие. Пройдите в пошаговом режиме по этой последовательности команд и нарисуйте файлы по мере их возникновения. Предскажите, каков будет результат выполнения последней команды в данной последова тельности. В этом примере использована большая часть команд для управления файлами. Команда ср выполняет копирование файла. Мы разработали версию команды ср в предшест вующей главе. Команда cat копирует содержимое файла на стандартный вывод. Команда mv переименовывает файл, как показано в первом примере, и перемещает файл в другой каталог, как показано во втором примере. Команда rm удаляет файл. Заметьте, что в пути может быть использована нотация вида Такая нотация обозначает каталог, который расположен на
136
Изучение файловых систем. Разработка версии pwd
уровень выше. Его называют родительским каталогом. Последовательность имен каталогов, с символами / в качестве разделителей в последовательности, определяет путь, который ведет к именованному объекту. В частности, заметьте, что используется окольный путь для удале ния файла с именем z.
Копирование, проверка наличия, переименование - это все стандартные операции, которые имеются на многих компьютерных системах. Команда In носит не столь общий характер, а является неотъемлемой частью Unix. В данном примере мы взяли сущест вующий файл../а/х и сделали на него ссылку. Ссылка была названа dl/xlink. Обратитесь к рисунку 4.2 и найдите эти два элемент а. Элемент, который называется х, находится в каталоге demodir/a, а элемент с именем xlink находится в каталоге demodir/c/dl. Как х, так и xlink называют ссылками. Ссылка - это указатель на файл. Как../а/х, так и dl/xlink указы вают на одни и те же данные на диске. В соответствии с нашим примером следующая команда Is > dl/xlink заменяет содержимое xlink выводом от команды Is. Что получится, если с помощью cat вывести содержимое файла../а/х?
4.2.4. Команды для работы с деревом Несколько команд в Unix предназначены для работы с древовидными структурами. Вот несколько примеров: ,S ' R
Команда Is может выводить в списочном формате содержимое всего дерева. С помощью опции -R задается такой режим отображения, когда будет выводиться содержимое указан ного каталога и всех подкаталогов под ним. В предшествующей главе мы разработали версию команды Is. Что еще необходимо сделать в нашей версии, чтобы добавить эти рекурсивные возможности. chmod -R
Команда chmod изменяет права доступа у файлов. С помощью опции -R в этой команде можно производить изменения прав доступа у всех файлов в подкаталогах. du
Команда du (имя является сокращением от disk usage - использование диска) сообщает о числе дисковых блоков, которые используются каталогом, указанным в команде, и всеми входящими в него файлами и подкаталогами с их файлами - т. е. рекурсивный спуск до листьев данного поддерева
4.3. Внутренняя структура файловой системы UNIX
137
find
Команда find ищет в каталоге и во всех вложенных подкаталогах файлы и каталоги, которые удовлетворяют критерию поиска, заданному в команде. Например, вы можете искать файлы с размером большим одного мегабайта, которые не были модифицированы в течение последней недели и которые доступны на чтение всем пользователям. Бще немного о командах Поддеревья каталогов составляют существенную часть файловой системы. В Unix имеет ся много команд для работы с деревьями. Вы многое можете обнаружить в этой области.
4.2.5. Практически нет пределов на древовидную структуру В каталогах может находиться большое количество файлов и большое число подкатало гов. Внутренняя структура системы не накладывает ограничений на глубину дерева ката логов. Можно создавать каталоги с такой глубиной, чтобы они удовлетворяли возможно стям большинства команд для работы с деревьями. Предупреждение. Если вы пожелаете, то проведите такой эксперимент на вашей собст венной машине. Системный администратор в вашей школе или на вашей работе вряд ли обрадуется, если вы попытаетесь провести эксперимент на его машине. Простой shell-скрипт (Этот термин все более часто используют при переводе литературы по Unix. Хотя вполне допустимо использование эквивалентного термина - shell-npoueдура. - Примеч. пер.) (см. главу 8). while true do . mkdir deep-well cd deep-well done
создаст связанный список каталогов очень большой глубины, даже если вы нажмете на Ctrl-C сразу через одну или две секунды после запуска скрипта на исполнение. Что пока жет утилита du при обработке этого “туннеля”? А как будут вести себя команды find и Is -R? Во многих версиях Unix команда rm -г deep-well не работает. Как можно удалить такие структуры с большой глубиной?
4.2.6. Итоговые замечания по файловой системе Unix В этом разделе мы рассмотрели файловую систему Unix с позиций пользователя. С этих позиций диск представляется в виде дерева каталогов, которое может расширяться как вглубь, так и вширь. В Unix имеется много программ для работы с объектами такой струк туры. Все файлы в систеце Unix располагаются в составе этой структуры. Как это все работает? Что такое каталог? Как узнать, в каком каталоге мы находимся? Что означает для вас, для человека, смена одного каталога на другой? Как команда pwd опреде ляет, где вы находитесь в дереве?
4.3. Внутренняя структура файловой системы UNIX
* Диск представляет собой набор металлических пластин. Несколько уровней абстракции преобразуют наше представление физического диска в файловую систему, которую мы рассматривали в предшествующем разделе.
Изучение файловых систем. Разработка версии pwd
138
4.3.1. Абстракция О: От пластин к разделам Диск может хранить набор данных. Как страна разделяется на штаты или округа, так и диск может быть разделен на разделы для создания отдельных регионов с определенной однородностью содержания. Мы будем рассматривать каждый раздел как отдельный диск.
4.3.2. Абстракция 1: От плат к массиву блоков Диск - это набор магнитных плат. Поверхность на каждой магнитной плате представляет собой структуру из концентрических окружностей, которые называют треками. Каждый из таких треков разделен на секторы, как пригородная улица разделяется на жилые масси вы. Каждый их этих секторов хранит некоторое число байтов данных, например 512. Сектор - это основная единица хранения данных на диске. На современных дисках ко личество секторов очень большое. На рисунке 4.3 представлена схема нумерации для дис ковых блоков.
А теперь рассмотрим важную идею: нумерацию дисковых блоков. Присвоение номеров дисковым блокам в последовательном порядке дает возможность системе вести учет каж дого блока на диске. Можно проводить нумерацию блоков в нисходящем порядке, от пла ты к плате, а можно нумеровать вовнутрь каждой платы - нумеровать блоки от трека к треку на плате. Подобно почтальону, который устанавливает соответствие каждому письму дома и улицы, должно быть программное обеспечение, которое управляет хране нием данных на диске по адресам, которые назначаются отдельным блокам на некотором треке на диске. Система нумерации на диске по схеме разбиения на секторы дает вам возможность рас сматривать диск как массив из блоков.
4.3.3. Абстракция 2: От массива блоков к дереву разделов \
Файловая система хранит файлы. Более точно, файловая система хранит содержимое фай лов, атрибуты файла (собственник, временные отметки и т. д.) и каталоги, в которых нахо дятся такие файлы. А где находятся в однородной последовательности блоков содержимое файла, атрибуты файла и каталоги? В Unix используется простой метод. Массив блоков разделяется на три секции, как пока зано на рисунке 4.4.
4.3. Внутренняя структура файловой системы UNIX
139
Рисунок 4.4 Три области файловой системы ♦
В одной из секций, которая называется областью данных, находятся данные файлов, яв ляющиеся их содержимым. В другой секции, которая называется таблицей inode, содержат ся свойства (атрибуты) файлов. И в третьей секции, которая называется суперблоком, нахо дится информация о самой файловой системе. Трехэлементная структура, которая распро страняется на пронумерованную структуру блоков, и называется файловой системой. Суперблок. Первый блок в составе файловой системы называется суперблоком. В этом блоке содержится информация об организации самой файловой системы. Например, в суперблок записываются размеры каждой из областей файловой системы. В суперблоке также находится информация о неиспользуемых блоках данных. Конкретное содержание и структура суперблока зависит от версии Unix. Обратитесь к электронному справочнику и к заголовочным файлам, чтобы узнать, что содержится в вашем суперблоке. Таблица inode. Следующая часть файловой системы называется таблица inode. Каждый файл имеет набор свойств, таких, как размер, пользовательский идентификатор собственни ка, время последней модификации. Эти свойства записываются в структуру, которая называ ется inode. Все эти структуры имеют один и тот же размер, а таблица inode представляет со бой просто массив из таких структур. Каждый файл, который находится в файловой системе, имеет один inode в этой таблице. Если вы обладаете правами суперпользователя, то можете открыть раздел как файл, почитать и распечатать содержимое таблицы inode. Это анало гично варианту обращения к файлу utmp для чтения и вывода его содержимого. Важно следующее: Каждый inode идентифицируется в системе своей позицией в таблице inode. Например, inode 2 будет третьей структурой в таблице inode файловой системы. Область данных. Третьей частью файловой системы является область данных. Здесь хранит ся содержимое файла. Все блоки на диске имеют один и тот же размер. Если, в файле содержатся данные, для хранения которых необходимо более одного блока, то содержймое такого файла будет располагаться в нескольких блоках. Количество блоков определяется размером файла. Большие файлы могут состоять из тысяч отдельных дисковых блоков. Каким образом система учитывает наличие цепочек из таких отдельных блоков?
4.3.4. Файловая система с практических позиций: Создание файла Идея поддержки одной области для хранения содержимого файла и другой области для хранения файловых характеристик выглядит достаточно простой, но как это будет рабо тать на практике? Что происходит, когда вы создаете новый файл? Рассмотрим простую команду: $ who > userlist
Когда команда будет выполнена, то, как результат, в файловой системе появится новый файл, в котором будет находиться вывод команды who. Как это все происходит? У файла есть содержимое, у файла также есть свойства. Ядро поместит содержимое файла в область дан
140
Изучение файловых систем. Разработка версии pwd
ных, свойства файла помещаются в структуру inode, а имя файла помещается в каталог. На рисунке 4.5 показан пример создания файла, для которого необходимо три блока дисковой памяти.
Рисунок 4.5 Внутренняя структура файла При создании нового файла выполняются следующие четыре основных действия: Сохранение свойств Файл имеет свойства. Ядро находит свободную структуру inode. В нашем примере ядро нашло inode под номером 47. Далее ядро записывает информацию о файле в этот inode. Сохранение данных Файл имеет содержимое. Для нашего нового файла требуется три дисковых блока памяти. Ядро отыскивает в списке свободных блоков три блока. В данном случае были найдены блоки 627, 200 и 992. Далее первая часть данных копируется из буфера ядра в блок 627. Следующая часть копируется в блок 200. И наконец, последняя часть данных копируется в блок 992. р
Сохранение информации о распределении блоков Содержимое файла было распределено по блокам 627, 200 и 992, именно в указанном порядке. Ядро записывает эту последовательность номеров блоков в структуру inode, в секцию для хранения информации о распределении. Эта секция представляет собой мас сив из номеров блоков. Найденные три номера дисковых блоков будут помещены в первые три элемента массива. Добавление имени файла в каталог Наш новый файл называется userlist. Как в Unix ведется учет того, что новый файл появился в текущем каталоге? Решение простое. Ядро добавляет к каталогу запись вида: (47, userlist). Эта запись, устанавливающая взаимосвязь между именем файла и номером inode, пред ставляет собой связь между содержимым файла с указанным именем и свойствами этого файла. Это обстоятельство заслуживает отдельного рассмотрения.
4.3. Внутренняя структура файловой системы UNIX
141
4.3.5. Файловая система с практических позиций: Как работают каталоги Каталог - это специальный тип файла, который предназначен для хранения списка имен фай лов. Внутренняя структура каталога может быть разной в разных версиях Unix, но абстракт ная модель остается всегда одной и той же - это таблица номеров inode и имен файлов:
Номер inode #
Имя файла
2342 43989 3421
hello.c
533870
myls.c
Представление внутреннего содержимого каталога Вы можете посмотреть содержимое каталога с помощью команды Is -lia (первая опция это цифра 1):
$ Is -1 ia demodir 177865. 529193.. 588277 а 200520 с 204491 у $ В этом выводе представлен список из имен файлов и соответствующих им номеров inode. Например, файлу с именем у соответствует номер inode 204491. Текущий каталог, который обозначается символом имеет номер inode 177865. Это означает, что информация о размере, собственнике, группе и т, д., расположена в таблице inode, в структуре с номером 177865. Опции -i и -1 (единица, а не 1) для команды Is могут быть для вас новыми. При использо вании опции -i команда Is будет включать в вывод номер inode, при указании опции -1 команда будет производить одноколонный вывод. Постройте на вашей машине собствен ную версию поддерева demodir, а потом посмотрите номера inode в поддереве. Множественность ссылок на один и тот же файл Вы можете использовать команду ls-i, чтобы получить номера inode для файлов в системе. Например, вы можете посмотреть номер inode для всех элементов в корневом каталоге вашей системы: $ Is -ia /
2
28673 etc
11 lost+found
43829 shlib
2 2 .. 3 auto 26625 bin 403457 boot 225281 dev
311297 home 8832 home2 24646 initrd 24579 install 161797 lib
4097 mnt 108545 opt 1 proc 24681 root 233473 sbin
40961 tmp 18433 usr 10241 var 183 xfer.log 183 transfers
142
Изучение файловых систем. Разработка версии pwd
В этом листинге следует обратить внимание на две вещи. Во-первых, в конце самой правой колонки представлены два файла с именами xfer.log и transfers. У этих файлов один и тот же номер inode, равный 183. Следовательно, оба из этих имен файла ссылаются на один и тот же inode. Структура inode представляет отдельный файл. Эта структура содержит свойства файла и список блоков данных для файла. Поэтому xfer.log и transfers - это два имени одного и того же файла. Короче говоря, это напоминает ситуацию, когда в телефонной книге один и тот же телефонный номер представлен в двух различных разделах. Но в обоих случаях - это ссылки на один телефонный номер1. Другая важная вещь, которую следует подчеркнуть в листинге содержимого корневого ката лога - это изображение точки и двух точек в начале колонки слева. В этих записях указан один и тот же номер inode, равный 2, что говорит о том, что имена и ссылаются на один и тот же каталог. Как может текущий каталог одновременно быть и родительским ката логом?2 Когда с помощью команды mkfs создается файловая система, то команда mkfs в каче стве родительского каталога для корневого каталога указывает сам корневой каталог.
4.3.6.Файловаясистемаспрактическихпозиций:Какработаеткомандаcat Мы рассмотрели, что делается внутри системы, когда вы пишете в новый файл, как это было в примере с командой who > userlist. А что будет происходить, когда вы будете пы таться прочитать что-то из файла. Например, как будет работать такая команда: $ cat userlist Проследуем через указатели от каталога к данным.
Поиск в каталоге указанного имени файла Имена файлов хранятся в каталогах. Ядро ищет в каталоге запись, в которой содержится поле со значением userlist.
Локализация и чтение inode 47 Ядро находит inode 47 в области, где расположена таблица inode в файловой системе. Для обнаружения inode требуется провести простой расчет. Все структуры inode имеют один и тот же размер, и в каждом дисковом блоке содержится фиксированное число таких структур. Структура inode может уже быть в буфере в ядре. В inode находится список бло ков данных. 1. Пример с телефонным справочником не совсем точен, поскольку два различных человека могут проживать в одном и том же доме. См. концепцию порта в главе, посвященной вопросам сетевого программирования.
1. Послушайте Гт My Own Grandpa, 1947, by Latham & Jaffe для продолжения дискуссии по данной теме.
4.3. Внутренняя структура файловой системы UNIX
143
Обращение к блокам данных в последовательном порядке, один за другим Ядро теперь знает, в каких блоках данных находятся данные, в каком порядке было рас пределение данных по этим блокам. По мере того как команда cat многократно вызывает read, ядро обращается пошагово в заданном порядке к блокам данных, копируя данные с диска в буферы ядра и оттуда в массив пользовательского пространства. Все команд^, которые читают из файла, такие, как cat, ср, more, who и тысячи других команд, передают имя файла системному вызову open для получения доступа к содержи мому файла. При каждом вызове open отыскивает в каталоге имя файла, затем использует номер inode в каталоге, чтобы получить доступ к атрибутам файла и локализовать место нахождения содержимого файла. Рассмотрим теперь ситуацию, когда вы пытаетесь выполнить open в отношении файла, к которому у вас нет прав на запись и чтение. Ядро использует имя файла, чтобы по нему найти номер inode, затем использует этот номер inode для локализации структуры inode. В этой структуре ядро находит разряды, определяющие права доступа к файлу, и пользо вательский ID собственника файла. Если ваш UID и UID для файла совпадают, а необхо димые разряды доступа не установлены, то системный вызов заканчивается с кодом воз врата -1 и в переменной errno устанавливается значение EPERM. На основе рассмотренного представления схемы из каталогов, таблицы inode, блоков дан ных вы теперь должны прочувствовать смысл работы других файловых операций. Обра тившись к исходному коду на какой-либо версии Unix, вы можете посмотреть, как работа ет системный вызов close.
4.3.7. Inodes и большие файлы Каким образом файловая система Unix учитывает распределение по блокам для больших файлов? Те пояснения, которые были представлены в предшествующем разделе, недоста точны. Кратко проблему можно представить так: Фактор 1 Для большого файла необходимо много дисковых блоков Фактор 2 В структуре inode хранится список распределения дисковых блоков Проблема Как можно сохранять в inode, который имеет фиксированный размер, длинный список распределения блоков?
Решение Сохранять большую часть списка распределения в области блоков данных, а в inode установить указатели на эти блоки
Рассмотрим ситуацию, изображенную на рисунке 4.7. Для файла необходимо выделить четырнадцать блоков для хранения его содержимого. Поэтому в списке распределения бу дет расположено четырнадцать номеров блоков. Печально, но факт: inode файла имеет область для хранения массива распределения блоков, в которую можно записать только тринадцать элементов массива. Звучит зловещая музыка! Как поместить список из 14 эле ментов в область, рассчитанную на хранение только 13 элементов? Да легко. Поместим первые 10 номеров из списка распределения в область распределения в inode. Далее по местим оставшиеся 4 номера дисковых блоков из списка распределения в какой-либо блок данных. Это — своего рода проведение инвентаризации на полке и перемещение всего лишнего с полки на склад. А теперь поговорим о деталях. В inode находится массив, в который можно записать 13 но меров блоков. Первые 10 элементов массива подобны “пространству на полке”. Здесь хранят ся в 10 элементах номера тех блоков, которые действительно содержат данные файла. Если реальный список распределения номеров содержит более 10 элементов, то заводится допол нительный блок для хранения номеров блоков. Но располагается дополнительная область не
144
Изучение файловых систем. Разработка версии [mi
дополнительные номера, записывают в 11 элемент массива в inode. Это аналогично ситуа ции, когда в книжном магазине кладут на полку такую пометку: “Дополнительные книги на складе, полка 3”.
Рисунок 4.7 Список распределения блоков содержится в области данных Отметим то обстоятельство, что для рассматриваемого файла требуется 15 блоков данных. В четырнадцати блоках будет храниться содержимое файла, а еще в одном блоке будет на ходиться та часть списка распределения, которая не поместилась в inode. Этот дополни тельный блок называют косвенным блоком. Что происходит, когда будет заполнен косвенный блок? По мере того как при работе будут добавляться новые данные, ядро будет присоединять к файлу дополнительные бло ки данных. Поэтому список распределения становится все длиннее и длиннее. И для него может потребоваться дополнительная память. Рано или поздно список распределения заполнит весь косвенный блок. Поэтому ядро начинает работать со вторым дополнитель ным блоком. Что делает ядро с номером второго дополнительного блока? Должно ли ядро поместить номер второго дополнительного блока в 12-й элемент массива в inode? Конечно, это возможно, но тогда это будет означать, что файл будет иметь три дополни тельных блока. Вместо того чтобы поместить номер второго блока в массив в inode, ядро обращается еще к одному блоку данных, в котором будет храниться список таких допол нительных косвенных блоков. А в элемент 12 в массиве в inode будет помещен номер бло ка,, но не второго дополнительного блока, а номер блока, в котором будут храниться номера второго, третьего, четвертого и т. д. дополнительных блоков. Такой блок называют двойной косвенный блок.
4.4, Понимание каталогов
145
Что происходит, когда будет заполнен двойной косвенный блок? Когда будет заполнен двойной косвенный блок, ядро начинает работать с новым двойным косвенным блоком. Ядро не помещает номер этого нового косвенного блока в область inode. Вместо этого ядро создает тройной косвенный блок, где будут размещаться номера нового двойного косвенного блока и всех последующих двойных косвенных блоков, которые понадобятся файлу. Номер этого тройного косвенного блока запоминается в последнем элементе в мас сиве в области inode. Что происходите, когда будет заполнен тройной косвенный блок? В данной ситуации предполагается, что файл достиг по размеру своего предела. Если вам требуется исполь зовать файлы большого размера, то можно установить файловую систему с большими раз мерами дисковых блоков. Когда вы создаете такую файловую систему, то вы можете опре делять не только размер таблицы inode и области данных, но вы можете задавать и размер дискового блока. Размер дискового блока не обязательно должен совпадать с размером сектора на поверхности диска. Часто в одном дисковом блоке содержится несколько дис ковых секторов. Большие файлы требуют больших системных затрат. Система распределения дисковой памяти является быстрой и эффективной для маленьких файлов. По мере роста размера файла ядро использует все больше и больше дискового пространства для хранения списка распределения. При поиске конкретного элемента в файле может потребоваться обраще ние к нескольким косвенным блокам, чтобы получить номер нужного блока данных.
4.3.8. Варианты файловых систем в Unix В предшествующем разделе было дано описание структуры файловой системы Unix. В различных версиях Unix используются различные версии этой модели. Из-за простоты этого классического метода возникает ряд важных слабых мест. Например, уязвимым местом в системе является суперблок. Если блок будет разрушен каким-то образом, то будет потеряна информация обо всей файловой системе. В некоторых версиях Unix сохраняют копии суперблока в самой файловой системе. Другой проблемой является фрагментация» По мере создания и удаления файлов свобод ные блоки распределяются в произвольном порядке по диску. Одно из решений - созда вать небольшие файловые системы, которые будут называться группы цилиндров. Классическая модель не устарела. Файлы создаются и удаляются в области данных поблочно. Атрибуты файлов также хранятся в inode в таблице inode, а в составе inode содержится массив распределения блоков диска для файла. Каталоги содержат списки из имен файлов и номеров inode. Мы можем теперь возвратиться к небольшому поддереву, которое мы построили и изучили в начале главы. Обогащенные знанием внутренней структуры файловой системы, мы получим и рассмотрим ‘'рентгеновский снимок” катало гов и файлов.
4.4. Понимание каталогов Теперь, когда мы знаем внутреннюю структуру файловой системы Unix, мы можем рас смотреть, что реально происходит с нашим поддеревом demodir. И при этом мы сможем разобраться, как работают различные команды для обработки каталогов.
4.4.1. Понимание структуры каталога Пользователи воспринимают файловую систему как набор каталогов и подкаталогов. Каждый каталог содержит файлы, в каждом каталоге могут находиться подкаталоги. КажПКТЙ ПППК-ЯТЯТТПГ MMfiftT ППТШТРПкРТСИЙ
тсятяппг Тякпе
ПРПРЙП ИТ
тсятяппгпи и Ляйпгт чягтп
146
Изучение файловых систем. Разработка версии pwd
изображают как набор прямоугольников (боксов), соединенных линиями связи. В каком смысле следует понимать выражение, что файл находится в каталоге? Что означает в тех ническом смысле термин “dl является подкаталогом с”? Что обозначают соединительные линии на таких рисунках? В содержательном смысле каталог - это файл, который содержит список. Список состоит из таких пар: имя файла и номер inode. Более того, пользователи видят список из имен файлов, в то время как Unix видит список поименованных указателей.
Как преобразовать одну диаграмму в другую? Используя номера inode, мы можем точно представить структуру дерева. Используем команду Is -iaR, чтобы получить рекурсивно список номеров inode для всех файлов.
4,4. Понимание каталогов
147
Реальное значение фразы “Файл находится в каталоге” Пользователи в разговоре говорят, что файлы находятся в каталогах, но мы теперь знаем, что файлы представляются записями в таблице inode, а содержание файлов хранится в области данных. В каком смысле следует понимать, что файл находится в каталоге? Например, с пользовательской точки зрения, файл у содержится в каталоге demodir. С системной точки зрения мы видим, что каталог содержит запись с именем файла у и номером inode, равным 491. .
148
Изучение файловых систем. Разработка версии pwd
Аналогично, фраза “файл х находится в каталоге а” означает, что существует ссылка на in ode 402 в каталоге с именем а, и х - это имя файла, которое соответствует этой ссылке. Также заметим, и это важно, что каталог с именем dl внизу, слева на диаграмме, имеет ссылку на inode 402 и что эта ссылка имеет имя xlink. Таким образом, имеются две ссылки на узел 402. Одна из них называется demodir/a/x, а другая - demodir/c/dl/xlink. Обе ссылки обращены к одному и тому же файлу. Короче говоря, каталоги содержат ссылки на файл. Каждая из таких ссылок называется link (связь). Содержимое файла находится в блоках данных, атрибуты файла записаны в структуре в таблице inode, а номер inode и имя файла хранятся в каталоге. Этот же принцип можно использовать для раскрытия смысла выражения “каталог содержит подкаталог”.
Реальное значение фразы “Каталог содержит подкаталоги” С точки зрения пользователя, каталог с именем а является подкаталогом каталога demodir. А как это выглядит изнутри? Опять же, это означает, что каталог demodir имеет ссылку на inode подкаталога. В верхней части диаграммы, рассматриваемой с системных позиций, есть ссылка с именем а, для которой inode имеет номер 277. Как мы узнаем, что 277 - это номер inode для каталога, изображенного слева на диаграмме? Каждый каталог имеет inode. Ядро в каждом каталоге устанавливает запись, которая относится к собственному inode каталога. Эта запись имеет имя В маленьком прямоугольнике слева точка ссылается на inode 277, поскольку каталог в левой части диаграммы имеет inode 277. Посмотрите на диаграмму и убедитесь в том, что каталог, для которого заведен inode 520, содержится в каталоге demodir. В списке имен он представлен под именем с. Аналогично, другой каталог, у которого номер inode равен 247, будет подкаталогом каталога с inode 520, и который имеет имя d2. Реальный смысл фразы “У каталога есть родительский каталог" Посмотрите с пользовательских позиций на диаграмму и найдите каталог d2. У него есть родительский каталог с именем с. Чтобы все это отобразить, опять используется простая ссылка на inode. Для каталога номер inode равен 520. А в каталоге d2 есть запись, в котором используется имя “.Л В этой записи указан номер inode 520. Двумя точками принято обозначать родительский каталог. Таким образом, inode 520 является родитель ским для inode. Заполнение пустых полей для записей, которые имеют в качестве имен точки Если вам стало все понятно при рассмотрении предыдущего раздела, то вы будете в состоянии заполнить пропущенные значения номеров inode на рисунке 4.10. Если вы не уверены, что нужно поместить в эти пустые поля, обратитесь к выводу команды Is, который был приведен ранее, и просмотрите еще раз текст предшествующего раздела. Множественные ссылки, счетчик ссылок В дереве demodir для inode 402 установлены две ссылки. Одна имеет имя х и находится в каталоге а, вторая называется xlink и находится в каталоге dl. Можно ли определить, какое из этих имен - имя оригинального файла, а какое имя - это имя ссылки? В структуре каталога в Unix эти две ссылки имеют одинаковый статус. Их называют твердыми ссыл ками на файл. Файл - это inode и “связка” блоков данных. Ссылка указывает на inode. Вы можете создать много ссылок на файл, если вам это необходимо.
4.4. Понимание каталогов
149
Ядро записывает значения числа ссылок к файлу. В случае для inode 402 это значение будет не меньше 2. К inode могут быть установлены и другие ссылки из других частей файловой системы. Счетчик ссылок хранится в inode. Счетчик ссылок - один из элемен тов структуры struct stat, которая возвращается в результате работы системного вызова stat. Имена файлов В файловой системе Unix файлы не имеют имен. Имена присваиваются ссылкам. А файлам соответствуют номера inodes. Полезность этого факта мы обсудим позже.
4.4.2. Команды и системные вызовы для работы с деревьями каталогов Внутренняя структура файловой системы Unix проста. Это большая структура из совме стно связанных данных. Узлы в данной структуре называют inodes (индексные узлы), наборы указателей называются каталогами, и оконечные узлы называют файлами. Мы имеем возможность управлять таким деревом с помощью стандартных команд Unix, таких, как mkdir, rmdir, mv, In и rm. Как работают эти команды? В частности, какие систем ные вызовы используются при работе этих команд? mkdir
Команда mkdir служит для создания новых каталогов. При обращении к команде можно зада вать одно или более имен каталогов. В команде mkdir используется системный вызов mkdir: mkdir НАЗНАЧЕНИЕ
Создание каталога
INCLUDE
tinclude < sys/stat.h > #include <sys/types.h>
ИСПОЛЬЗОВАНИЕ
int result = mkdir(char ‘pathname, modej mode)
АРГУМЕНТЫ
pathname - имя нового каталога mode - маска для разрядов прав доступа
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Системный вызов mkdir позволяет создавать и устанавливать ссылку на новый узел ката лога в дереве файловой системы. То есть mkdir создает inode для каталога, выделяет дис ковый блок для хранения его содержимого, записывает в каталоге две записи с именами, и.., с необходимыми для них номерами inode. Затем добавляется ссылка на новый узел из родительского каталога. rmdir
Команда rmdir позволяет удалять каталог. При обращении к команде можно задавать одно или более имен каталогов. В команде rmdir используется системный вызов rmdir:
rmdir 1 НАЗНАЧЕНИЕ
Удаление каталога. Каталог должен быть пустым.
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
int result = rmdir(const char *path);
АРГУМЕНТЫ
path - имя каталога
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
150
Изучение файловых систем. Разработка версии pwd
Системный вызов rmdir удаляет узел каталога из дерева каталогов. Каталог должен быть пустым, т. е. в нем не должно содержаться записей о файлах и подкаталогах, кроме запи сей с именем точка и точкаточка. Удаляется ссылка из родительского каталога. Если при этом обнаруживается, что удаляемый каталог не используется другими процессами, то ос вобождаются также его inode и блоки данных. rm
Команда rm позволяет удалять записи'из каталога. При обращении к команде можно зада вать одно или более имен каталогов. В команде rm используется системный вызов unlink:
unlink НАЗНАЧЕНИЕ
Удаление записи в каталоге
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
int result = unlink(const char *path);
АРГУМЕНТЫ
path - имя записи в каталог для удаления
КОДЫ ВОЗВРАТА
- 1 - п р и ошибке 0 - при успехе
Системный вызов unlink удаляет запись в каталоге. В соответствующем inode уменьшается на 1 счетчик ссылок. Если счетчик ссылок становится равным нулю, то освобождаются блоки данных и inode. Если после декремента счетчика остаются еще ссылки на inode, то блоки данных и inode не отсоединяются. Системный вызов unlink нельзя использовать для аналогичных действий в отношении каталогов. In
Команда In позволяет создавать ссылку на файл. Команда In использует при работе систем ный вызов link:
link НАЗНАЧЕНИЕ INCLUDE
Установление новой ссылки на файл #include < unistd.h >
ИСПОЛЬЗОВАНИЕ
int result = link(const char *orig, const char *new);
АРГУМЕНТЫ
orig - имя оригинальной ссылки new - имя новой ссылки
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
С помощью системного вызова link устанавливается новая ссылка на inode. В новую ссыл ку записывается новое имя ссылки и номер inode оригинальной ссылки. Если при обраще нии к вызову было указано уже существующее имя, то фиксируется ошибка выполнения системного вызова link. Использовать link для создания новых ссылок на каталоги нельзя. mv
Команда mv позволяет изменять имя или расположение файла или каталога в дереве ката логов. Команда mv является более гибкой командой, нежели другие команды, которые бы ли представлены в этом разделе. Ряд ее внутренних деталей мы рассмотрим позже. Во многих случаях при работе этой команды используется системный вызов rename:
4.4. Понимание каталогов
151
rename НАЗНАЧЕНИЕ
Переименовать или переместить ссылку
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
int result = renamefconst char *from, const char *to);
АРГУМЕНТЫ
from - имя оригинальной ссылки to - имя новой ссылки
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Системный вызов rename изменяет имя или место расположения файла или каталога. Например, при вызове rename(M y M ,"y.old") будет изменено имя файла, а при вызове rename('y\M c/(J2/y.old") будет изменено имя и место расположения файла. Системный вызов rename можно использовать как в отношении файлов, так и в отношении катало гов. Но существует ряд ограничений при работе с перемещением каталогов. Например, вы не сможете переместить каталог в один из его подкаталогов. Попробуйте предска зать результат работы вызова rename("demodir/c","demodir/d2/c M ) и посмотреть, какой опус тошительный будет результат. В отличие от link системный вызов rename удаляет суще ствующий файл или пустой каталог с именем to.
Как работает rename, зачем существует rename? Как rename перемещает файл в другой каталог? Файлы реально в каталогах не находятся. В каталогах находятся ссылки на файлы. Поэтому rename перемещает ссылку из одного каталога в другой. Схема действий при переименовании у на c/d2/y.old представлена на ри сунке 4.11.
Изучение файловых систем. Разработка версии pwd
152
Перед переименованием ссылка на inode 491 с именем у находилась в каталоге demodir. После переименования ссылка на 491, которая стала называться y.old, будет находиться в каталоге c/d2, а оригинальная ссылка пропадает. Как ядро перемещает ссылку? В ядре Linux базовый алгоритм системного вызова rename такой: скопировать оригинальную ссылку в соответствии с новым именем и/или местом расположения удалить оригинальную ссылку
В Unix есть два системных вызова link и unlink,с помощью которых можно выполнить эти два действия. Поэтому вызов rename("xM,"zM) будет работать так: if (linkf'xYz") != -1) unlinkf'x");
В Olden Days ® не было системного вызова rename. Поэтому команда mv использовала вызовы link и unlink. С добавлением в ядро вызова rename были решены две проблемы. Во-первых, вызов rename делает возможным благополучно переименовывать или переме щать каталоги. Раньше, в более старых системах, обычным пользователям не разрешалось выполнять вызовы link или unlink в отношении каталогов. Поэтому они могли быть исполь зованы для переименования каталогов. Еще одно важное преимущество системного вызова rename - поддержка файловых систем не для Unix. При работе в Unix переименование файла или каталога сводится к изменению ссылки, но в других системах эта схема может не работать. Добавляя к ядру общий метод, который был назван rename, добиваются скрытия деталей реализации, что обеспечивает возможность работы с одним и тем же кодом на все типах файловых систем. cd
Команда cd изменяет текущий каталог для процесса. Команда cd влияет на процесс, а не на каталог. Пользователь может сказать “Я перешел в каталог /tmp и нашел там много моих рабочих файлов”. Это по сути то же, что сказать “Я отправился на чердак и обнаружил там много моих старых книг”. При работе команды cd используется системный вызов chdir:
chdir НАЗНАЧЕНИЕ
Изменить текущий каталог у вызывающего процесса
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
int result = chdrrfconst char *path);
АРГУМЕНТЫ
path - путь к новому каталогу
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Для каждой исполняемой программы в Unix устанавливается ее текущий каталог. Систем ный вызов chdir позволяет изменять текущий каталог процесса. После этого говорят, что ‘'процесс находится в этом каталоге”. В процессе поддерживается переменная, в которой хранится номер inode текущего каталога. Когда вы выполняете “переход в новый каталог”, то вы заставляете изменить значение этой переменной. Важное упражнение. Как работает команда cd..? Обратитесь к примеру поддерева demodir и представьте, что вы находитесь в каталоге с именем с. Какой номер inode у ваше го текущего каталога? Далее выполните команду cd dl. Каким стал теперь номер inode вашего текущего каталога? Каким образом ядро получает значение этого номера? Если вы
4.5.Разработкапрограммыpwd
153
теперь выполните команду cd../.., то какой будет номер inode вашего текущего каталога? Какие шаги выполняет ядро, чтобы получить значение этого номера? Если вы разобра лись, какие действия происходят при выполнении этого важного упражнения, то вы поня ли, как работает команда pwd.
4.5.
Разработка программы pwd
Команда pwd выводит путь для текущего каталога. Например, если вы спустились по дере ву вниз в каталог demodir/c/d2 и далее выполнили команду pwd, то вы увидите нечто похо жее на следующее: $pwd /home/yourname/experiments/demodir/c/d2
Где хранится такой длинный путь? Он не хранится в текущем каталоге. Текущий каталог обращается сам к себе с помощью ссылки “.” и имеет номер inode. Каталог - это просто узел среди набора узлов, соединенных между собой. Каким образом команда pwd узнает, что каталог называется d2, как она узнает, что у текущего каталога родительским будет каталог с, как узнает, что у каталога с родительским каталогом будет demodir и т. д.?
4.5.1. Как работает команда pwd? Ответ на этот вопрос, как и ответы на все вопросы этой главы, будет простым. Команда, следуя по указателям, читает содержимое каталогов. На самом деле команда pwd поднима ется вверх по дереву, от каталога к каталогу, отмечая для каждого шага перемещения номер inode для имени “точка”. Затем через родительский каталог выбирается имя, которое установлено для этого номера inode. Это делается до тех пор, пока не будет дос тигнута вершина дерева. Рассмотрим, например, рисунок 4.12:
Наше восхождение по дереву начинается в текущем каталоге. Он обозначен на рисунке индексом 1 в нижнем правом углу. В этом каталоге для ячейки с именем номер inode равен 247. Теперь с помощью chdir переместимся в родительский каталог где найдем
Изучение файловых систем. Разработка версии pwd
154
запись, содержащую номер inode 247. Для этого номера в этой записи указано имя d2. Поэтому имя последнего компонента в пути будет d2. А какое имя у родительского катало га? В родительском каталоге обращаемся к записи, содержащей имя и выбираем его номер inode 520. Далее с помощью chdir переходим еще на шаг вверх по дереву. И уже в этом родительском каталоге находим запись с номером inode 520. Из нее узнаем имя ка талога - с. Поэтому последние два элемента пути будут теперь такими: c/d2. Алгоритм для повторения этих трех шагов можно выразить так: 1. Выбрать номер inode для имени назовем его п (использовать stat). 2. chdir... (использовать chdir). 3. Найти имя для ссылки с номером inode п (.использовать opendir, readdir, closedir). Повторить (до тех пор, пока не достигнете вершины дерева). Все выглядит достаточно просто. Но остались два вопроса. Вопрос 1: Как мы узнаем, что достигли вершины дерева? В корневом каталоге файло вой системы Unix в записях с именами ссылок и указан один и тот же inode. Про граммисты часто отмечают конец связанных структур указателем NULL. Разработчики Unix могли бы использовать нулевой указатель в записи корневого каталога, содержащего имя ”. ”. Но вместо этого было принято решение “зациклиться на себя”. В чем преимуще ство такого решения? В нашей версии pwd повторение при составлении пути будет проис ходить до тех пор, пока не будет достигнут каталог, в котором в записях с именами ссылок “ ” и записан один и тот же inode. Вопрос 2: Как нам вывести имена каталогов в пути в правильном порядке? Мы напи шем цикл и построим строку, состоящую из имен каталогов, с помощью strcat или sprintf. Мы не будем использовать при составлении строки с рекурсивной программой, которая разворачивается к вершине дерева и выводит по одному имена каталогов, по мере продви жения по дереву.
4.5.2. Версия команды pwd Г spwd.c: упрощенная версия pwd ★ * начало работы в текущем каталоге и * последующее восхождение по дереву файловой системы к ее корню. * При этом сначала выводится головной элемент, а затем текущая часть пути
*
* Используется readdir() для получения информации об элементах каталога
★
* Нештатная ситуация: Вывести пустую строку, если достигли"/"
★★ j
tinclude <stdio.h> «include <sys/types.h> tinclude <sys/stat.h> tinclude ino.t get_inode(char *); void printpathto(ino_t); void inum_to_name(ino_t, char *, int); int main()
I Разработка программы pwd printpathto(get_inode( putchar('\n'); return 0;
Г здесь вывод пути */ Г затем добавить новую строку */
}. void printpathto( ino_t thisjnode)
Г
* *
вывод пути, ведущего к объекту с этим inode, как будто рекурсивно
7
{ ino_t myjnode; char its_name[BUFSIZ]; if (get inodef'..") != thisjnode)
{ chdirf'.."); /* перемещение вверх на каталог */ inurn_to_name(thisJnode,its_name,BUFSIZ);/* получить имя каталога7 myjnode = get_inode("."); Г печать головной чаете printpathto(myjnode); /* рекурсия */ printf("/%s", its.name); Г теперь печать */ /* name of this */
void inum to name(ino_t inodejo find, char *namebuf, int buflen)
/*
* Найти в текущем каталоге файл с этим номером inode ж и скопировать его имя в namebuf 7 DIR *dir_ptr; struct dirent *direntp; dir_ptr = opendirf."); if (dir ptr == NULLK perror ("."); exit(1);
Г каталог */ /* каждая запись */
Г * поиск каталога для файла с заданным inum 7 while ((direntp = readdir(dir_ptr)) != NULL) if (direntp->d_ino == inodeJo_find) strncpy( namebuf, direntp->d_name, buflen); namebuf[buflen-1] = '\0'; closedir(dir_ptr); return;
/* на всякий случай */
Изучение файловых систем. Разработка версии pwd
156
fprintf(stderr, "error looking for inum %d\n", inode_to_find); exit(1);
} inotget inode(char*fname)
r~
-
’ возвратить номер inode файла
7 { struct stat info; if (stat(fname, &info) == -1){ fprintf(stderr, "Cannot stat"); perror(fname); exit(1);
} return info.st ino;
}
Работает ли наша программа? Вот реальный вывод после запуска программ:
$ /bin/pwd /home/bruce/experiments/demodir/c/d2
$spwd /bruce/experiments/demodir/c/d2
$ Все закончилось красиво. В реальной версии команды pwd в составе пути выводятся все каталоги до корня дерева. Но и наша версия останавливается, когда она достигает верши ны дерева. Так в чем проблема? Есть ли ошибки? Нет. Программа на самом деле работает правильно. Она действительно останавливается, когда достигает корня файловой системы. Но корень этой файловой системы не является корнем всего дерева, которое поддерживается на компьютере. В Unix можно построить на одном диске дерево, состоящее из деревьев. Каждый диск, или каждый раздел на диске, содержит дерево каталогов. Эти отдельные деревья соединяются вместе и составляют одно единое дерево. Наша версия pwd столкнулась с одним из огра ничений такого построения.
4.6. Множественность файловых систем: Дерево из деревьев Что происходит, когда в системе Unix используется два диска или раздела? Мы наблюда ли, как происходит работа в системе. Мы смогли организовать один раздел, в котором раз местилось дерево каталогов. А если у вас имеется два раздела, то можем ли мы получить два отдельных дерева? Как организуется работа с разделами в других системах? В некоторых операционных сис темах каждому диску или разделу назначается символ дисковода или имя тома. Этдт сим вол устройства или имя тома становится частью полного пути к файлу. Есть экстремаль ные схемы, когда в системах назначаются номера блоков по всем дискам, с тем чтобы соз дать один виртуальный диск. . В Unix есть еще одно, третье преимущество. В каждом разделе хранится дерево собствен ной файловой системы. Но поскольку на компьютере существуют более одной файловой
4.6. Множественность файловых систем: Дерево из деревьев
157
Пользователь видит одно полное дерево каталогов. Действительно же существуют два дерева - одно на диске 1, а другое на диске 2. Каждое дерево имеет корневой каталог. Одна из файловых систем выступает в роли корневой файловой системы. Вершина этого дерева явля ется одновременно вершиной единого дерева. Другая файловая система присоединяется к не которому подкаталогу корневой файловой системы. С позиций системы ядро устанавливает ссылку на другую файловую систему из каталога в корневой файловой системе.
4.6.1. Точки монтирования В Unix используется выражение монтировать файловую систему со смыслом, который аналогичен выражению пришпилить бабочку или приклеить картину на картон,1 т. е. не обходимо что-то к чему-то прикрепить. Корневой каталог поддерева прикрепляется к ка талогу в составе корневой файловой системы. Каталог, к которому присоединяется под дерево, называется точкой монтирования для этой второй системы. Команда mount выводит список файловых систем, которые в текущий момент примонтированы в системе, и их точки монтирования:
$ mount /dev/hda1 on / type ext2 (rw) /dev/hda6 on /home type ext2 (rw) none on /proc type proc (rw) none on /dev/pts type devpts (rw,mode=0620)
$ В первой строке выводится информация о том, что раздел 1 на устройстве /dev/hda (первый IDE дисковод) смонтирован в корневой вершине дерева. Этот раздел является корневой файловой системой. Во второй строке сообщается о том, что файловая система на устрой стве /dev/hda6 присоединена к корневой файловой системе к каталогу /home. Таким обра зом, когда пользователь переходит с помощью chdir из каталога / в каталог /home, то проис ходит переход от одной файловой системы в другую. Когда наша версия pwd будет прохо дить по дереву, то она остановится в каталоге /home, поскольку она достигнет вершины своей файловой системы.
158
Изучение файловых систем. Разработка версии pwd
Плюрализм Unix В Unix допускается монтировать к корневой файловой системе файловые системы раз личных типов. Например, на Unix машине, где есть корневая файловая система, можно смонтировать а файловую систему IS09660 на CD-ROM. Файлы и каталоги этого диска становятся после этого частью единого дерева. Если в ядре есть подпрограммы, которые могут работать со структурой файловой системы Macintosh, то можно примонтировать файловую систему Macintosh, расположенную на каком-то диске. Можно даже монтиро вать файловые системы, которые расположены на других компьютерах, используя сетевые средства.
4.6.2. Дублирование номеров Inode и связей между устройствами Объединение нескольких файловых систем в составе одного дерева имеет ряд преиму ществ. Однако есть один небольшой нюанс. При работе в Unix каждый файл в файловой системе имеет номер inode. Аналогично тому, как могут быть расположены на двух раз личных улицах дома с одним номером 402, так и на двух различных дисках могут быть файлы, каждый из которых имеет inode под номером 402. В каких-то каталогах могут быть записи, где для файлов с некоторыми именами указан номер inode 402. Как ядро может уз нать, какой номер inode следует использовать?
Рисунок 4.14 Номера inode и файловые системы Рассмотрите внимательно на увеличенной части рисунка два каталога. Один из них нахо дится в корневой файловой системе, а другой в примонтированной файловой системе. В каждом каталоге есть ссылка на inode 402. Имена ссылок myls.c и y.old соотнесены одно му и тому же номеру inode. Но где расположен этот inode? В файловой системе на диске 1 есть inode с номером 402, а на диске 2 в файловой системе есть другой inode с номером 402. Это означает, что эти ссылки вовсе не ведут к одному и тому же файлу. На этом примере иллюстрируется проблема, которая возникает при создании дерева деревьев. Как оказалось, номер inode идентифицирует файл совсем не однозначно. Как мы только что убедились, один и тот же номер inode 402 находится в двух различных катало гах, но каждый из них ссылается на разные файлы. При этом все выглядит так, будто ссыл ки установлены на один и тот же файл. Но это не так. Как мне сослаться на один и тот же файл из различных файловых систем? Вы не сможе те этого сделать. Файл существует как набор блоков данных и inode на диске. В каталоге на этот inode есть ссылка. Что должно произойти, если ссылка на одном диске будет ука зывать на inode, который находится на другом диске? Если другой диск был размонтиро
4.6. Множественность файловых систем: Дерево из деревьев
159
ван, то файл будет потерян. Еще хуже случай, если будет примонтирован совсем другой диск, на котором окажется файл с номером inode 402. Но содержание этого другого файла будет совершенно другим, чем нам нужно. Существует еще ряд проблемных ситуаций, над которыми вы можете подумать. Знают ли системные вызовы link и rename о том, что было рассмотрено выше? Да. Системный вызов link отказывается создавать связи между устройствами, а системный вы зов rename отказывается перемещать номер inode по файловым системам. Читайте доку ментацию в справочнике для изучения того, какими могут быть коды ошибок.
4.6.3. Символические ссылки: Панацея или блюдо спагетти? Твердые ссылки по сути являются указателями, с помощью которых каталоги объеди няются в дерево. Такие ссылки являются указателями, которые связывают имена файлов с собственно файлами. Твердые ссылки не могут указывать на inodes в других файловых системах. И даже root не может сделать твердую ссылку на каталог. Однако есть достаточно причин, чтобы иметь возможность ссылаться на каталоги или файлы в других файловых системах. Для удовле творения таких требований в Unix поддерживается еще один тип ссылок - символические ссылки. Символическая ссылка производит обращение к файлу по имени, а не по номеру inode. Сделаем такое сравнение:
$ who > whoson $ In whoson ulist $ Is -li whoson ulist 377 -rw-r--r-- 2 bruce users 235 Jul 16 09:42 ulist 377 -rw-r--r-- 2 bruce users 235 Jul 16 09:42 whoson
$ In -s whoson users $ Is -li whoson ulist users 377 -rw-r--r-- 2 bruce users 235 Jul 16 09:42 ulist 289 Irwxrwxrwx 1 bruce users 6 Jul 16 09:43 users -> whoson 377 -rw-r--r~ 2 bruce users 235 Jul 16 09:42 whoson
Файлы whoson и ulist ссылаются на один и тот же файл. Для обоих файлов указаны одни и те же характеристики: они имеют номер inode 377, у каждого указан один и тот же размер файла, одно время модификации файла и одно и то же значение числа ссылок. Твердая ссылка ulist была создана с помощью команды In. С другой стороны, с помощью команды In -s создается символическая ссылка на файл who son, которая будет называться users. С помощью команды Is -И обнаруживаем, что ссылка users имеет inode 289. Символ 1 в позиции для указания типа файла свидетельствует о том, что это символическая ссылка. Число ссылок, время модификации и размер имеют значе ния, отличные от значений этих же характеристик у оригинального файла. Файл users не является оригинальным файлом whoson, но ведет себя в точности так, как ведет себя ори гинальный файл при обращении к нему на чтение или запись. Например:
$ wc -I whoson users 5 whoson 5 users 10 totol
$ diff whoson users
160
Изучение файловых систем. Разработка версии pwd
С помощью команд wc и diff производится обращение к файлам и подсчет строк в этих файлах. Потом производится сравнение содержимого этих файлов. В рассмотренном случае ядро использует имя для обращения к оригинальному файлу. Но, с другой стороны, при выполнении вызова stat будет получена информация о ссылке, а не об оригинальном файле. Символические ссылки могут действовать в составе различных файловых систем, потому что они не хранят inode оригинального файла. Такие ссылки можно также устанав ливать на каталоги. Это свойство значительно отличает этот вид ссылок от других ссылок и позволяет рассматривать в качестве средства “связывания” файловых систем для получения единого целого3. Символическим ссылкам свойственны те проблемы, о которых шла речь при обсуждении связей между устройствами. Если удаляется файловая система, содержащая оригиналь ный файл, или оригинальный файл получит новое имя, или будет инсталлирован в систе ме новый файл с таким же именем, как у оригинального, то символическая ссылка будет ссылаться (в соответствии с порядком перечисления этих вариантов): в никуда, в никуда, на совсем другое содержание, чем у оригинального файла. Символические ссылки могут указывать на родительские каталоги, тем самым создавая циклы в дереве каталогов. С помощью символических ссылок можно превратить вашу файловую систему в “порцию спагетти”. Но ядро признает только такие символические ссылки и никакие другие. Поэтому ядро может проверять ссылки на наличие потери объектов, на которые произво дится ссылка, а также на наличие бесконечных циклов. Системные вызовы для символических ссылок Системный вызов symlink создает символическую ссылку. С помощью системного вызова readlink можно получить имя оригинального файла. С помощью системного вызова Istat получают статусную информацию об оригинальном файле. Обратитесь к справочнику и прочтите документацию относительно вызовов unlink, link, чтобы узнать, как они рабо тают с символическими ссылками.
Заключение Основные идеи •
•
•
•
•
Unix организует на дисковой памяти файловые системы. Файловая система - это объединение файлов и каталогов. Каталог - это список имен и указателей. Каждая запись в каталоге указывает на файл или каталог. В каталоге находятся записи, которые указывают на его родительский каталог и его подкаталоги. Файловая система в Unix состоит из трех основных частей: суперблок, таблица inode, область данных. Позиция inode в таблице называется номером inode файла. Номер inode является уникальным идентификатором файла. Один и тот же номер inode может находиться в различных каталогах, но для каждого такого номера будут разные имена файлов. Каждая такая запись называется твердой ссылкой. Символическая ссылка - это ссылка, которая обеспечивает обращение к файлу по имени, а не номеру inode. Несколько файловых систем могут быть объединены в одно дерево. Операция, с помощью которой ядро связывает каталог одной файловой системы с корнем другой файловой системы, называется монтированием. В Unix имеются системные вызовы, с помощью которых программист может создавать и удалять каталоги, дублировать указатели, перемещать указатели, изменять имена, которые ассоциированы указателям, присоединять и отсоединять другие файловые системы.
3. Вызов Istat разыменовывает ссылку.
Заключение Визуальное заключение Запись в каталоге состоит из имени файла и номера inode. Номер inode указывает на структуру на диске. Эта структура содержит информацию о файле и о распределении бло ков данных файла.
Что дальше? Файлы - это только один из источников данных. Программы также обрабатывают данные, которые поступают с устройств, таких, как терминалы, видеокамеры, сканеры. Как про граммы в Unix получают данные от устройств и как посылает на них данные?
Исследования 4.1 Команда pwd выводит путь к текущему каталогу в файловой системе. В определенном смысле каталог - это ваше место расположения в дереве. На самом деле такой каталог представляет собой некоторое объединение байтов, которые расположены на диске в каком-то месте. Это место можно определить с помощью указания головки, дорожки, сек тора и байта. Или можно указать с помощью цилиндра, головки сектора и байта. Какие имеются возможности для преобразования имени текущего каталога в термины аппарат ных средств, с помощью которых производится указание места расположения? 4.2 Исследуйте один из твердых дисков в системе, которую вы используете. Определите, сколько разделов на этом диске. Определите для каждого раздела число inodes и число блоков данных. 4.3 Абстракцию типа “диск как массив” использует не только система Unix при создании фай ловой системы. Ее вправе использовать любой, кто имеет необходимые права доступа Чтобы реализовать такой проект, вам необходимо иметь права доступа root. Каталог /dev содержит файлы, с помощью которых вы можете читать байты данных размещаемые в блоках на диске так, будто эти байты находятся в файле. В система> Linux с IDE-дисководами вы можете найти файлы, которые называются /dev/hda, /dev . *. /j_../Lj. / л ^ . / u a a /Kottrtui vrrnnwrTR не являются обычными файлам*
162
Изучение файловых систем. Разработка версии pwd данных, аналогичные файлам /etc/passwd или /var/adm/utmp. Эти файлы устройств предоставляют возможность доступа к необработанным (raw) данным на диске. Вы можете использовать команды cat, more, ср и любые другие команды для работы с файлами для чтения содержимого, которое расположено на диске. Диск, подобно файлу utmp, имеет вполне очевидную структуру. Одна из возможностей получить поблочное содержимое диска - выполнить команду od -с /dev/hda | more. По мере выполнения такого постраничного вывода вы будете читать содержимое диска так, как будто вы читаете одну непрерывную последовательность из дисковых блоков. Для каждого раздела представлен один из таких специальных файлов устройств. Например, первый раздел на/dev/hda называется /dev/hdal. Исследуйте ваш каталог /dev и определите, какие специальные файлы в этом каталоге соотнесены твердым дискам, гибким дискам, CD-ROM или другим дисковым устрой ствам в системе.
4.4
В ядре имеется кодовый текст, который определяет место расположения свободного inode и находит свободные дисковые блоки. Это делается, когда ядро создает новый файл. Как ядро узнает, какие из блоков являются свободными? Как ядро узнает, какие из inode являются свободными? Какой метод используется в файловой системе на вашей машине для учета последовательности неиспользуемых блоков и inodes?
4.5
В Unix можно читать и монтировать диски (такие как PC-DOS и Macintosh диски), на ко торых находятся non-Unix-файловые системы. В этих файловых системах нет inodes. Тем не менее, если вы используете команду mount, для присоединения одного из таких дисков к системе Unix, то после выполнения команды Is -i вы обнаружите вывод списка inode для таких систем. Обратитесь к исходному коду Linux и поищите там ответ на вопрос: откуда берутся эти номера? Зачем в Linux происходит добавление этих номеров?
4.6 Текст, предназначенный для описания списка распределения блоков в составе inode, пред ставляет собой описание десяти прямых блоков, одного косвенного блока, одного двойно го косвенного блока и одного тройного косвенного блока. В некоторых версиях Unix используют другие номера для представления прямых и косвенных блоков. (а) Какой формат списка распределения в inode используется в вашей системе? Дета ли можно найти при рассмотрении заголовочных файлов. (в) Какой размер блока данных на вашей системе? (c) Какой самый большой файл в вашей системе не использует косвенные блоки? (d) Какой самый большой файл в вашей системе не использует двойные косвенные блоки? Сколько реально использует блоков этот самый большой файл? 4.7 Счетчик ссылок для каталогов. Файл может иметь много ссылок. Число таких ссылок запи сывается в счетчик ссылок для файла. А как обстоят дела в отношении каталогов? Исполь зуйте в вашей версии дерева demodir команду Is -I, чтобы посмотреть на значения счетчика ссылок для каталогов. Сравните эти значения счетчиков с числом дуг (Для каждого ката лога. - Примеч. пер.) на диаграмме. Объясните значение счетчика ссылок для каталога. Почему каждый каталог имеет значение счетчика ссылок, которое будет не меньше 2? 4.8
Ссылки на каталоги. Использовать вызов link для образования новой ссылки на ката лог нельзя. В Olden Days ® делать ссылки на каталоги разрешалось суперпользовате лю. В примере demodir проследите действие системного вызова link("demodir/c","demodir/d2/e") в пользовательском и системном режимах. Затем поясните результаты работы команды Is -iaR demodir.
Заключение
163
4.9 Скрытые поддеревья. Когда вы с помощью команды mount присоединяете одну файловую систему к другой файловой системе, то точка монтирования должна быть каталогом в ори гинальной файловой системе. Например, вы можете присоединить файловую систему на диске /dev/hda4 к каталогу /home2. Ответьте на следующие два вопроса: (а) Что произойдет, если точки монтирования (в данном случае /Ьоте2)не существует? (в) Что произойдет, если точка монтирования существует и содержит файлы и подка талоги? 4.10 Команда rmdir не удаляет каталог, в котором содержатся файлы и подкаталоги. Почему такое решение можно считать хорошим? Но, с другой стороны, вы можете удалять каталог, в котором находится пользователь. Сделайте следующее и удивите ваших друзей: образуйте новый каталог с произволь ным именем, перейдите в этот каталог. Далее откройте еще одно shell-окно и удалите этот каталог. Закройте второе shell-окно и выполните команду /bin/pwd. Объясните, что произойдет. 4.11 Что означает термин цилиндр для твердого диска? Какая физическая конструкция твердого диска, которая делает концепцию цилиндров важной с позиций эффективного использова ния диска? Найдите через Web пояснения термина группа цилиндров. Объясните связь между этой идеей и моделью файловой системы, которая была представлена в тексте. 4.12 Нехватка пространства на диске. Для большинства людей знакома проблема нехватки пространства на диске. В файловой системе Unix имеется область для inodes и область для данных. Поэтому возможно, что все пространство в области для inode будет использова но, хотя еще есть свободное пространство в области данных. Когда вы инсталлируете новый диск в Unix, то вам необходимо выделить на диске область для расположения в ней таблицы inode и выделить область данных. Для каждого файла в файловой системе необ ходим один inode. Чем больше места будет отведено под таблицу inode, тем меньше про странства останется для хранения содержимого файлов. Пускай вы собираетесь устанавливать новый твердый диск. Команда mkfs позволяет образовать новую файловую систему и дает вам возможность определить размер таб лицы inode. Почитайте документацию по этой команде. Почему вам может потребо ваться много inodes? Почему у вас может появиться необходимость запросить мень шее их число, чем обычно? 4.13 Системному вызову stat передается имя файла и указатель на структуру, которую он запол няет информацией о файле. Объясните, как работает системный вызов stat, используя для этого модель каталога, inode и данных. Где системный вызов находит данные о файле, которые он копирует в структуру stat?
Программные упражнения 4.14 Напишите текст одной командной строки в Unix, чтобы можно было построить дерево ка талогов demodir. 4.15 В Unix-команде mkdir можно использовать опцию -р. Напишите версйю команды mkdir, в которой можно использовать эту опцию. 4.16 Команда mv - это всего лишь обертка системного вызова rename. Напишите версию команды mv, для которой при обращении потребуется указывать два аргумента. Первый аргумент должен быть именем файла, а второй аргумент должен быть именем файла или
164
Изучение файловых систем. Разработка версии pwd именем каталога. Если вторым аргументом указано имя каталога, то команда mv переме щает файл в этот каталог. В противном случае команда mv переименовывает файл, если это возможно.
4.17 Текст версии rename написан с использованием link и unlink. В этом кодовом фрагменте производится проверка кода возврата из link, но не проверяется код возврата из unlink. Расширьте возможности этого кода с целью достижения корректной реакции при ошибках исполнения unlink. 4.18 Ознакомьтесь с документацией в справочнике и с содержанием заголовочных файлов, что бы разобраться со структурой суперблока на вашей системе. Напишите программу, которая открывает файловую систему, читает содержимое суперблока, и отображает ряд характеристик файловой системы в ясном, читабельном формате. Это упражнение анало гично составлению тех программ, которые были написаны для отображения содержимого utmp записей и stat структур. 4.19 Процедура создания нового файла включает четыре основные операции. Все они должны быть успешно завершены для того, чтобы файл был правильно включен в состав файло вой системы. Что случится, если вдруг будет выключено питание компьютера где-либо при выполнении этих действий по созданию файла? Например, что произойдет, если дан ные были размещены в области данных, но inode сформировать не успели? (a) Определите порядок, в котором должны выполняться эти четыре основные опера ции. Аргументируйте ваш выбор. (b) Предположите, что система будет построена и работает в соответствии с тем, как вы ответили на вопрос (а). Что будет, если авария произойдет между какими-то двумя шагами в вашей процедуре? Например, если ваш процедура состоит из четырех шагов, то таких точек между шагами будет три. Объясните, какие некорректности воз никнут в системе при возникновении аварии в каждой из этих трех точек? (c) Почитайте документацию по Unix-команде fsck. Насколько похож ваш ответ на во просы пункта (Ь) на те действия, которые выполняет команда fsck? 4.20 В главе 3 мы разработали версию команды Is -1. Модифицируйте эту программу таким образом, чтобы она выводила бы номер inode дополнительно к той информации, которую она до того выводила. Где будет производиться поиск номера inode в модифицированном варианте вашей версии?
Проекты На основе материала этой главы вы можете изучить дополнительный материал и разрабо тать на его основе следующие Unix-программы: find, du, Is -R, mount, dump
Глава 5 Управление соединениями. Изучение stty
Цели Идеи и средства • • • • • •
Подобие файлов и устройств. Отличие между файлами и устройствам. Атрибуты соединений. Условия гонок и атомарные операции. Драйверы устройств. Потоки
Системные вызовы и функции •
fcntl, ioctl
•
tcsetattr, tcgetattr
Команды •
stty
•
write
Управление соединениями. Изучение stty
166
5.1. Программирование устройств % В нескольких главах мы рассмотрели программы, которые работают с файлами и катало гами. В компьютере есть еще один источник данных — периферийные устройства. Это модемы, принтеры, сканеры, мыши, громкоговорители, видеокамеры, терминалы. В этой главе мы рассмотрим сходство и различие между файлами и устройствами. Рассмотрим, каким образом можно использовать такие свойства при управлении соединениями с устройствами. В этой главе мы напишем версию команды stty. Команда stty дает возможность пользовате лям проверять и модифицировать установки, с помощью которых производится управле ние соединением клавиатуры и экрана.
5.2. Устройства подобны файлам Многие считают, что файл представляет собой “связку” данных, хранимых на диске. Но в Unix поддерживается более абстрактное представление файла. Прежде всего рас смотрим несколько характеристик, касающихся файлов. Файлы содержат данные, у фай лов есть свойства, файлы идентифицируются с помощью имен в каталогах. Вы можете по байтно читать данные из файла, а также побайтно записывать данные в файл. Ну а теперь заметьте: эти же самые характеристики и действия применимы и в отношении устройств. Рассмотрим звуковую карту, к которой присоединен микрофон и громкоговоритель. Вы говорите что-либо в микрофон, звуковая карта преобразует сигналы вашего голоса в поток данных, а программа может читать этот поток данных. Когда программа записы вает поток данных на карту, то полученный звук передается на громкоговорители. Для программы звуковая карта является источником данных и местом, куда можно передавать данные. Терминал, имеющий клавиатуру и дисплей, также аналогичен файлу. Значения клавиш, на которые вы нажимаете, считываются программой и воспринимаются как входные данные для нее. А символы, которые процесс передает на терминал, отображаются на экране. Для Unix звуковые карты, терминалы, мышь и дисковые файлы - все это рассматривается как один и тот же тип объектов. В системе Unix каждое устройство трактуется как файл. Каждое устройство имеет имя, номер inode, собственника, разряды прав доступа и время последней модификации. Каждый, кто знает, как можно работать с файлами, автоматиче ски может использовать эти знания при работе с терминалами и другими устройствами.
t
5.2.1. Устройства имеют имена файлов Каждое устройство (терминал, принтер, мышь, диск и т. д.), которое присоединено к Unix машине, представлено в системе именем файла. По традиции файлы, которые представ ляют устройства, помещены в каталоге /dev, но вы вправе создавать файлы устройств в любом каталоге. Рассмотрите состав каталога /dev на различных Unix-машинах. Ниже показан фрагмент листинга для машины, на которой я сейчас работаю:
$ Is -С /dev | head -5 XOR agpgart apm_bios arcd dsp
fd1u720 fd1u800 fd1u820 fd1u830 flashO
loopl IpO Ip1 Ip2 mcd
ptyqf ptyrO ptyrl ptyr2 ptyr3
sda7 sda8 sda9 sdb sdbl
stderr ' stdin stdout tape tcp
ttysd ttyse ttysf ttytO ttytl.
5.2. Устройства подобны файлам
167
На этом листинге представлено несколько типов устройств. Файлы с именами 1р* в треть ей колонке - это принтеры. Файлы с именами fd* во второй колонке - это дисководы гиб ких дисков. Файлы с именами sd* — это разделы SCSI-устройств. Имя файла /dev/tape при своено ленточному устройству, предназначенному для построения на нем системных ко пий (backup). Файлы с именами tty* в последней колонке — это терминалы. Программы при чтении из таких файлов получают значения символов при нажатии клавиш на клавиа туре. По мере записи данных в эти файлы программы посылают данные на экраны терми налов. Файл dsp представляет собой соединение со звуковой картой. Процесс проигрывает звуко вой файл путем записи данных из звукового файла в этот файл устройства. Процесс может открыть файл /dev/mouse и далее воспринимать события, связанные с нажатием на кнопки мыши и со всеми изменениями расположения курсора мыши.
5J2.2. Устройства и системные вызовы Устройствам можно не только присваивать имена файлов. В их отношении можно исполь зовать все системные вызовы, предназначенные для работы с файлами: open, read, write, lseek, close, stat. Например, фрагмент программы для чтения данных с магнитной ленты будет иметь такой вид: int fd; fd = openf/dev/tape", 0_RD0NLY); /* связаться с ленточным устройством */ lseek(fd, (long) 4096, SEEKSET); Г перемотка ленты на 4096 байтов */ n = read(fd, buf, buflen); Г чтение Данных с ленты */ close(fd); /* разрыв связи с устройством */
Для работы с устройствами можно использовать те же системные вызовы, которые вы использовали для работы с дисковыми файлами. В Unix фактически нет других средств для связи с устройствами.
Некоторые устройства не поддерживают все файловые операции Когда вы перемещаете мышь и нажимаете на кнопки мыши, то от мыши в систему посту пают байты данных, которые процесс может читать с помощью вызова read. Ну а что про изойдет, если процесс попытается выполнить вызов write в отношении мыши? Передачи данных на мышь не произойдет. Мышь можно только перемещать и нажимать на ней кнопки. Для файла /dev/mouse не поддерживается системный вызов write. Конечно, кто-то может придумать мышь с моторчиком и написать для нее усовершенствованный драйвер, который будет способен как принимать события от мыши, так и вырабатывать их. Для терминалов поддерживаются системные вызовы read и write, но не поддерживается вызов lseek. А почему?
5.2.3. Пример: Терминалы аналогичны файлам Большая часть пользовательских входов для Unix производится через терминалы. Файлы ttysd, ttyse и т. д. в приведенном листинге представляют собой терминалы. Терминалом на зывают все, что ведет себя аналогично классической клавиатуре с устройством отображе ния. Сюда можно отнести печатающий терминал 70-х годов, и клавиатуру с экраном, которые подсоединены к последовательному порту, и ПК с модемом и программой эмуля ции терминала, которая связана с системой через телефонную линию. Окна telnet или ssh,
168
Управление соединениями. Изучение stty
через которые можно входить в систему через Интернет, ведут себя как терминалы. Основными компонентами терминала являются источник ввода символов от пользователя и любое устройство отображения для вывода данных пользователю. Устройство отобра жения может даже выдавать тексты для слепых в кодах Брайля или воспроизводить дан ные в звуковом виде. С помощью команды tty можно узнать имя файла, который представляет ваш терминал. Давайте поэкспериментируем с терминальными файлами:
$ tty /dev/pts/2
$ ср /etc/motd /dev/pts/2 Today is Monday, we are running low on disk space. Please delete files. - your sysadmin
$ who > /dev/pts/2 bruce pts/2 Jul 17 23:35 (ice.northpole.org) bruce pts/3 Jul 18 02:03 (snow.northpole.org)
$ Is -li /dev/pts/2 4 crw-w-w-1 bruce tty 136,2 Jul 18 03:25 /{Jev/pts/2
Команда tty сообщает, что мой терминал подсоединен к файлу /dev/pts/2, т. е. оконечное имя файла 2, файл находится в подкаталоге pts для каталога /dev. Мы можем использовать про извольные файловые команды и операции для работы с этим файлом: ср, перенаправление вывода с помощью операции >, mv, In, rm, cat, Is. Команда ср читает данные из обычного файла /etc/motd и записывает их на устройстве /dev/ pts/2, что приводит к отображению содержимого исходного файла на экране. При записи данных в файл устройства происходит передача данных на устройство. На следующей строке в данном примере показывается, как производится передача результатов работы команды who с помощью оператора перенаправления > в файл /dev/pts/2. После этого дан ные отображаются в символьном виде на указанном экране1.
5.2.4 Свойства файлов устройств У файлов устройств имеется большая часть тех же свойств, что есть у дисковых файлов. В выводе результатов работы команды Is, что представлен выше, видно, что файл /dev/pts/ 2 имеет inode 4, права доступа: rw~w—w-, счетчик ссылок равен 1, собственник файла bruce, группа-tty, время последней модификации Jul 18 03:25. Обозначение типа файла “с”. Этим обозначением показывается, что такой файл в действительности является устройством, относительно которого происходит побайтная пересылка данных. Права доступа выглядят несколько странными, а вместо размера файлов мы видим выражение 136, 2. Что означает это выражение? Файлы устройств и размер файла. Обычные дисковые файлы содержат какое-то количе ство байтов данных. Число байтов в дисковом файле называют размером файла. Файл устройства - это соединение, а не контейнер. Клавиатуры и мышь не хранят, сколько было нажатий на клавиши или на кнопки мыши. В inode файла устройства хранится не размер файла и распределение его по памяти, а указатель на подпрограмму в ядре. Такая подпро грамма ядра, которая получает данные от устройства и передает данные на устройство, называется драйверам устройства. 1. Или в коде Брайля, или воспроизводится звук.
5.2. Устройства подобны файлам
169
В нашем примере с файлом /dev/pts/2 программный код, который перемещает данные меж ду системой и терминалом (туда и обратно). Это подпрограмма под номером 136 в табли це драйверов. Для этой подпрограммы при вызове задается целочисленный аргумент. В случае работы с файлом /dev/pts/2 значением аргумента будет 2. Эти два номера, 136 и 2, называют старший номер и младший номер устройства. Старший номер определяет, какая подпрограмма будет управлять конкретным устройством. Значение младшего номера бу дет передаваться этой подпрограмме. Файлы устройств и права доступа. У каждого файла имеются разряды, с помощью которых задаются права на чтение, запись и исполнение. Какой смысл будет в использова нии этих разрядов прав доступа, когда речь идет не о файле, а о файле устройства? При по пытке записи данных в файл устройства данные передаются устройству. Поэтому право на запись означает право на передачу данных устройству. В нашем текущем примере собст венник файла и члены группы tty имеют право писать на терминале, но только собствен нику файла разрешено читать данные с терминала. При чтении из файла устройства, аналогично чтению из обычного файла, получают данные из файла. Ввод данных с терми нала заключается в нажатии пользователем на клавиши клавиатуры. Если пользователи, не являясь собственниками файла терминала, получат право на чтение из файла/dev/pts/2, то они смогут читать символы, которые будут нажиматься на клавиатуре. Но при чтении данных с клавиатуры другого пользователя могут возникнуть проблемы. С другой стороны, запись символов на терминал другого пользователя является целью ко манды write.
5.2.5. Разработка команды mite Еще задолго до появления средств обмена сообщениями и всевозможных chat rooms (Без перевода! - Примеч. пер.) Пользователи в Unix беседовали с друзьями, которые нахо дились за другими терминалами, с помощью команды write:
$ man 1 write WRITE( 1) Linux Programmer’s Manual WRITE( 1) NAME write - send a message to another user SYNOPSIS write user [ttyname] DESCRIPTION Write allows you to communicate with other users by copy-ing lines from your terminal to theirs. When you run the write command, the user you are writing to gets a message of the form: Message from yourname@yourhost on yourtty at hh:mm Any further lines you enter will be copied to the speci-fied user’s terminal. If the other user wants to reply, they must run write as well. When you are done, type an end-of-file or interrupt char-acter. The other user will see the message EOF indicating that the conversation is over.
170
Управление соединениями. Изучение stty
Версия команды write, следующая далее, не будет посылать сообщение “Message from” vi требует имени файла для терминала (ttyname), а не пользовательского имени собеседника:
Г writeO.c ж * * * * * *
цель: посылка сообщений на другой терминал метод: открыть другой терминал для вывода, затем произвести копирование stdin на другой терминал представление: терминал, который воспринимается как файл и поддерживает обычный ввод/вывод обращение: writeO ttyname
7 #include <stdio.h> «include main(int ac, char *av[])
{ int fd; char buf[BUFSIZ]; Г проверка аргументов */ if (ас != 2){ fprintf(stderr/’usage: writeO ttyname\n"); exit(1);
}
Г открытие устройств */ fd = open(av[1], О WRONLY); if(fd==-1){ perror(av[1]);exit(1);
}
Г цикл, пока не будет признак EOF на входе */ while(fgets(buf, BUFSIZ, stdin) != NULL) if (write(fd, buf, strien(buf)) == -1) break; close(fd);
} Тщательно проанализируйте эту программу и попытайтесь найти в ней специальные сред ства для установления соединения вашей клавиатуры с экраном другого пользователя. Их нет. В этом примере программы write производится копирование строк из одного файла в другой. Эта простая программа и примеры в предшествующем разделе показывают, что к терминалам, а также ко всем устройствам, которые присоединены к Unix-машине, мож но обращаться точно так же, как к дисковым файлам.
5.2.6. Файлы устройств и Inodes Как работать с файлами устройств? Как в файловой системе Unix inodes и блоки данных поддерживают идею файлов устройств? На рисунке 5.1 показаны соединения.
5.3. Устройства не похожи на файлы
171
Каталог - это список имен файлов и номеров inode. В каталоге нет ничего такого, что го ворило бы о принадлежности имени дисковому файлу или принадлежности имени устройству. Разница между типами файлов проявляет себя на уровне inode. Каждый номер inode - это ссылка на структуру в таблице inode. Каждая такая структура может быть либо inode дискового файла, либо inode файла устройства. Тип inode записы вается в поле типа в элементе st_mode структуры struct stat. В inode дискового файла находятся указатели на блоки на диске, где содержатся данные. В inode файла устройства находится указатель на таблицу подпрограмм ядра. С помощью старшего номера указывается, где нужно искать программный код, с помощью которого можно будет получать данные от устройства. Рассмотрим, например, как работает системный вызов read. Ядро находит inode для фай лового дескриптора. С помощью inode ядро узнает о типе файла. Если это дисковый файл, то ядро будет получать данные с использованием списка распределения блоков. Если это файл устройства, то ядро читает данные с помощью обращения к коду read в составе драй вера для этого устройства. Подобная логика поддерживается и в отношении других опера ций - open, write, lseek, close.
5.3. Устройства не похожи на файлы Внешне дисковые файлы и файльгустройсхв похожи. Оба имеют имена и обладают свой ствами. Системный вызов open создает соединение файлами и с устройствами. Но соеди нение с дисковыми файлами отличается от соединения с терминалом. На рисунке показан процесс, у которого имеются два файловых дескриптора. Один определяет соединение с дисковым файлом, а другой определяет соединение с пользователем за терминалом.
172
Управление соединениями. Изучение stty
У нас уже есть определенное представление о структуре этих соединений. В соединении с дисковым файлом обычно присутствуют буферы ядра. Данные, которые передаются от процесса к диску, накапливаются в буферах и позже передаются из буферов в память ядра. Буферирование является атрибутом соединения с диском. Соединения с терминалами имеют отличия. Процессы, которые желают послать данные на терминалы, хотят, чтобы это происходило максимально быстро. Соединение с терминалом или модемом также имеет атрибуты. Для последовательного соединения это скорость передачи, биты четности, опредленные значения стоп-битов. Символы, которые вы набираете, на клавиатуре, обычно отображаются на экране. Но ино гда, например при наборе вашего пароля, символы набираются без эхоотображения. Эхоотображение символов не является частью клавиатуры и не является частью действий, вы полняемых в программе. Эхоотображение - это атрибут соединения. Соединения с диско выми файлами не имеют таких атрибутов.
5.3.1. Атрибуты соединения и контроль В Unix поддерживается подобие файлов и устройств, когда нужно это подобие. И прини мается во внимание их различие, когда в этом есть необходимость. Соединение с диско вым файлом отличается от соединения с модемом. Обратимся к атрибутам соединений: 1. Какие атрибуты есть у соединения? 2. Как можно проверить текущие атрибуты? 3. Как можно изменять текущие атрибуты? Далее мы рассмотрим два примера: соединения с дисковыми файлами и соединения с терминалами.
5.4. Атрибут дисковых соединений
173
5.4. Атрибуты дисковых соединений Системный вызов open создает соединение между процессом и дисковым файлом. Это со единение имеет несколько атрибутов. Рассмотрим более подробно два атрибута и обменя емся мнениями о других.
5.4.1. Атрибут 1: Буферизация На следующей диаграмме изображен файловый дескриптор в виде двух каналов, которые присоединены к обрабатывающему устройству. Таким обрабатывающим устройством является ядро, которое выполняет буферизацию и другие задачи по обработке данных. Внутри этого ящика находятся переменные, с помощью которых определяется, какие вы полнять шаги по обработке данных. Картинка выглядит так:
Вы можете модифицировать действие файлового дескриптора, изменяя значения этих управляющих переменных. Например, вы можете выключить дисковую буферизацию^ ис пользуя для этого простую, трехшаговую процедуру.
Сначала выполним системный вызов, чтобы скопировать управляющие переменные из файлового дескриптора в ваш процесс. Далее модифицируем копию ваших управляющих переменных. Новые значения установок помещаются в состав обрабатывающего кода. И теперь ядро обрабатывает данные в соответствии с новыми значениями установок. Ниже представлен код, который воспроизводит в программном виде эти три шага:
174
Управление соединениями. Изучение stty #include int s; // установки s = fcntl(fd, F_GETFL); // получить флаги s |= 0_SYNC; // установить бит SYNC result = fcntl(fd, FSETFL, s); // установить флаги if (result == -1) // если ошибка perror( "setting SYNC"); // отчетность
Атрибуты файлового дескриптора кодируются с помощью битового представления целого числа. Системный вызов fcntl позволяет вам получить контроль над файловым дескрип тором с помощью операций чтения и записи этого целого числа:
fcntl НАЗНАЧЕНИЕ
Управление файловыми дескрипторами
INCLUDE
#include < fcntl.h > #indude #include <sys/types.h>
ИСПОЛЬЗОВАНИЕ
int result = fcntl(int fd, int cmd); int result = fcntl(int fd, int cmd, long arg); int result = fcntl(int fd, int cmd, struct flock *lockp);
-
АРГУМЕНТЫ
fd - файловый дескриптор,который контролируется cmd - операция, которую нужно выполнить arg - аргументы для операции lock - информация о блокировке
КОДЫ ВОЗВРАТА
-1 - при ошибке Другие значения зависят от операции
Системный вызов fcntl выполняет действие cmd над открытым файлом, который опреде лен дескриптором fd. Для действия cmd можно задавать аргументы arg. В нашем примере с помощью действия F_GETFL получаем текущий набор битов (также называют флаги). Этот набор флагов помещается в переменную s. Оператор поразрядного или устанавливает бит OSYNC. Тем самым с помощью этого бита устанавливается требование к ядру о том, что возврат из вызова write должен произойти только после окончания записи на реальное устройство. Следовательно, действие по умолчанию не выполняется, когда возврат из вы зова происходит сразу после того, как данные будут скопированы в буфер ядра. Наконец, мы должны передать модифицированные значения установок обратно ядру. Мы определяем действие F_SETFL как второй аргумент в системном вызове и указываем с помо щью третьего аргумента, какие модифицированные значения следует установить в ядре. Такая трехшаговая процедура - чтение текущих установок из ядра в переменную, изменение значений считанных установок, помещение этих установок обратно в ядро - является стан дартным средством для чтения и модификации атрибутов соединений в Unix. При установке 0_SYNC снижается эффективность от буферирования в ядре. Поэтому необходимы убедительные причины, которые вынуждают отключить буферирование.
5.4.2. Атрибут 2: Режим Auto-Append Другим атрибутом файлового дескриптора является режим auto-append. Режим auto-append полезен для файлов, в которые производится одновременная запись несколькими процессами.
5.4. Атрибуты дисковых соединений
175
Почему полезен режим Auto-Append Рассмотрим журнал wtmp. В журнале wtmp сохраняется история всех вхождений в систему и выходов из системы. Когда пользователь входит в систему, то программа login добавляет запись в конец файла wtmp. Когда пользователь выходит из системы, то система добавляет запись о выходе в конец файла wtmp, этого своеобразного дневника системы. Как в днев нике человека, так и здесь каждая новая запись должна добавляться в конец имеющегося текста.
А не можем ли мы использовать Iseek, чтобы добавлять записи в конец файла? Рассмотрим следующую логику для login:
Системный вызов lseek устанавливает текущую позицию на конец файла, а затем систем ный вызов write добавляет входную запись к файлу. Что плохое может здесь произойти? Что будет, если два человека входят в систему в одно и то же время? Обратимся к ри сунку 5.6, на котором изображено распределение времени обработки.
Файл wtmp изображен в середине рисунка. Слева изображена временная ось, на которой находятся четыре временных отметки. Последовательность действий при вхождении пользователя А представлена слева, а последовательность действий при входе пользова
176
Управление соединениями. Изучение stty
теля В приведена справа. Пока все нормально? Важным обстоятельством является тот факт, что Unix является системой разделения времени, и что в этой процедуре требуется выполнение двух отдельных шагов: lseek и write. Теперь посмотрим в замедленном темпе, как все будет происходить: time 1 — Процесс В ищет конец файла. time 2 — Интервал времени для В закончился. Процесс А ищет конец файла. time 3 — Интервал времени для А закончился. Процесс В производит запись. time 4 — Интервал времени для В закончился. Процесс А производит запись. Таким образом, запись от процесса В будет потеряна, поскольку произойдет ее перезапись процессом А. Такая ситуация называется условием гонок. (Иногда называют условием состязаний. Примеч. пер.) Окончательный результат обработки файла, который разделяется этими дву мя процессами, будет зависеть от того, как будет спланировано развитие этих процессов. Если выполнить даже небольшие изменения в распределении действий во времени, то это может привести к потере записи о входе пользователя А. Или все может произойти так, что ничего не будет потеряно. Как можно аннулировать это условие гонок? Есть множество вариантов, чтобы избе жать условий гонок. Условия гонок представляют собой критическую проблему в области системного программирования. Мы будем многократно возвращаться к этой теме. В нашем конкретном случае можно воспользоваться средством, которое находится в ядре и которое обеспечивает простое решение проблемы: поддержка режима auto-append. Если будет установлен бит 0_APPEND для файлового дескриптора, то это приведет к тому, что в системном вызове write автоматически будет включен lseek для выставления на конец файла. В этом фрагменте кода устанавливается режим auto append и затем вызывается write: #include ints; //установки s = fcntl(fd, FJ3ETFL); // получить флаги s |= (JAPPEND; // установить бит APPEND result = fcntl(fd, F_SETFL, s); //установить флаги if (result — -1) // если ошибка perrorf’setting APPEND"); // сообщение else write(fd, &rec, I); // записать в конец файла
Атомарные операции. С важным термином условия гонок связан другой важный термин-атомарная операция. Вызовы lseek и write представляют собой раздельные во времени системные вызовы. Ядро может прервать процесс в точке, которая располо жена между этими двумя вызовами. Когда установлен бит 0_APPEND, то ядро комби нирует одну атомарную операцию из lseek и write. Две операции объединяются в одну неделимую операцию. (При выполнении неделимой операции ядро уже не может ее прервать. - Примеч. пер.)
5.4. Атрибуты дисковых соединений
177
5.4.3. Управление файловыми дескрипторами с помощью системного вызова open O SYNC и OAPPEND - два атрибута файлового дескриптора. Но их гораздо больше. Мы рассмотрим другие установки в последующих главах. В документации системногр вызова fcntl представлен список всех опций и операций, которые поддерживаются для вызова в вашей системе. Установка атрибутов файлового дескриптора с помощью fcntl не является единственной возможностью. Часто при открытии файлов вам известно, какие нужно сделать установки. Системный вызов, open дает вам возможность определить биты атрибутов файлового де скриптора, используя для этого часть второго аргумента при обращении к вызову. Напри мер, с помощью вызова. fd = open(WTMP_FILE, 0_WR0NLY!0_APPEND|0„SYNC);
будет открыт на запись файл wtmp (В примере, в системном вызове open указано имя файла WTMP FILE. - Примеч. пер.) с установленными битами 0_APPEND и O SYNC. Второй аргумент в системном вызове open используется не только для указания режима открытия: на чтение, на запись, на чтение/запись. Например, вы можете запросить при выполнении open, чтобы файл был предварительно создан. Это делается с помощью флага 0_CREAT. Следующие два вызова будут эквивалентны: fd = creat(filename, permission_bits); fd = open(filename, 0^CREAT|0.TRUNC|0_WR0NLY, permission_bits);
Почему существует системный вызов creat, если ту же работу можно выполнить с помо щью системного вызова open? В старых системах с помощью open происходило только открытие файлов, а с помощью creat создавался новый файл. Позже системный вызов open был модифицирован и стал поддерживать большее количество флагов, в том числе и оп цию по созданию файла.
Другие флаги, которые поддерживаются в open O.CREAT OJRUNC 0_EXCL
Создать файл, если он не существовал. Смотри 0_EXCL. Если файл существует, то следует уничтожить его содержимое -установить размер файла равным 0 (транкатенировать). Флаг OJEXCL предполагает предотвращение попытки создания одного и того же файла двумя процессами. Если указанный файл существует и установлен флаг 0_CREAT, то системный вызов возвратит -1.
Комбинация флагов 0_CREAT и 0_EXCL может быть использована для устранения сле дующей ситуации гонок. Что произойдет, если два процесса попытаются одновременно создать один и тот же файл? Например, что будет, если два процесса пожелают вести запи си в файл wtmp, но потребуется создать этот файл, если он до этого не существовал? Программа определяет, существует ли файл с помощью вызова stat. Затем вызывается creat, если обнаруживается, что файл не существует. Проблема может возникнуть тогда, если процесс будет прерван в точке между stat и creat. Флаги 0_EXCL/0_CREAT позволяют объ единить эти два системных вызова в атомарную операцию. Несмотря на наши старания, в ряде важных случаев эта комбинация работать не будет. Надежным альтернативным решением будет использование link. В упражнениях есть при мер на эту тему.
178
Управление соединениями. Изучение stty
54. 4. Итоговые замечания о дисковых соединениях Ядро передает данные между дисками и процессами. Код в ядре, который выполняет такие передачи, имеет много опций. Программа может использовать системные вызовы open и fcntl с тем, чтобы управлять (контролировать) выполнением внутренней работой по пересылке этих данных.
5.5. Атрибуты терминальных соединений Системный вызов open создает соединение между процессом и терминалом. Рассмотрим более детально ряд атрибутов соединения с терминалом.
5.5.1. Терминальный ввод/вывод не такой\ как он кажется Соединение между терминалом и процессом выглядит достаточно простым. Вы можете передавать байты данных между устройством и процессом, используя getchar и putchar. Абстракция потока данных делает похожей такую систему пересылки на систему, где кла виатура и экран подключены прямо к процессу:
5.5. Атрибуты терминальных соединений
179
Простой эксперимент показывает, что эта модель не является полной. Рассмотрим программу: /* listchars.c * цель: представление в списке тех символов, которые * поступают на вход программы * вывод: одна пара в строке, значения символа в формате char и в ascii коде * ввод: stdin,пока не появится на входе символ Q * замечание: программа полезна для показа, что присутствуют средства * буферирование/редактирование
V «include <stdio.h> mainQ
{ int с, n = 0; while((c = getchar()) != 'Q') printff'char %3d is %c code %d\n", n++, c, c);
} Программа производит посимвольную обработку, т. е. читает очередной символ, а затем вы водит значение счетчика цикла, сам символ и его внутренний код. Откомпилируйте и запустите программу на исполнение. После запуска вы можете получить такой результат: $./listchars hello char 0 is h code 104 char 1 is e code 101 char 2 is I code 108 char3islcode 108 char 4 is о code 111 char 5 is code 10 Q
$ Что происходит? Если коды символов поступают непосредственно с клавиатуры на getchar, то мы должны были бы получать желаемый результат сразу после нажатия на каждую клавишу. Но вместо этого нам придется нажать на пять клавиш при наборе слова hello, а потом нажать на клавишу Enter. Только тогда программа обработает введенные симво лы. Ввод оказался буферируемым. Подобно данным, которые передаются на диск, поток байтов при передаче с терминала сохраняется в каком-то месте. Иногда listchars может вести себя по-другому. При нажатии на клавиши Enter или Return обычно посылается код ASCII, равный 13, что соответствует символу возврат каретки (carriage return). Из вывода программы listchars следует, что код ASCII, равный 13, при передаче был заменен на код 10, который соответствует символу line feed или newline
(перевод строки). Третий вид обработки, который влияет на программный вывод. Программа listchars при выводе посылает символ перехода на новую строку (\п) в конце каждой строки. Код пере хода на новую, строку указывает на необходимость перемещения курсора на одну строку, но не указывает на необходимость возврата курсора в самую левую колонку строки. Код 13 (carriage return, т. е. возврат каретки) требует возврата курсора в самую левую КОЛОНКУ.
180
Управление соединениями. Изучение stty
Попросите вашего дедушку, чтобы он рассказал вам о блестящей ручке, которая находи лась с левой стороны каретки пишущей машинки. Вы узнаете, что с ее помощью происхо дил возврат каретки в левую исходную позицию листа бумаги. Результат исполнения listchars показал, что в файловом дескрипторе должен быть где-то обрабатывающий уровень. На рисунке 5.9’проиллюстрировано действие этого уровня.
На этом примере представлено три вида обработки: 1. Процесс не принимает данных до тех пор, пока пользователь не нажмет на клавишу Return. 2. Пользователь нажимает Return (код ASCII 13), но процесс воспринимает символ newline (код ASCII 10). 3. Процесс посылает символ newline и терминал принимает пару символов: Return-Newline. Соединения с терминалами имеют сложный набор свойств и шагов по обработке данных.
5.5.2. Драйвер терминала Соединение терминала с процессом выглядит так, как показано на рисунке 5.10.
5.5. Атрибуты терминальных соединений
181
Набор подпрофамм ядра, которые обрабатывают данные, передаваемые между процес сом и внешним устройством, называют драйвером терминала или драйвер tty 2. Драйвер содержит много установок, с помощью которых производится управление его работой. Процесс может читать, модифицировать и сбрасывать значения этих управляющих фла гов драйвера. 5.5.3. Команда stty Команда stty предоставляет пользователю возможность читать и изменять значения уста новок в драйвере терминала. Использование команды stty для работы с установками драйвера дисплея. Выходные результаты работы команды stty будут выглядеть приблизительно так: $ stty speed 9600 baud; line = 0;
$ stty -all speed 9600 baud; rows 15; columns 80; line = 0; intr = X; quit = Л\; erase =A?; kHI = AU; eof = AD; eol = ; eol2 = ; start = AQ; stop = AS; susp = AZ; rprnt = AR; werase = AW; Inext = AV; flush = A0; min = 1; time = 0; -parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk brkint ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iucic -ixany imaxbel opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nIO crO tabO bsO vtOffO isig icanon iexten echo echoe echok -echonl -nofish -xcase -tostop -echoprt echoctl echoke
По умолчанию команда выдает короткий листинг. При использовании опции -all выдается список, в котором представлено много установок. Некоторые установки представлены как переменные, имеющие некоторые значения. Некоторые же установки являются булевски ми константами. Например, установки baud rate (скорость в бодах), число строк и колонок на экране имеют числовые значения. А такие установки, как intr, quit и eof, имеют символь ные значения. Наконец, такие значения, как icrnl, -olcuc и onlcr, представляют собой флаги, которые либо установлены, либо сброшены. Каково назначение всех этих установок? Установка icrnl является аббревиатурой от Input: convert Carriage Return to NewLine (Ввод: преобразование Carriage Return в NewLinej. Она обозначает действие, которое выполнял драйвер в нашем предшествующем примере. Аббревиатура onlcr означает Output: add to NewLine a Carriage Return (Вывод: добавить Carriage Return к NewLine). Знак минус, который стоит перед атрибутом, указывает на то, что данная операция выключена. Например, установка -olcuc означает невозможность выполнения действия типа Output: convert LowerCase to UpperCase (Вывод: преобразова ние из нижнего регистра в верхний). Многие старинные терминалы печатали только заглавными символами, поэтому преобразование вывода в заглавные символы было для них полезным. Изменение установок драйвера с помощью команды stty. Вот несколько примеров использования команды stty по изменению установок драйвера: 2. tty - это осталось от ссылок на первые'печатающие терминалы, которые производились Teletype Corporation.
182
Управление соединениями. Изучение stty $ stty erase X # сделать 'X' клавишей стирания $ stty -echo # набор без эхо отображения $ stty erase @ echo # несколько установок терминала
В первом примере мы используем команду stty для изменения клавиши, с помощью которой можно корректировать ошибки при вводе. Обычно такое действие по стиранию введенного символа закреплено за клавишей backspace или delete. Но вы можете закре пить выполнение такого действия за любой клавишей. Во втором примере мы отключили воспроизведение символа при нажатии на клавишу. Когда вы набираете пароль при входе в систему, то при наборе символов пароля эти символы не воспроизводятся на вашем экране. А дальше, при нажатии на клавиши, символы опять будут отображаться. Выключение отображения символов приводит к тому, что при наборе вы не будете видеть на экране, что набираете. В третьем примере мы используем команду stty для изменения сразу нескольких установок. Мы заменили символ стирания на символ и опять включили режим echo (режим с эхоотображением при наборе). Как работает команда stty? Можем ли мы написать команду stty?
5.5.4. Программирование драйвера терминала: Установки Драйвер терминала поддерживает дюжины операций, которые он может выполнить над данными, передаваемые с его помощью. Эти операции сгруппированы по четырем кате гориям: Входная Задается, что драйвер делает с символами, которые поступают к нему с терминала Выходная Задается, что драйвер делает символами, которые он выдает на терминал Управляющая Задается, как представлены символы - число разрядов, четность, стоп-биты и т. д. Локальная Задается, что драйвер делает, когда символы находятся внутри драйвера
Входная обработка включает в себя преобразования представления символов из нижнего регистра в верхний, сбрасывание high bit, преобразование управляющего символа carriage returns в newlines. При выходной обработке символы табуляции заменются на последова тельность из пробелов, происходит преобразование управляющего символа newlines в car riage returns, происходит преобразование символов из нижнего регистра в верхний. Управ ляющая обработка включает в себя even parity, odd parity (проверку на четность или нечет ность), работу со стоп-бйтами. Локальная обработка включает в себя установление для пользователя режима эхо отображения и буферирование ввода до тех пор, пока пользова тель не нажмет на клавишу Return. Кроме включений/выключений установок, драйвер поддерживает список ключей (клавиш) со специальным назначением. Например, пользователи могут для удаления сим вола нажимать на клавишу backspace. Драйвер отслеживает нажатия на эту клавишу и производит действие по этому ключу стирания. Кроме того, драйвер отслеживает нажа тия еще ряда других управляющих символов. В документации на команду stty приводится список большинства установок и управ ляющих символов.
5.5.5. Программирование драйвера терминала: Функции Изменение установок в драйвере терминала производится аналогично тому, как делаются изменения установок для соединений с дисковым файлом: (a) Получить атрибуты от драйвера. (b) Модифицировать какие-то атрибуты, которые вы желаете, fcl Пеоелать эти молиАипиоованные атоибуты обоатно лоайвеоу.
5.5. Атрибуты терминальных соединений
183
Например, в последующем программном коде включается режим эхоотображения для со единения: #include struct termios attribs; /* структура, где хранятся атрибуты */ tcgetattr(fd, Ssettings); /* получить атрибуты драйвера */ settings.cjflag |= ECHO; /* включить бит ECHO в наборе флагов */ tcsetattr(fd, TCSANOW, &settings); /* передать атрибуты обратно драйверу */
Общая процедура изменения атрибутов изображена на рисунке 5.11:
Библиотечные функции tcgetattr и tcsetattr обеспечивают доступ к драйверу терминала. Обе функции работают с установками в структуре struct termios. Детали функций следующие:
tcgetattr НАЗНАЧЕНИЕ
Чтение атрибутов драйвера терминала
INCLUDE
#include #include < unistd.h >
ИСПОЛЬЗОВАНИЕ int result = tcgetattrfint fd, struct termios *info); АРГУМЕНТЫ
fd - файловый дескриптор для терминала info - указатель на структуру termios
КОДЫ ВОЗВРАТА -1 при ошибке О - при успехе
Функция tcgetattr копирует текущие установки драйвера терминала, которому сопоставлен дескриптор открытого файла устройства fd, в структуру info. tcsetattr * НАЗНАЧЕНИЕ INCLUDE
Установить атрибуты драйвера терминала #include #include < unistd.h >
ИСПОЛЬЗОВАНИЕ int result = tcsetattr(int fd, int when, struct termios *info);
184
Управление соединениями. Изучение stty
tcsetattr АРГУМЕНТЫ
fd - файловый дескриптор для терминала info - указатель на структуру termios when - когда изменять установки
КОДЫ ВОЗВРАТА -1 - при ошибке 0 - при успехе Функция tcsetattr копирует установки драйвера из структуры, на которую указывает info, и передает их драйверу терминала, которому сопоставлен файловый дескриптор fd. С по мощью аргумента when при обращении к функции указывается, когда следует модерни зировать установки драйвера. Допустимы следующие значения для аргумента when: TCSANOW
Немедленно модернизировать установки драйвера. TCSADRAIN
Ждать, пока все выходные данные, которые собраны в очереди в драйвере, не будут пере даны на терминал. TCSAFLUSH
Ждать, пока все выходные данные, которые собраны в очереди в драйвере, не будут пере даны на терминал. Далее сбросить все поступившие входные данные. Затем выполнить изменения установок.
5.5.6. Программирование драйвера терминалов: Флаги В типе данных struct termios содержится несколько наборов флагов и массив управляющих символов. Во всех версиях Unix включены следующие поля: . struct termios
{ tcflagj c_iflag; /* флаги режима ввода */ tcflag_t coflag; Г флаги режима вывода */ tcflag_t ccflag; /* флаги управляющего режима */ tcflagj cjflag; /* флаги локапьного режима */ cc_t c_cc[NCCS]; j* управляющие символы */ speedt cjspeed; f скорость ввода */ speed t с ospeed; /* скорость вывода */
}; Скорость передачи в бодах для входных и выходных потоков данных хранится в полях c_ispeed и c_ospeed. Распределение разрядов в каждом из наборов флагов показано на рисунке 5.12. Первые четыре члена - это наборы флагов. Каждый набор флагов состоит из разрядов, которым сопоставлены операции в этой группе. Например, член cjflag содержит разряд со значением INLCR. Член ccflag содержит бит проверки на нечетность (odd parity), который называется маской PARODD. Все эти маски определены в termios.h. Когда вы обращаетесь к текущим атрибутам из драйвера в struct termios, то все значения в этой структуре вам доступны для проверки и модификации.
5.5. Атрибуты терминальных соединений
c_iflag
185
I 1 Т УТ ГГ 1 ! I I I I I I I 1 МЮН
г ог„о '_З ’ зс йо гО я а » о ё » о ы ю
“ “ ■ ■ “ " т зда*
c_oflag
I Г ПТГГТ I 1 I I I 1 1Л
c_cflag
[ in "i i" i... i т гтт-тт
c_lflag
i :i ill г пп гглллт
Рисунок 5.12 <
< M
м
2! 73
I
О
Разряды и символы в составе членов termios
Член с_сс - это массив управляющих символов. В этом массиве хранятся символы тех кла виш, при нажатии на которые выполняются различные управляющие функции. Каждый элемент в массиве определяется константой из файла termios.h. Например, присвоение значения вида attribs.c_cc[VERASE]-\b’ будет означать для драйвера, что он будет рас сматривать ключ backspace как символ стирания.
Программирование драйвера терминала: Битовые операции Теперь мы знаем, как получить установки драйвера и как их передать драйверу обратно. Рассмотрим теперь технику модификации атрибутов драйвера. Каждый атрибут - это бит в составе набора флагов. Маски для атрибутов определены в файле termios.h. Для проверки значения некоторого атрибута вы должны замаскировать набор флагов маской для этого бита. Для установления значения некоторого атрибута вы должны установить соответствующий бит. Для сброса значения атрибута, вы должны сбросить соответствующий бит. В таблице показаны эти действия. Действие
Код
Проверка бит
if(flagest & MASK)...
Установка бита
flagset | = MASK
Очистка бита
flagset &= -MASK
186
Управление соединениями. Изучение stty
5.5.7. Программирование драйвера терминала: Примеры программ Пример: echostate.c — показать состояние бита echo Наша первая программа будет сообщать нам об установке символов режима эхо отображе ния. Производится чтение установок, проверка бита и вывод результатов проверки. Г echostate.c * сообщает, установлен ли бит echo у драйвера терминала, файловый дескриптор fd которого равен О * показывает, как читаются атрибуты из драйвера терминала и как проверяются значения бита
7 «include <stdio.h> «include main()
{ struct termios info; int rv; rv = tcgetattr(0, &info); /* читать значения атрибутов драйвера */ if (rv== -1){ perrorftcgetattr"); exit(1);
} if (info.c.lflag & ECHO) printf(" echo is on, since its bit is 1\n”); else printf(" echo if OFF, since its bit is 0\n");
} В этой программе читаются атрибуты терминала через файловый дескриптор 0. Нулевой файловый дескриптор принадлежит стандартному вводу, т. е. такой файловый дескриптор обычно устанавливается для клавиатуры. Далее показаны команды компиляции и запуска программы на исполнение: $ сс echostate.c -о echostate $./echostate echo is on, since its bit is 1
$ stty -echo $./echostatr: not found $ echo is OFF, since its bit is 0
Пример показывает нам, что по команде stty -echo устанавливается запрет на эхоотображе ние в драйвере. Если пользователь будет набирать тексты двух каких-то команд после ука занной команды, то тексты этих команд отображаться на экране не будут. Но при этом ре зультаты выполнения этих команд отображаются на экране.
Пример: setecho.c - изменить состояние бита echo Наша вторая программа может переключать режим эхоотображения для клавиатуры. Если при обращении к программе аргумент будет начинаться с символа “у”, то флаг терминала echo должен быть установлен. В противном случае флаг echo будет сброшен. Программа выглядит так:
5.5. Атрибуты терминальных соединений
187
[* setecho.c ’ обращение: setecho [у|п] х показывает, как читать, изменять и переустанавливать атрибуты терминала
7 «include <stdio.h> «include «define oops(s.x) {perror(s); exit(x);} main(intac, char*av[])
{ struct termios info; if (ac == 1) exit(O); if (tcgetattr(0,&info) == -1) /* получить атрибуты */ oops("tcgettattr", 1); if (av[1][0] == У) info.c_lflag j= ECHO; /* включить бит */ else info.c_lflag &= -ECHO; /* выключить бит */ if (tcsetattr(0,TCSANOW,&info) == -1) /* установить атрибуты */ oops("tcsetattr",2);
} Проверим и запустим на исполнение наши две программы и выполним обычную команду stty: $ echostate; setecho n; echostate; stty echo echo is on, since its bit is 1 echo is OFF, since its bit is 0
$ stty -echo; echostate; setecho y; setecho n echo is OFF, since its bit is 0
В первой командной строке мы использовали программу setecho для выключения режима эхоотображения. Далее мы использовали команду stty, чтобы восстановить режим эхоотображения. Драйвер и установки драйвера хранятся в ядре, а не в процессе. Процесс может изме нять установки драйвера. Другой процесс также может читать или изменять установки.
Пример: showtty.c - отобразить набор атрибутов драйвера Мы можем применить технику, которая была использована в программах setecho.c и echos tate.c и разработать полную версию команды stty. Драйвер терминала обрабатывает три ви да установок: специальные символы, числовые значения и битовые значения. В програм ме showtty находятся функции для отображения каждого из этих типов данных. Далее сле дует код программы: /* showtty.c * отображает некоторые текущие установки терминала
7 «include «include main()
<stdio.h>
8
Управление соединениями. Изучение stty struct termios ttyinfo; f* эта структура содержит информацию о терминале */ if (tcgetattr(0, &ttyinfo) = -1){f* получить информацию */ perrorfcannot get params about stdin"); exit(1);
} /* show info */ showbaud (cfgetospeed(&ttyinfo)); /* получить + показать baud rate*/ printf(”The erase character is ascii %d, Ctrl-%c\n", ttyinfo.c_cc[VERASE], ttyinfo.c_cc[VERASE] -1 +’A'); printffThe line kill character is ascii %d, Ctrl-%c\n", ttyinfo.c_cc[VKILL], ttyinfo.c_cc[VKIll]* 1 +'A ’); show some flags(&ttyinfo); /* показать misc. flags */
} showbaud(int thespeed)
Г *
вывод скорости по-английски
7 { printf("the baud rate is"); switch (th£sp6ed){ case B300: case B600: case В1200 case В1800 case B2400 case B4800 case B9600: default:
printf("300\n"); printf("600\n”); printf(”1200\n”); printf("1800\n"); printf(”2400\n"); printf{”4800\n”); printf("9600\n");
break; break; break; break; break;
printf("Fast\n");
break;
break;
} ) struct flaginfo {int fl_value; char *fl_name;}; struct flaginfo input flags[] = { "Ignore break condition", ’ IGNBRK, BRKINT, "Signal interrupt on break",, IGNPAR, "Ignore chars with parity errors", PARMRK, "Mark parity errors", INPCK, "Enable input parity check", "Strip character", ISTRIP, "MapNLtoCR on input", INLCR, "Ignore CR", IGNCR, ICRNL, "Map CR to NL on input", IXON, "Enable start/stop output control", "разрешить некоему символу производить рестарт вывода", Г IXANY, "Enable start/stop inputcontrol", IXOF, 0, NULL}; struct flaginfo local_flagsQ = {
5.5. Атрибуты терминальных соединений
189
ISIG, "Enable signals", ICANON, "Canonical input (erase and kill)", Г _XCASE, "Каноническое проявление upper/lower", */ ECHO, "Enable echo", ECHOE, "Echo ERASE as BS-SPACE-BS", ECHOK, "Echo KILL by starting new line", .0, NULL}; show some flags(struct termios *ttyp)
Г * * *
показывает значения двух флагов в наборе: c.iflag и c_lflag добавление флагов c_oflag и ccflag - это подпрограмма, которая добавляет новую таблицу, указанную выше, и биты, как это показано ниже
7 { show_flagset(ttyp->c_iflag, input_flags); show flagset(ttyp->c Wag, local flags);
} show flagset(int thevalue, struct flaginfo thebitnamesQ) f• . * проверят каждый битовый шаблон и выводит краткое сообщение
7 { inti; for (i=0; thebitnames[i].fl_value; i++) { printff' %s is", thebitnamesp] .ff_name); if (thevalue & thebitnamesp] .fl_value) printf("ON\n"); else printf("OFF\n“);
} } Программа showtty выводит текущее состояние семнадцати атрибутов драйвера, сопровождая вывод разъяснительным текстом. Наша программа использует массив структур для упроще ния кода. При вызове функция show_flagset задается целое число и набор флагов драйвера. В функции show_flagset циклически проверяются все биты и отображается статус каждого из них. Что потребуется добавить в нашей программе для работы с другими наборами флагов? Что еще нужно добавить в эту программу, чтобы она работала как полная версия stty?
5.5.8. Итоговые замечания по соединениям с терминалами Терминал - это устройство, которое используется человеком для связи с процессами Unix. Терминал имеет клавиатуру, с которой процесс читает символы, и дисплей, на котором ото бражаются символы, выдаваемые процессом. Терминал - это устройство. Поэтому он пред ставлен как специальный файл в дереве каталогов. Обычно он заносится в каталог /dev. Передача и обработка данных между процессом и терминалом происходит под управле нием драйвера терминала, который является частью ядра. Этот код ядра поддерживает буферирование, редактирование и преобразование данных. Программы могут проверять значения и модифицировать значения установок этого драйвера с помощью функций tcgetattr и tcsetattr.
190
Управление соединениями. Изучение stty
Программирование других устройств: iocti
5.6.
Соединение с дисковым файлом имеет один набор атрибутов, а соединение с терминалом имеет другой набор. А что можно сказать относительно соединений с другими типами устройств? Рассмотрим пишущие CD. Перезаписываемые CD можно стирать. На CD можно прово дить запись с разными скоростями. Сканеры имеют собственный набор установок, таких как разрешение при сканировании и глубина цвета. У других типов устройств также имеются собственные наборы установок. Как прбграммист может проверять и управлять установками для устройств? С каждый файлом устройства может работать системный вызов iocti: iocti НАЗНАЧЕНИЕ
Управление устройством
INCLUDE
#include < sys/ioctl.h >
ИСПОЛЬЗОВАНИЕ
int result = iocti (int fd, int operation [, arg..])
АРГУМЕНТЫ
fd - файловый дескриптор устройства operation - операция, которую необходимо выполнить arg... - аргументы, необходимые для выполнения операции
КОДЫ ВОЗВРАТА
-1-при ошибке Другие значения зависят от устройства
Системный вызов iocti позволяет получить доступ к атрибутам драйвера устройства и к операциям над атрибутами. Драйверу соответствует файловый дескриптор fd. Каждый тип устройства имеет собственный набор свойств и iocti операций. Например, экран терминала имеет размер, который измеряют числом строк и числом колонок в строке или в пикселях. Следующий ниже код. #include <sys/ioctl.h> . void print screen_dimensions()
{ struct winsize wbuf; if (ioctl(0, TIOCGWINSZ, &wbuf) != -1){ printf("%d rows x %d cols\n", wbuf.wsjow, wbuf.ws_col); printf("%d wide x %d tall\n", wbuf.ws xpixel, wbuf.ws ypixel);
} } выполняет вывод значения размера экрана. Здесь TIOCGWINSZ - это имя кода функции, а адрес wbuf является аргументом при обращении к этой функции управления устройством. Лучшим способом изучения типов устройств и их функций является чтение заголовочного файла. В документации на устройства также приведены списки свойств и функций. Например, при обращении к справочнику Linux за документом st(4), получим детальное описание вариантов использования iocti для управления ленточным устройством SCSI.
5.7. О небо! Это файл, это устройство, это поток! В Unix файл рассматривается либо как источник данных, либо как приемник данных. Основные системные вызовы применимы в равной степени как к дисковым файлам, так и к файлам устройств. Различия возникают в действиях, которые происходят в соединени ях. В файловом дескрипторе для дискового файла со ержится ко для буферирования
Заключение
191
и присоединения данных. Файловый дескриптор для терминала содержит код, который производит редактирование, поддерживает эхоотображение, преобразование символов, а также другие операции. Мы описали каждый шаг по обработке, как атрибут соединения. Но вместо этого можно так же сказать, что соединение - это просто комбинация шагов по обработке. System V Unix, являющаяся одним из вариантов Unix, была разработана AT&T в 80-х годах. В этой системе была предложена модель потока данных. Основаная идея модели - построение последова тельности шагов обработки. Это напоминает последовательность действий при мытье авто мобиля. Сначала ваш автомобиль обрызгивают моющим раствором. Затем смывают грязь с помощью больших щеток. Далее смывают грязь с поверхности, что делают с помощью мыльного раствора из шлангов с высоким давлением. Накладывается антикоррозийный ингибитор, набрызгивается горячий воск и накладывается полировка для хромированных колпаков. Наконец, обрабатывают поверхность мягкой тканью и горячим воздухом. Все, дело сделано! Конечно, каждый этап является отдельной операцией, которую захотел выполнить владе лец автомобиля. Владелец выбрал их из последовательности тех операций, на которые в данной компании был разбит процесс помывки машины. Кроме того, вы можете отка заться от некоторых конкретных шагов. (Но, пожалуйста, не отказывайтесь от шага нане сения горячего воска!) То, что было описано, представляет собой в огрубленном виде идею модели ПОТОКОВ (STREAMS) относительно данных и атрибутов соединений. Элегантной частью потоко вой4 модели является модульность обработки. Если вы не удовлетворены драйвером терминала, который поддерживает только такие скучные операции как преобразование символов из нижнего регистра в верхний и наоборот, то вы можете разработать и инстал лировать модуль для перевода цифр в римские цифры. Итак, вы должны написать обраба тывающий модуль, который выполняет преобразования арабских цифр в римские. Вы должны были написать его в соответствии со спецификациями модуля STREAMS. Затем следует использовать специальные системные вызовы для правильной инсталляции этого модуля между шагом отделки колпака и шагом протирки мягкой тканью. Когда ваш автомобиль достигнет конца мытья, то все цифры на приборном щитке будет заменены на римские. Обратитесь к вашему справочнику за документами по теме streamio, чтобы более подроб но изучить вопросы управления свойствами соединений. ПОТОКИ используются в неко торых версиях Unix для реализации сетевых служб.
Заключение Основные идеи •
•
Ядро передает данные между процессами и теми объектами, которые находятся извне. Такими внешними объектами могут быть дисковые файлы, терминалы и периферийные устройства (принтеры, ленточные устройства, звуковые карты, мыши). Соединения с дисковыми файлами и соединения с устройствами имеют как подобия, так и отличия. Дисковые файлы и файлы устройств имеют имена, свойства и разряды прав доступа. Как для файлов, так и для устройств можно использовать стандартные файловые системные вызовы: open, read, write, close и lseek. Управление доступом к устройствам происходит с помощью разрядов прав доступа к файлам. Управление происходит точно так же, как это происходит при работе с дисковыми файлами.
192
•
•
•
Управление соединениями. Изучение stty
Соединения с дисковыми файлами отличаются от соединений с файлами устройств в методах обработки и передачи данных. Код ядра, который управляет соединениями с неким устройством, называют драйвером устройства. Процесс может читать и изменять установки в драйвере устройства с помощью системных вызовов fcntl и ioctl. Соединения с терминалами являются настолько важными, что были разработаны специальные функции tcgetattr и tcsetattr, с помощью которых можно контролировать работу драйверов терминалов. Unix команда stty предоставляет пользователю доступ к функциям tcgetattr и tcsetattr.
Визуальное заключение
Процесс использует системные вызовы read и write для извлечения данных из файлового дескриптора и помещения данных в него соответственно. Файловые дескрипторы можно устанавливать для связи с дисковыми файлами, терминалами и периферийными устройст вами. Файловый дескриптор приводит процесс к драйверу устройства. Драйвер устройст ва имеет установки.
Что дальше? Чтение данных с дисков производится достаточно просто, а вот чтение данных, которые поступают от людей, может быть достаточно изощренным, поскольку люди ведут себя весьма непредсказуемо. Программы, которые разрабатываются для чтения данных, которые поступают от людей, могут использовать свойства терминального драйвера по управлению соединением. В следующей главе мы более детально рассмотрим некоторые темы программирования в отношении пользовательских программ.
Исследования. 5.1 На Linux-машине легко читать данные, поступающие от мыши. Чтобы достичь этого, вам необходимо находиться в текстовом режиме. Находясь в shell, убедитесь в том, что не работает программа gpm. Для этого наберите gpm -к. Затем выполните cat /dev/ mouse. Теперь перемещайте мышь и нажимайте на кнопки . Команда cat будет читать
Заключение
5.2 5.3
5.4
5.5
5.6 5.7
5.8
5.9
193
данные из файла устройства. Те байты, которые будут прочитаны, будут соответство вать сообщениям о таких событиях, как нажатия на кнопки и перемещение мыши. Каково назначение бита, разрешающего исполнение для файла устройства? Изучите команду biff, чтобы получить представление об использовании этого бита. Операции над каталогами и файлы устройств. Мы обсудили, как работают операции ввода/вывода для файлов устройств. А что можно сказать относительно операций над каталогами типа In, mv, rm и т. д. Используя рисунок 5.1, объясните, каким образом будут воздействовать эти три команды на каталоги, inodes и на драйверы. rm и специальные файлы. Команда rm и лежащий в ее основе системный вызов unlink удаляют ссылку на inode. Если число ссылок на inode достигнет нуля, то ядро освобо ждает дисковые блоки и inode (Которые относятся к файлу, имя которого указано в ко манде. -Примеч. пер.) В inode устройства нет списка распределения. Файл устройст ва не содержит блоков данных. Вместо этого в inode для файла устройства содержится указатель на подпрограмму устройство-драйвер в ядре. Если вы удалите имя файла для устройства и ядро отметит соответствующий inode как свободный, то драйвер при этом останется в ядре. Как можно будет создать новый файл устройства и соединить его с устройством? (Подсказка: почитайте документацию о системном вызове mknod.) Рассмотрим условия гонок, которые возникают при присоединении данных к файлу. В обсуждении проблемы, которое было представлено в тексте книги, рассматрива лась одна из возможных последовательностей планирования. Сколько можно соста вить управляющих последовательностей в отношении двух операций для двух про цессов? Что должно происходить при выполнении каждой их этих последовательно стей? Обратитесь к коду ядра Linux и найдите там место, где можно увидеть, как проверя ется бит OAPPEND. Как реализуется автоматический переход в конец файла? Как работает системный вызов rename? Системный вызов rename является ато марной операцией. Из каких шагов состоит этот целостный вызов? Найдите код в ядре некоторого варианта Unix и рассмотрите все условия гонок и возможные конфликты, которые могут возникнуть при управлении. Комментарии, которые вы можете найти в ядре Linux, выглядят весьма неряшливо и забавно. Стандартная библиотечная функция fopen поддерживает открытие файла в режиме append (режим добавления в конец файла). Например, fopen ("data”,"а”). Как устанав ливается на вашей системе режим append - с помощью O APPEND или с помощью lseek выполняется переход в конец файла после его открытия? Найдите исходный код для функции fopen или проведите эксперимент и напишите программу, которая дваж ды открывает один и тот же файл в режиме присоединения, а затем поочередно про изводит запись в два потока. Проверка работы программы echostate с другими устройствами. Программа echos tate.c оповещала о состоянии бита echo в драйвере для файлового дескриптора 0. Используйте оператор перенаправления < для присоединения стандартного ввода к другим файлам или устройствам. Проведите такой эксперимент: echostate < /dev/tty echostate < /dev/lp echostate < /etc/passwd echostate < 'tty'
Объясните результаты, которые будут получены после работы каждой их этих команд.
194
Управление соединениями. Изучение stty
5.10 Изменение атрибутов других терминалов. Программа setecho позволяла изменять бит echo в драйвере, присоединенном к стандартному вводу. Если вы перенаправите стандартный ввод на другой терминал, то вы можете изменить бит echo для этого терминала.
Проведите такой эксперимент: (a) Войдите дважды в систему на одной и той же машине (или сразу откройте два окна). (b) Выполните в каждом окне команду tty, чтобы определить имена файлов устройств для этих двух окон. Скажем, одно из них будет связано с файлом устройства /dev/ ttyp 1, а другое - с файлом устройства /dev/ttyp2. (c) В окне ttypl выполните setecho n < /dev/ttyp2 (d) В окне ttyp2 выполните команду echostate (e) Теперь в ttypl выполните команду echostate < /dev/ttyp2
(0 Объясните эффект от того, что в результате получилось.
(g) Попытайтесь выполнить то же с обычной командой stty. Вы когда-нибудь поймете, что полученные результаты чрезвычайно полезны. 5.11 Файлы устройств и управление терминалом. Примеры в тексте использовали значе ние 0 для файлового дескриптора в вызовах tcgetattr и tcsetattr. Первый аргумент при обращении к вызову - это файловый дескриптор. Он может быть любым значением, с помощью которого производится ссылка на связь с терминальным устройством. Файловый дескриптор 1 ссылается на стандартный вывод. Модифицируйте програм мы echostate и setecho так, чтобы использовать файловый дескриптор 1 вместо 0. Как это повлияет на работу программ? Часто стандартный ввод и стандартный вывод ссы лаются на терминал. Объясните, что будет при таком перенаправлении: echostate » echostate.log. Какие преимущества от использования файлового дескриптора 0? 5.12 Если вы хотите установить систему с присоединением ррр соединений, то вам пона добится инсталлировать модем и произвести конфигурацию последовательного порта. Терминальный драйвер для последовательного порта может быть сконфи гурирован для работы с модемом. Почитайте документацию о файлах /etc/gettydefs и /etc/inittab, чтобы понять, как Unix определяет терминальные установки для вхожде ний по последовательным линиям связи. 5.13 В некоторых версиях Unix поддерживаются три варианта использования O SYNC:
только для блоков данных, только для inodes, то и другое вместе. Почему вам может потребоваться использовать одну из версий? Как называются флаги, которые контро лируют работу в каждой из этих версий? 5.14 Каково назначение прав на чтение и запись при управлении терминальным специаль ным файлом? Используйте tty, чтобы определить имя вашего терминала, а потом вы полните команду chmod ООО /dev/yourtty, чтобы сделать ваш терминал недоступным на чтение даже для вас. Что после этого произойдет? Почему? 5.15 Обратитесь к каталогу /dev на вашей системе и найдите там файлы, которые не под держивают read, файлы, которые не поддерживают write, файлы, которые не под держивают lseek. 5.16 Используйте Is -1 в каталоге /dev, чтобы посмотреть на старшие и младшие номера раз личных устройств. Какой формат вы видите? Какие устройства разделяют один и тот же главный номер? Что общего у этих устройств, в чем они различаются? 5.17 Назовите имя для каждой из четырех групп установок для tty драйвера, объясняет
назначение каждой группы и назовите имя двух бит в каждой группе.
Заключение
195
5.18 Программа использует stcsetattr для выключения режима echo для текущего термина ла. Когда эта программа заканчивается, то терминал остается в режиме с отключен ным echo. Но с другой стороны, когда программа открывает файл и использует fcntl для установке дескриптора в режим OAPPEND, то следующая программа, которая открывает этот файл, не получает возможности установить режим auto-append. Объясните эту явную несовместимость. 5.19 Соединением с терминалом является обыкновенный файловый дескриптор. Можно ли использовать вызов fcntl для установки атрибута OAPPEND в файловом дескрип торе? Что означает режим auto-append для устройств? 5.20 В чем заключается разница между iocti и fcntl? 5.21 В каталоге /dev содержатся файлы /dev/null и /dev/zero. Эти файлы не представляют собой реальное соединение с устройствами, но это также и не дисковые файлы. Како во назначение этих файлов и для чего они могут быть полезны? Можно ли найти в каталоге /dev еще файлы, которые являются виртуальными устройствами, как эти два файла?
Программные упражнения 5.22 Расширьте версию программы write, которая была представлена в главе. Представлен ная версия требовала, чтобы пользователь указывал имя файла устройства. В этой версии не выводились начальные идентификационные приветствия. Напишите новую версию, для которой при обращении можно будет указывать в качестве аргумента имя пользователя. Эта версия будет выводить на экран сообщение о том, какой пользова тель приглашает вас к взаимодействию. Посмотрите, как это делается в обычной версии команды write. Ваша программа должна предусматривать обработку при возникновении ряда особых ситуаций. Например, лицо, с которым вы хотели бы вступить во взаимодействие, может просто не работать в системе. Другая ситуация, когда лицо, с которым вам хоте лось бы взаимодействовать, могло войти в систему сразу с нескольких терминалов. 5.23 Пользователи, которые не хотят, чтобы им мешали другие пользователи, выпол няющие команду write, могут использовать команду mesg. Почитайте документацию по этой команде. Поэкспериментируйте с командой и посмотрите, как она работает. Затем напишите версию этой программы. 5.24 Использование linkQ для блокировки. Условие гонок возникает в ситуации, когда два процесса пытаются одновременно модернизировать один и тот же файл. Например, когда вы изменяете ваш пароль на некоторой системе, то программа passwd перезапи сывает некоторую информацию в файле /etc/passwd. А что произойдет, если два поль зователя попытаются одновременно изменить свои пароли? Один из подходов по пре дотвращению одновременного доступа к файлу заключается в использовании важно го свойства системного вызова link. Рассмотрим код:
г * Программа пытается построить ссылку /etc/passwd. LCK * После выполнения программы код возврата равен 0, если ссылка была построена, * 1 • если уже есть блокировка, 2 - если возникли другие проблемы
*/ int lock passwd()
{ int rv = 0; Г код возврата по умолчанию */
196
Управление соединениями. Изучение stty if {link("/etc/passwd", "/etc/passwd.LCK") == -1) rv = (errno == EEXISTS? 1 :2); return rv;
} (a) Если два процесса попытаются одновременно выполнить этот код, только одному из них удастся достичь успешного решения. Что вы можете сказать о системном вызове link, который может служить полезным средством для установления бло кировки доступа к файлам? (b) Напишите короткую программу, которая использует метод присоединения строки текста к файлу. Ваша программа должна попытаться построить ссылку с помощью link. Если попытка создания ссылки будет успешной, то программа может далее открыть файл, присоединить строку, а затем удалить связь. Если попытка построения ссылки будет неуспешной, то программа должна использовать системный вызов sleep (1), чтобы подождать одну секунду и потом повторить попытку построения ссылки. При программировании требуется позаботиться о гарантии, чтобы ваша программа не ждала бы бесконечно долго. (c) Напишите функцию unIock_passwd, которая отменяет действие lock_passwd. (d) В примере показан способ, как процессы могут блокировать доступ к сущест вующему файлу. Но как может программа, где используется link, предотвратить воз можность попытки одновременного процесса одного и того же файла? (e) Изучите команду vipw. Может ли vipw использовать links для установления бло кировок? 5.25 Связи и блокировки, часть II. В предшествующей задачи показано, как при возникно вении проблемы гонок можно использовать связи для установления блокировок на файлы. Блокировка на файл должна быть снята, когда программа, которая установила блокировку, закончит модификацию файла. Если программа не снимет блокировку файла, то другие программы будут ждать доступа к этому файлу неопределенно дол го. А что будет, если в программе (Которая установила блокировку. - Примеч. пер.) есть ошибки и она аварийно закончилась или она была “убита” пользователем, ко торый нажал на клавиши Ctrl-C? И все это произошло до сброса программой бло кировки на файл. Один из вариантов решения проблемы - в программе, которая установила блокиров ку, выполнять модификацию файла каждые п секунд. Для этого программа моэкет ис пользовать utime. Программы, которые ожидают по условию блокировки, могут про верять время модификации, чтобы убедиться - находится или нет файл в “теплом ’’состоянии. Если блокировка не была модифицирована на установленном временном интервале, то другие программы будут в праве удалить связь с тем, чтобы потом по вторно ее установить. Напишите новую версию функции Iock_passwd, которая при вызове получает в каче стве аргумента значение длительности интервала в секундах. Эта новая версия долж на реализовать логику, которая была описана в предыдущем параграфе.
Заключение
197
5.26 Как скажется на производительности, если вы отключите механизм буферизации? Напишите программу, которая производит запись в большой файл. Запись произво дится небольшими порциями. Например, :запись производится частями в 16 байт в файл размером 2 Мегабайта. Попытайтесь писать в файл в ситуациях, когда уста новлен атрибут 0_SYNC и когда атрибут 0_SYNC не установлен. Поэксперимен тируйте с размерами файла и размерами записываемых порций. Посмотрите, как это будет влиять на результаты работы. 5.27 В рассмотренном ранее тексте есть программный код, где выключается дисковая буферизация для файлового дескриптора. Напишите функцию, которая включала бы механизм буферизации. 5.28 Напишите программу uppercase.с, которая могла бы переключать бит OLCUC в драй вере терминала и оповещала бы о текущем состоянии этого бита. 5.29 Размер окна u iocti В выводе команды stty -а есть информация о числе строк и коло нок в терминальном окне. Эти значения исходят не от tcgetattr, исходят iocti. Исполь зуйте этот системный вызов для модификации версии программы more из главы 1 с тем, чтобы программа использовала устанавливаемый размер экрана терминала вместо фиксированного значения 24.
Проекты На основе материала этой главы вы можете изучить документацию и написать версию следующих программ Unix: write, stty, passwd, wall, biff, mt (программа управления ленточным устройством; может отсут ствовать на вашей системе).
Глава 6 Программирование дружественного способа управления терминалом и сигналы
Цели Идеи и средства • • • • • •
Инструментальные программные средства или пользовательские программы. Чтение и изменение установок драйвера терминала. Режимы работы драйвера терминала. Неблокируемый ввод. Таймауты на пользовательский ввод. Введение по теме сигналов: Как работает Ctrl~C.
Системные вызовы •
fcntl
•
signal
6.1. Инструментальные программные средства В системе Unix устройства во многом выглядят аналогично дисковым файлам. Но устрой ства - это не то же, чем является дисковый файл. В главе 5 нами была рассмотрена возможность использования системных вызовов open, close, read, write, lseek в отношении устройств. Но мы также видели, что устройства имеют драйверы, и эти драйверы имеют большое число атрибутов и средств управления устройствами. Как программам относиться к этой двойственности?
6.1. Инструментальные программные средства
199
Инструментальные программные средства: Чтение из stdin или из файлов, запись в stdout
Программы, которые не ощущают разницы между дисковыми файлами и устройствами, называют инструментальными программными средствами (Эти средства чаще всего назы вают утилитами. - Примеч. пер.) В системах Unix используют сотни инструментальных про граммных средств таких, как who, Is, sort, uniq, grep, tr, du. Эти средства построены на основе модели, которая представлена на рисунке 6.1.
Инструментальные программные средства читают данные со стандартного входа, производят некоторую обработку данных, записывают результирующий выходной поток байтов на стандартный вывод. Утилита посылает сообщения об ошибках, которые опять же рассматриваются как поток байтов, на стандартный вывод сообщений об ошибках. С файлами, терминалами, мышью, фотоэлементами, принтерами, программными кана лами должны быть соединены три файловых дескриптора. Утилиты не обращают внимания на то, каков источник поступления данных на обработку и каков приемник полученных результатов обработки. Многие из таких программ читают информацию из файлов, имена которых задаются при обращении к команде в командной строке. Ввод и выход для таких программ может быть присоединен ко всем типам соединений: $ sort > outputfile $ sort х > /dev/lp $who|tr'[a-z]''[A-Z]'
Программы, ориентированные на устройства: Управление устройством в конкретном применении Однако есть и другие программы, которые написаны для взаимодействия с конкретными устройствами. В качестве примера можно назвать программы для управления сканерами, для записи компакт дисков, для работы с ленточными накопителями, для производства цифровых фотографий. В этой главе мы изучим идеи и средства написания программ, ориентированных на конкретные устройства. Мы познакомимся с наиболее общими типами таких программ. Это программы, которые взаимодействуют с терминалами, которые разработаны специально для удобного использования человеком. Мы будем далее называть такие терминально ориентированные программы пользовательскими программами.
200
Программирование дружественного способа управления терминалом и сигналы
,Пользовательские программы: Общий тип программ, ориентированных на устройства Примерами пользовательских программ являются vi, emacs, pine, more, lynx, hangman, robots и многие игры, которые были разработаны в University of California at Berkeley1 Такие программы управляют установками в драйвере терминала. После чего могут отслеживать нажатия на клавиши и контролировать вывод.. При работе.с драйвером можно использовать большое количество установок. Но для пользовательских программ, прежде всего, необходимы такие: (a) Немедленный отклик на нажатие ключей (клавиш). (b) Лимитированный входной набор. (c) Таймаут на входе. (d) Защита от нажатия на клавиши Ctrl-C. Мы изучим эти темы при написании программы, где будут реализованы все эти свойства.
6.2. Режимы работы драйвера терминала В предшествующей главе было начато обсуждение, касающееся драйвера терминала. Теперь мы изучит драйвер более детально, что будет сделано с помощью эксперимента с короткой программой преобразования:: /* rotate.c: map a->b, b->c,.. z->a * назначение: полезна для показа режимов работы tty
7 «include <stdio.h> «include int main()
{ intc; while ((c=getchar()) != EOF){ if (c == *z') с = 'a'; else if (islower(c)) C++;
putchar(c);
} }
6.2.1. Канонический режим: Буферизация и редактирование Запустите приведенную программу, в которой используются установки по умолчанию (<- это клавиша backspace):
$ сс rotate.c -о rotate $./rotate abx<-cd
l. Исходные коды таких программ можно найти через Web. Следует искать bsdgames.
6.2.Режимыработыдрайвератерминала
201
bcde
tAgCtri-C $ На рисунке 6.2 показаны терминал, ядро, программа rotate и потоки данных.
В ходе нашего эксперимента обнаружились следующие свойства обработки стандартного ввода: (a) Программа никогда не сможет воспринять символ “х”. Символ стирается при вводе ключом backspace. (b) Символы воспроизводятся на экране по мере того, как вы их набираете на клави атуре. (c) Символы, набираемые на входе, не поступают в программу до тех пор, пока не будет нажата клавиша Enter. (d) Ключ Ctrl-C прерывает ввод и останавливает программу. В программе rotate нет перечисленных операций. Буферирование, эхоотображение, редактирование и обработка управляющих ключей - все это делается терминальным драйвером. На рисунке 6.3 изображены эти операции в виде уровней в драйвере. Буферирование и редактирование совместно составляют каноническую обработку. Когда эти возможности установлены, то говорят, что терминальное соединение работает в каноническом режиме.
202
Программированиедружественногоспособауправлениятерминаломисигналы
6.2.2. Неканоническая обработка Проведем теперь такой эксперимент (опять вводим: abx<-cd, затем вводим efg Ctrl-C): $ stty -icanon;./rotate abbcxyA?cdde effggh $ stty icanon
Команда stty -icanon выключает канонический режим обработки в драйвере. На печатной странице трудно в полной мере ощутить особенности неканонического режима. Вывод выше показывает, как изменится обработка на входе. (В протоколе результатов жирным шрифтом выделено то, что вводит пользователь, а не жирным - результат работы программы. - Примеч. пер.) В неканоническом реясиме, в частности, нет буферирования. Когда вы нажимаете на клавишу “а”, ,то драйвер обходит уровень буферирования и доставляет этот символ программе rotate, которая выведет на экран символ “Ь”. При использовании небуфери-рованного пользовательского входа могут возникнуть неприятности. Когда пользователь попытается стереть введенный символ, то драйвер в ответ не может этого сделать. Символ уже был размещен в пользовательскую память. Для окончания эксперимента выполните командные строки, которые следуют ниже, и опять наберите abx<-cd, а затем efg Ctrl-C. $ stty -icanon -echo; ./rotate bcyA?de fgh $ stty icanon echo (Замечание: Вы этого не увидите. Почему?)
В этом примере мы выключили канонический режим, а также выключили режим эхоотображения. После этого драйвер не будет отображать те символы, которые мы набираем. Будет отображаться только вывод из программы. Когда вы закончите исполнение программы, то драйвер останется в режиме без эхоотображения, в неканоническом режиме. Драйвер остается в этом состоянии до тех пор, пока программа не изменит его установки. Интерпретатор shell выводит приглашение и ждет ввода от вас текста очередной командной строки. Некоторые интерпретаторы сбрасывают установки драйвера, а некоторые не делают этого. Если ваш shell не сбрасывает установки драйвера, то вы будете продолжать работу без эхоотображения и в неканоническом режиме.
6.2. Режимы работы драйвера терминала
203
6.2.3. Итоговые замечания по режимам терминала Если вы еще не потренировались с примерами на терминале, то сделайте это теперь. Эти примеры показывают, что драйвер терминала может работать в различных режимах. Когда вы разрабатываете программу для Unix, то вам необходимо решить - какой терминальный режим для приложения вы выбираете. канонический режим
Канонический режим, который называют еще cooked режим, - это режим, в котором чаще всего будут работать пользователи. Драйвер накапливает входящие символы в буфер. Он посылает эти символы из буфера программе, когда определит, что нажат ключ Enter^. Буферирование данных позволяет драйверу выполнять базовые функции редактирования, такие, как удаление символа, слова или целой строки. Эти функции начинают выполняться, когда пользователь нажимает соответственно на ключи: erase, word-erase или kill. Вызов этих трех функций закрепляется за специальными клавишами, что записано в драйвере в качестве установок. Эти установки можно изменить с помощью команды stty или системного вызова tcsetattr. неканонический режим или crmode
Когда выключено действие функций буферирования, а следовательно, и функций редактирования, то говорят, что соединение работает в неканоническом режиме. Но драйвер терминала все также поддерживает обработку специальных символов, таких, как Ctrl-C, поддерживает преобразование символов newline в carriage return. Однако клавиши erase, word-erase, kill утрачивают свой специальный смысл. Поэтому коды этих клавиш будут рассматриваться драйвером как обычные данные. Если вы пишете программу, которая использует неканонический режим, и вы захотите использовать редактирование при вводе, то вам придется написать в вашей программе функции редактирования. “никакой” режим или raw mode
Также для управления каждым шагом обработки используются отдельные биты. Напри мер, бит ISIG указывает на то, что нажатие на ключ прерывания (обычно это Ctrl-C) при ведет к обычному действию - программа будет “убита”. В программе можно выключить все шаги по обработке. Когда выключены все шаги обработки, то драйвер передает ввод непосредственно программе. В таком случае говорят, что драйвер работает в raw режиме (Буквально - в сырам режиме. Примеч. пер.) Этот режим был впервые использован в Olden Days ® когда драйвер терминала был гораздо проще. Там он и получил название raw-режим. В команде stty установка raw режи ма производится с помощью определенной опции. Назначение raw режима для команды stty представлено в документации. Драйвер терминала - это набор подпрограмм в ядре. Роль различных компонентов драй вера становится все более ясной по мере изучения и проведения экспериментов с драй вером. На рисунке 6.4 представлены наиболее важные части.
2. Или при нажатии на ключ, обозначающий конец файла. Обычно это Ctrl-D.
204
Программирование дружественного способа управления терминалом и сигналы
Несколько режимов работы терминала были разработаны потому, что каждый из них посвоему полезен. Для понимания практической значимости этих режимов мы разработаем пользовательскую программу, которая использует различные режимы работы драйвера.
6.3. Написание пользовательской программы: piay.again.c Во многих пользовательских приложениях, таких как приложения для банкоматов или в видео играх, пользователю задают вопросы, предполагающие ответы в форме yes/no. Следующий ниже shell-скрипт является главным циклом обработки при работе банкомата: #!/bin/sh
# # atm.sh - это “обертка” для двух программ
# while true do do_a_transaction # запустить программу if play.again # запустить нашу программу then continue # если "у", то повторить цикл fi break # если "п", то прервать цикл обработки done
Используя типичный стиль Unix, работу с банковской машиной можно представить как скрипт, который содержит отдельные компоненты. Первым компонентом является про грамма do_a_transaction, которая воспроизводит работу банкомата (ATM). Вторым компо нентом является программа play_again, которая получает от пользователя ответы - либо yes, либо по. Мы напишем эту вторую программу. Такая компонентная архитектура позво ляет нам легко присоединить play_again к последующим реализациям.
6.3. Написание пользовательской программы: р!ау_ again, с
Логика play.again.c проста: обращение к пользователю с вопросом, получение ответа, если “у”, то возвратить О, если “п”, то возвратить 1.
Пример: play_again0.c - выполнение задания Г play_again0.c * назначение: обращение к пользователю с вопросом - хочет ли он получить * некую транзакцию * метод: задать вопрос, ожидать ответа yes/no * коды возврата: 0=>yes, 1=>по * усовершенствования: устраняется потребность в нажатии на клавишу return
*/ <stdio.h> tinclude tinclude tdefine QUESTION "Do you want another transaction" int get_response(char *); int main()
{ int response; response = get_response(QUEST!ON); /* получить какой-то ответ */ return response;
} int get response(char ‘question)
Г * назначение: задать вопрос и ждать ответа у/п * метод: использовать getchar и игнорировать ответы, которые не отвечают • * форме у/п * коды возврата: 0=>yes, 1=>по
★
7 printf("%s (y/n)?", question); while(1){ switch{getchar()) { case'y’: case T: return 0; case'n': case’N': case EOF: return 1;
205
206
Программирование дружественного способа управления терминалом и сигналы
Эта программа выводит вопрос и далее переходит в цикл, считывая ввод от пользователя до тех пор, пока пользователь не наберет какой-либо из симоволов: “у” или “n”, “Y” или “N”. В программе play_againO есть две проблемы, которые явились следствием использова ния канонического режима. Во-первых, пользователь нажимал на ключ Enter для того, чтобы программа play_againO могла бы приступить к обработке ввода. Во-вторых, програм ма принимала и обрабатывала всю строку данных после того, как пользователь нажимал на Enter. Поэтому программа play_agaipO после такого с ней взаимодействия: $ play_againO Do you want another transaction (у/n)? sure thing!
сочтет, что ответ на ее вопрос был отрицательный. Нашим первым улучшением этой версии программы явился отказ от канонического режима ввода для того, чтобы програм ма принимала и обрабатывала символы по мере нажатия пользователем на клавиши.
Пример: play_again1.с-немедленный ответ /*play_again1.c назначение: обращение к пользователю с вопросом - хочет ли он получить * некую транзакцию * метод: установить терминал в посимвольный режим (char-by-char), читать * символ, возвратить результат * коды возврата: 0=>yes, 1=>по * усовершенствования: не отображать неподходящий ввод
7 <stdio.h> #include «include «define QUESTION "Do you want another transaction' main() int response; /* сохранить режим терминала */ tty_mode(0); Г установить режим chr-by-chr */ set_crmode(); response = get_response(QUESTION); /* получить некий ответ */ tty_mode(1); Г восстановить режим терминала */ return response; int get response(char ‘question)
Г * назначение: задать вопрос и ждать ответа у/п ’ метод: использовать getchar и выражать недовольство по поводу ответов, * представленных не в форме у/п ж коды возврата: 0=>yes, 1=>по
*/
int input; printf(’’%s (у/п)?", question); while(1){ switch(input = getchar()){
6.3. Написание пользовательской программы: p/ay_again.c
207
case’у’: case Y: return 0; case 'n': case 'N': case EOF: return 1; default: printf("\ncannot understand %c,", input); printf("Please type у or no\n");
} } } set crmode()
Г * назначение: получить файловый дескриптор 0(т. е. stdin)s режиме chr-by-chr * метод: использовать разряды в структуре termios
7 { struct termios ttystate; tcgetattr(0, &ttystate); Jtystate.cJflag &= - ICANON; ttystate.c_cc[VMIN] = 1; tcsetattr(0, TCSANOW, &ttystate);
/* читать текущие установки */ /* без буферизации */ /* получать за раз 1 символ */ /* инсталляция установок */
} /* how == 0 => save current mode, how == 1 => restore mode 7 tty_mode(int how)
{ static struct termios original_mode; if (how == 0) tcgetattr(0, &original_mode); else return tcsetattr(0, TCSANOW, &original mode);
} В программе play_again1 прежде всего выполняется перевод терминала в посимвольный (character-by-character) режим. Затем вызывается функция для вывода приглашения и для получения ответа. Наконец, производится восстановление режима работы терминала. Заметим, что в конце мы не переводим драйвер в канонический режим. Вместо этого мы сначала копируем первоначальные установки в структуру original_mode, а потом эти уста новки восстанавливаются. Перевод терминала в посимвольный режим состоит из двух частей. Мы сбрасываем бит ICAN ON и устанавливаем в 1 элемент VMIN в массиве управляющих символов. Такое значение VMTN указывает драйверу, сколько символов он должен однократно считывать при вводе. Мы хотим производить посимвольное считывание, поэтому было установлено значение 1. Если бы нам потребовалось читать по три символа3, то нужно было бы установить число 3. 3. Я это использовал для управления функциональными ключами. На моей клавиатуре функциональные ключи посылают многосимвольные последовательности, такие, как escape-[-l-l- Когда моя программа читает символ escape (код ASCII равен 27), то она предполагает три или четыре символа в строке.
208
Программирование дружественного способа управления терминалом и сигналы
Откомпилируем и запустим эту программу. В качестве ответа будем набирать слово sure:
$ make play_again1 сс play_again 1 .с -о play_again1
$./play_again1 Do you want another transaction (y/n)?
s cannot understand s, Please type у or no
u cannot understand u, Please type у or no
r cannot understand r, Please type у or no
e cannot understand e, Please type у or no
У$ Как и предполагалось, программа play_againl принимает и обрабатывает символы по мере их набора на клавиатуре без ожидания нажатия на клавишу Enter. Но раздражает вывод негативной реакции программы на каждый символ. Более очевидным решением было бы отключение режима эхо отображения и тем самым игнорирование неприемлемых симво лов, пока не будут введены ожидаемые символы.
Пример: play_again2.c - игнорирование недопустимых ключей *
Г play_again2.c назначение: обращение к пользователю с вопросом-хочет ли он получить некую транзакцию * метод: установить терминал в посимвольный режим (char-by-char)n погасить * эхоотображение, читать символ, возвратить результат * коды возврата: 0=>yes, 1 =>по * усовершенствования: таймаут, если пользователь уходит
7 «include <stdio.h> «include «define QUESTION main()
"Do you want another transaction"
{ int response; tty_mode(0); /* сохранить режим */ set_cr_noecho_mode(); /* установить-icanon,-echo */ response = getjesponse(QUESTION); /* получить некий ответ */ tty_mode( 1); f* восстановить состояние терминала */ return response;
} int get response(char ‘question)
Г * назначение: задать вопрос и ждать у/п ответа * метод: использовать getchar и игнорировать ответы, которые не в формате у/п * коды возврата: 0=>yes, 1=>по
*/
6.3. Написание пользовательской программы: p!ay_again.c
209
printf("%s (y/n)?", question); while( 1){ switch(getchar()){ case'y’: case Y: return 0; case 'n': case'N': case EOF: return 1;
} } } set_cr_noecho_mode()
Г * назначение: установить для файлового дескриптора 0 посимвольный режим и * сброс эхоотображения * метод: использование разрядов в структуре termios
7 { struct termios ttystate; tcgetattr(0, &ttystate); /* читать текущие установки */ ttystate.cjflag &= - ICANON; /* без буферирования */ ttystate.c_lflag &= - ECHO; /* также без echo 7 ttystate.c_cc[VMIN] = 1; /* получать при чтении однократно 1 символ 7 tcsetattr(0, TCSANOW, &ttystate); /* инсталляция установок */
} /* how == 0 => сохранить текущий режим, how == 1 => восстановить режим */ tty mode(int how)
{ static struct termios original_mode; if (how == 0) tcgetattr(0, &origina!_mode); else return tcsetattr(0, TCSANOW, &original mode);
} Эта программа отличается от предшествующей версии двумя особенностями. Есть функ ция, которая сбрасывает бит echo при установке режима работы драйвера терминала. Заметим, что функция восстановления не включает этот бит. Другой особенностью явля ется то, что функция get_response больше не выводит предупредительных сообщений, когда получает недопустимые символы. Драйвер просто их игнорирует. Откомпилируйте и выполните эту программу. Если при работе вы наберете слово sure, то ничего не увидите на экране. Только когда вы наберете у или п, то программа будет на это явно реагировать. Программа play_again2 делает то, что мы предполагали. Но у нее есть еще одна особен ность. Что, если эта программа будет использована в реальном банкомате (ATM) и поку патель ушел по рассеянности без нажатия на клавиши у или п. Тогда следующий покупа
210
Программирование дружественного способа управления терминалом и сигналы
тель может нажать шу и получить доступ к счету покупателя, который ушел. Пользова тельские программы имеют больший уровень защищенности, когда в них есть средства поддержки таймаута.
6.3.1. Неблокируемый ввод:p!ay_again3.c В следующей версии нашей программы включена возможность таймаута. Мы создадим средство таймаута, которое должно будет сообщать драйверу терминалу о том, что далее не следует ожидать ввода. Если мы не обнаруживаем ничего на входе, то мы должны будем перейти в состояние ожидания на несколько секунд и опять проверить состояние входа. После трех неуспешных попыток происходит отказ от дальнейших проверок.
Блокируемый и неблокируемый ввод Когда вы обращаетесь к getchar или к read, чтобы читать данные через файловый дескрип тор, то обычно системный вызов ожидает ввода. В примере play_again при обращении к get char программа ждала до тех пор, пока пользователь не нажмет на клавишу. Программа блокировалась аналогично тому, как блокируется автомобиль на железнодорожном пере езде. Программа блокируется, пока на ее входе не появится какой-либо символ или не бу дет обнаружено поступление признака конца файла. А как отключить блокировку ввода? Блокировка является свойством любого открытого файла, а не только соединений с терми налами. Программы могут использовать системные вызовы fcntl или open для того, чтобы установить неблокируемый ввод для файлового дескриптора. Программа play_again3 использует системный вызов fcntl, чтобы установить флаг 0_NDELAY4 для файлового дескриптора. Мы выключили блокировку файлового дескриптора и обратились к вызову read. Ну и что произойдет? Если ввод доступен, то вызов read получает входные данные и возвращает, как результат, количество прочитанных символов. Если символов на входе не было, то вызов read возвращает 0, как это делается в случае приема на входе символа конца файла. Вызов read возвращает -1, если была обнаружена ошибка. В непосредственной реализа ции неблокируемое действие является весьма простым. Для каждого файла имеется про странство для хранения допустимых непрочитанных данных, что изображено на рисунке 6.4 в верхней части бокса внутри драйвера. Если файловый дескриптор имеет установлен ный бит 0_NDELAY и это пространство пустое, то системный вызов read возвратит 0. Если вы обратитесь к исходному коду Linux с командной grep для поиска места использования OJNDELAY, то там вы найдете детали реализации.
Пример: play_again3.c - использование неблокируемого режима для таймаутов Г play_again3.c * назначение: обращение к пользователю с вопросом - хочет ли он получить некую транзакцию * метод: установить терминал в посимвольный режим (char-by-char)n погасить * эхоотображение, установить терминал в режим no-delay, читать символ, * возвратить результат * коды возврата: 0=>yes, 1=>по 2=>таймаут * усовершенствования: сброс режима терминала по прерыванию
7 #include
<stdio.h>
4. Вы можете также использовать бит 0_N0NBL0CK; обратитесь к документации.
1. Написание пользовательской программы: p!ay_again. с «include tinclude tinclude <string.h> tdefine ASK tdefine TRIES 3 tdefine SLEEPTIME 2 tdefine BEEP putchar('\a') mainQ
"Do you want another transaction" /* максимальное число попыток */ [* время на одну попытку */ /* предупреждение пользователя */
{ int response; /* сохранить текущий режим */ tty_mode(0); Г установить -icanon, -echo */ set_cr_noecho_mode(); Г noinput => EOF */ set_nodelay_mode(); response = get_response(ASK, TRIES); /* получить некий ответ */ tty_mode( 1); /* восстановить исходный режим */ return response;
} get response(char ‘question, int maxtries)
/*’ * назначение: задать вопрос и ждать ответа у/п или ждать в течение * указанного максимального времени * метод: использовать getchar и реакцию на поп-у/п ввод * returns: 0=>yes, 1=>по, 2=>таймаут
7 { int input; printf("%s (y/n)?'', question); fflush(stdout); while (1){ sleep(SLEEPTIME); input = tolower(get_ok_char()); if (input == У) return 0; if (input == 'n') return 1; if (maxtries-- == 0) return 2; BEEP;
/* задать вопрос 7 Г обеспечить вывод */ /* ожидать указанное время */ [* получить следующий символ ’
/* вышло время? */
Лда7
}
Л * пропустить недопустимые символы и возвратить y.Y.n.N или EOF */ get ok char()
{ "’ int с;
2
Программирование дружественного способа управления терминалом и сигналы while((c = getchar()) != EOF &&strchr("yYnN",c) == NULL)
i return c;
} set_cr noecho_mode()
/*' " * назначение: установить для файлового дескриптора 0 посимвольный режим и * режим без эхоотображения * метод: использовать биты в структуре termios 7
{
^
struct termios ttystate; tcgetattr(0, &ttystate); ttystate.cjfiag &= - ICANON; ttystate.cjflag &= - ECHO; ttystate.c_cc[VMIN] = 1; tcsetattr(0, TCSANOW, &ttystate);
/* читать текущие установки */ /* не буферировать */ /* также без echo 7 /* получать однократно по 1 символу */ /* инсталлировать установки */
} set nodelay mode()
Г * назначение: установить файловый дескриптор 0 в режим no-delay * метод: использовать fcntl для установки разрядов * . замечания: подобное выполняет tcsetattr(), но она более сложна 7
{ int termflags; termflags = fcntl(0, F_GETFL); /* читать текущие установки */ termflags |= 0_N DELAY; /* переустановить бит nodelay */ fcntl(0, F SETFL, termflags); /* и инсталлировать установку в ядре 7
} Г how == 0 => сохранить текущий режим, how == 1 => восстановить режим */ Г В этой версии происходит управление структурой termios и флагами fcntl */ tty mode(int how)
{’ static struct termios original_mode; static int original_flags; if (how == 0){ tcgetattr(0, &original_mode); original flags = fcntl(0, F GETFL);
} else{ -----
tcsetattr(0, TCSANOW, &original_mode); fcntl(0, F_SETFL, original_flags);
г
6.3. Написание пользовательской программы: ptay_again. с
213
Новые свойства в этой версии программы - использование системного вызова fcntl для включения и выключения режима неблокируемого ввода и использование sleep, а также счетчика maxtries в getjesponse. Небольшие проблемы с программой play_again3 Программа play_again3 не идеальна. При запуске в неблокируемом режиме программа засы пает на две секунды, прежде чем обратиться к getchar и дать пользователю шанс набрать что-нибудь. Если пользователь справится с набором за одну секунду, то программа не по лучит этот символ, пока не истекут две секунды. Возможно, это запутает пользователей. Можно ли написать программы с более быстрой реакцией? Мы можем сократить время на ожидание перед вызовом getchar и компенсировать это уменьшение за счет увеличения числа итераций. Во-вторых, обратим внимание на вызов fflush после вывода приглашения. Без этой строки приглашение не появится, пока в программе не будет вызова getchar. И вот почему. Драйвер терминала не только производит буферирование ввода на основе по строчного принципа он также поддерживает и построчное буферирование вывода. Драй вер буферирует вывод до тех пор, пока он не получит символ newline или пока программа не сделает попытку чтения с терминала. В этом примере, где пользователю дается шанс для прочтения приглашения на основе метода откладывания обращения к вводу, нам пона добится добавить вызов fflush. Другие методы реализации таймаутов В Unix есть более хорошие методы для реализации таймаутов для пользовательского вво да. Элемент VTIME в массиве управляющих символов с_сс[] драйвера терминала позврляет устанавливать значение таймаута в драйвере терминала. Детали представлены в упражне нии (в конце главы). Устанодление значения таймаута можно сделать с помощью систем ного вызова select. Вызов select мы обсудим в следующей главе. Большая проблема с play_again3 Программа play_again3 игнорирует символы, которые ей не нужны, идентифицирует и обра батывает допустимые входные символы и выполняет exit, если не поступили допустимые символы в течение установленного временного интервала. Что произойдет, если пользова тель нажмет на Ctrl-C? Вот простой пример работы: $ make play_again3 сс play_again3.c -о play_again3 $./play_again3 Do you want another transaction (у/n)? press Ctrl-C now $ logout Connection to host closed. bash$
Когда мы нажимаем на Ctrl-C, то будет убит процесс исполнения программы. Убивается не только процесс исполнения программы, но убивается также и вся login сессия (Ранее автор не давал четкого определения login сессии. Это совокупность процессов, которые имеют один и тот же идентификатор пользователя. Они были порождены по инициативе пользователя, вошедшего в систему. - Примеч. пер.) Как это могло случиться? При работе программы можно выделить такие шаги. Программа play_again3 состоит из трех частейинициализация, получение ввода от пользователя, восстановление установок. Это показа но на рисунке 6.5.
214
Программирование дружественного способа управления терминалом и сигналы
В инициализирующей части производится установка терминала в неблокируемый режим ввода. Затем программа входит в основной цикл ввода, где выводится приглашение, переход в состояние ожидания и ввод данных. Затем процесс исполнения программы был убит с помощью ключа Ctrl-C. А в каком состоянии остался драйвер терминала? После того, как исполнение программы было закончено, не существует какой-то части про граммы, которая могла бы сбросить установки драйвера в исходное состояние. К моменту, когда shell выведет на экран новое приглашение и будет готов получать текст командной стро ки от пользователя, терминал остается в неблокируемом режиме ввода. В shell запускается команда read для чтения текста командной строки. Но работа read будет проходить в небло кируемом режиме ввода и поэтому сразу будет возвращено значение 0. Таким образом, про грамма оставила файловый дескриптор и соответствующий драйвер с неправильными атри бутами. В нашем следующем проекте мы поучимся, как защитить нашу программу от дейст вия ключа Ctrl-C.
Когда я это попробовал сделать, то не мог выйти из системы! Во многих системах Unix командные интерпретаторы обладают возможностями по редак тированию. Такими, например, как использование клавишей со стрелками для скроллиро вания по списку введенных ранее команд. Командные интерпретаторы bash и tcsh сбрасы вают терминальные атрибуты после того, как произойдет выход из вашей программы или она будет досрочно окончена.
6.4. Сигналы Ключ Ctrl-C прерывает исполнение текущей программы. Это прерывание производится с помощью механизма ядра, который называют механизмом сигналов. Сигналы - это про стая и продуктивная идея. Мы изучим основные идеи механизма сигналов и узнаем, как можно использовать сигналы, по мере решения нашей проблемы в версии play_again3. В следующей главе мы рассмотрим сигналы более детально.
6.4.Сигналы
215
6.4.1. Что делает управляющая последовательность Ctrl-C Вы нажимаете ключ Ctrl-C, и программа заканчивает свою работу. Как может нажатие на клавиши привести к гибели процесса? Здесь нужно отметить роль драйвера терминала. На рисунке 6.6 представлена цепочка событий:
Ключ может быть отличным от Ctrl-C. Чтобы сделать замену текущего управляющего символа VINTR на другой символ, нужно использовать команду stty (или tcsetattr).
6.4.2. Что такое сигнал? При нажатии на Ctrl-C вырабатывается сигнал. Но что такое сигнал? Сигнал - это в общем случае однословное сообщение. Зеленый свет - это сигнал. Жест судьи - это сигнал. Эти элементы и события не содержат сообщений. Они сами представляют собой сообщения: идите, стойте, вон с поля! Когда вы нажимаете на ключ Ctrl-C, то тем самым вы выдаете ядру требование - послать сигнал прерывания текущему развивающемуся процессу. Каж дому сигналу сопоставлен цифровой код. Обычно цифровой код сигнала прерывания про граммы (interrupt) будет равен 25. Откуда поступают сигналы? Сигналы приходят из ядра, но требования на сигнал поступают из трех источников, как это показано на рисунке 6.7.
5. И если его изменить, то многие скрипты будут прерываться.
216
Программирование дружественного способа управления терминалом и сигналы
Пользователи Пользователь может нажать на ключи (клавиши) Ctrl-C, CtrlA или нажать на
любой другой ключ, который поддерживает драйвер терминала и использу ется в качестве средства для выработки сигнала. Ядро Ядро посылает процессу сигнал, когда процесс делает что-либо неправиль но. Например, фиксируются такие ошибки: происходит нарушение сегмен тации памяти, возникает ситуация исключения при выполнении операции с плавающей точкой, делается попытка выполнить недопустимую машин ную команду. Таким образом, ядро использует сигналы, чтобы оповещать процесс о возникновении определенных событий. Процессы Процесс может посылать сигналы другому процессу с помощью системного вызова kill. Посылка сигнала-это один из возможных способов для процесса установить связь с другими процессами. Сигналы, которые возникают в результате деятельности процесса (например, деление на ноль) называют синхронными сигналами. Сигналы, которые возникают при возникнове нии внешних событий в отношении процесса (например, нажатие пользователем на ключ прерывания) называют асинхронными сигналами. Где можно найти список сигналов? Номера сигналов и их символические имена чаще всего находятся в файле /usr/include/signal.h. Ниже приведен фрагмент текста из этого файла: ffdefine SIGHUP «define SIGINT #define SIGQUIT «define SIGILL #define SIGTRAP «define SIGABRT «define SIGEMT «define SIGFPE «define SIGKILL «define SIGBUS «define SIGSEGV «define SIGSYS «define SIGPIPE «define SIGALRM «define SIGTERM
1 2 3 4 5
6 7
8 9
10
11 12
13 14 15
Г hangup, generated when terminal disconnects */ Г interrupt, generated from terminal special char */ Г (*) quit, generated from terminal special char */ /* (*) illegal instruction (not reset when caught)*/ [* (*) trace trap (not reset when caught) */ /* (*) abort process */ Г (*) EMT instruction */ /* (*) floating point exception */ I* kill (cannot be caught or ignored) */ /* (*) bus error (specification exception) */ Г (*) segmentation violation */ /* (*) bad argument to system call */ f* write on a pipe with no one to read it */ /* alarm clock timeout */ /* software termination signal */
64. Сигналы
217
Например, сигнал прерывания (interrupt signal) называется SIGINT, сигнал выхода (quit) называется SIGQUIT, а сигнал по ошибке “нарушение сегментации памяти” называется SIGSEGV. В каждой версии Unix есть документация, в которую включена более детальная информация. В Linux следует обратиться к документу signal(7). Что делают сигналы? Результат зависит от обстоятельств. Многие сигналы приводят к гибели процесса. Процесс развивался, а затем умирает, что проявляется в освобождении памяти, закрытии всех дескрипторов, удалении процесса из таблицы процессов. Для уничтожения процесса мы будет использовать сигнал SIGINT. Но процесс может защи титься от воздействия сигнала.
6.4.3. Что может процесс сделать с сигналом? Процесс может и не погибнуть, когда он получит сигнал SIGINT. Процесс может обратить ся к ядру с помощью системного вызова signal и сообщить ядру, как он желал бы реагиро вать на сигнал. Для процесса есть три варианта действий:
согласиться с действием по умолчанию (обычно гибель процесса) В документации для каждого процесса указано действие по умолчанию. Действием по умолчанию для сигнала SIGINT является гибель процесса. Процесс может восстановить действие по умолчанию с помощью такого вызова: signal(SIGINT, SIG_DFL);
проигнорировать сигнал Процесс может создать защиту от поступающих сигналов. Программа сообщает ядру, что она хочет игнорировать поступление сигнала SIGINT с помощью системного вызова: signal(SIGINT, SIGJGN);
вызов функции Этот вариант наиболее мощный из трех возможных вариантов. Рассмотрим пример play__again3. Когда пользователь нажимает ключ Ctrl-C, то программа, находясь в том виде, как она сейчас представлена, будет прервана. При этом не производится вызов функции по восстановлению установок драйвера. Программу можно улучшить, если при поступле нии сигнала SIGINT будет вызвана функция по восстановлению установок терминала, а затем будет выполнен вызов exit. Третий вариант работы системного вызова signal в полной мере решает проблему. Программа может сообщить ядру, какую функцию следует вызвать при поступлении сигнала. Функция, которая вызывается при поступлении сигнала, называется обработчиком сигнала. Для инстал ляции обработчика сигнала программа обращается к вызову: signal(signum, functionname);
signal НАЗНАЧЕНИЕ
INCLUDE
Управление сигналом tinclude < signal, h >
ИСПОЛЬЗОВАНИЕ
result = signal (int signum, void (*action)(int))
АРГУМЕНТЫ
signum - сигнал action - каким образом реагировать
КОДЫ ВОЗВРАТА
-1 - при ошибке При успехе - предыдущая реакция на сигнал
218
Программирование дружественного способа управления терминалом и сигналы
Системный вызов signal инсталлирует новый обработчик сигналов в отношении сигнала с указанным номером signum. В качестве аргумента action может быть задано имя функции или одно из двух специальных значений: SIGJGN - проигнорировать сигнал; SIG_DFL - восстановить действие сигнала no умолчанию.
Системный вызов signal возвращает, как результат выполнения вызова, предшествующую установку для обработчика сигнала. Этим значением будет указатель на функцию обра ботки сигнала.
6.4.4. Пример обработчика сигнала Пример 1: Перехват сигнала Г sigdemol .с - показывает, как работает обработчик сигнала * - запустите программу и нажмите несколько раз Ctrl-C «include «include mainQ
<stdio.h> <signal.h>
{ void f(int); inti; signal(SIGINT, f); for{i=0; i<5; i++){ printf("hello\n"); sleep( 1);
Г декларирование обработчика */ /* инсталляция обработчика */ /* выполнение некой работы */
} } void f(int signum)
/* вызывается эта функция */
{ printf("OUCH!\n");
J Функция main состоит из двух частей - из вызова signal и цикла. В программе sigdemol .с при обращении к signal производится инсталляция функции f, которая должна управлять сигналом SIGINT. Если процесс принял сигнал SIGINT, то ядро заставит программу вызвать функцию f. В программе управление передается этой функций, функция выполня ется, и управление передается обратно программе. Вся эта последовательность действий такая же, как при обращении к подпрограмме. На рисунке 6.8 показаны два независимых потока управления: обычный, который прохо дит по функции main, через цикл и к выходу из main. Другой поток проходит через функ цию f и возникает по мере появления сигнала.
6.4.Сигналы
219
Ниже приведена иллюстрация работы программы:
$ ./sigdemol hello
hello нажать здесь Ctrl-C OUCH! hello нажать здесь Ctrl-C OUCH! . hello hello
$ Откомпилируйте программу и попытайтесь выполнить все это сами. В программе нет явного вызова функции f. Обращение к функции происходит по мере прихода сигнала. (Указанная в примере логика выполнения системного вызова signal справедлива для Linux,' но может быть несправедлива для других версий ОС Unix. Например, в System V, если в системном вызове signal указывается функция обработки сигнала, то первое, что выпол няется в этой функции при поступлении сигнала, - будет восстановлена стандартная реак ция на сигнал. Для Ctrl-C реакция по умолчанию - завершение процесса. Поэтому при по вторном поступлении этого же сигнала процесс завершится:
$./sigdemo1 hello
hello нажать здесь Ctrl-C OUCH! Здесь будет восстановлена Стандартная реакция на сигнал hello нажать здесь Ctrl-C; программа будет завершена
$ При использовании системного вызова sigaction, который соответствует стандарту POSIX, установленная функция обработки сигнала сохраняется до переустановки реакции на сиг нал. Автор рассматривает особенности этого системного вызова в главе 7. - Примеч. ред.)
Программирование дружественного способа управления терминалом и сигналы
220
Пример 2: Проигнорировать сигнал Г sigdemo2.c - показывает, как можно проигнорировать сигнал *
- нажмите Ctrl-\, чтобы убить процесс
*/ «include «include main()
<stdio.h> <signal.h>
{ signal(SIGINT, SIGJGN); ' printf(”you can’t stop me!\n"); while( 1)
{ sleep( 1); printf("haha\n");
} } Программа sigdemo2.c использует вызов signal, чтобы установить режим игнорирования сигнала прерывания. Пользователь может сотни раз нажимать на Ctrl-C, но это никак не повлияет на развитие процесса.
Ниже проиллюстрирована работа с программой: $ ./sigdemo2 you can't stop me! haha haha haha нажать здесь Ctrl-C haha нажать здесь два раза Ctrl- С haha haha haha нажать здесь Л| Quit $
6.5. Подготовка к обработке сигналов: p!ay_again4.c
221
При нажатии на ключ Ctrl-\ возникает уже другой сигнал, сигнал quit (выход). А в данной программе не было выполнено никаких приготовлений для игнорирования или перехвата сигнала SIGQUIT.
6.5. Подготовка к обработке сигналов: piay_again4.c Мы теперь знаем, как нужно модифицировать программу play_again3.c, чтобы было можно управлять сигналами. Но мы теперь должны найти проектное решение. Будем ли мы игнорировать сигналы и требовать, чтобы пользователь набирал yes или по в ответ на вопрос? Следует ли перехватывать сигналы от клавиатуры и организовывать выход из программы, если пользователь наберет ответ по? Или следует выполнить exit и с помощью кода возврата сообщить, что процесс исполнения программы был убит? В представленной версии программы происходит перехват сигнала SIGINT, восстановле ние установок драйвера, и возврат кода по:
f play_again4.c * назначение: запросить, не желает ли пользователь выполнить некую транзакцию * метод: установить посимвольный режим терминала, режим no-echo, * установить no-delay режим терминала * читать символ, возвратить значение результата * восстановить режимы терминала при поступлении * сигнала SIGINT, проигнорировать поступление * сигнала SIGQUIT * коды возврата: 0=>yes, 1=>по, 2=>таймаут * усовершенствования: сброс терминального режима при прерывании
7
«include <stdio.h> «include «include «include <string.h> «include <signal.h> «define ASK "Do you want another transaction" «define TRIES 3 /* максимальное число попыток */ «define SLEEPTIME 2 /* время на одну попытку */ «define ВЕЕР putchar('\a') Г оповещение пользователя */ main()
{
int response; void ctrl_c_handler(int); tty_modi{6); set_cr_noecho_mode(); sefn6delay_mode(); signal(SIGINT, Ctrl с handler); signal(SIGQUIT, SIGJGN); response = get_response(ASK, TRIES); tty_mode(1); return response;
Г сохранение текущего режима */ Г установить -icanon, -echo */ /* noinput => EOF */ Г обработка сигнала INT */ [* игнорировать сигналы QUIT */ [* получить некий ответ */ /* восстановить оригинальный режим */
}
get response(char ‘question, int maxtries)
/*"
* назначение: задать вопрос и ждать у/п ответа или таймаут * метод: использование getchar и предостережений о вводе символов, отличных от у/п ’ коды возврата: 0=>yes, 1=>по
*/
2
Программирование дружественного способа управления терминалом и сигналы
{
int input; -----printf("%s (y/n)?”, question); /* вопрос */ fflush(stdout); /* форсировать вывод */ while (1){ sleep(SLEEPTIME); /* ожидание 7 input = tolower(get_ok_char()); /* получить следующий символ */ if (input == 'у') return 0; if (input == 'n') return 1; if (maxtries- == 0) /* истекло время? */ return 2; /* такой выход */ BEEP;
} }
Г
* пропускать недопустимые символы и возвращать y,Y,n,N или EOF
7
get ok char() {"" int с; while((c = getchar()) != EOF &&strchr(,,yYnN",c) == NULL)
t return c;
}
set cr noecho mode()
I*
* назначение: перевод файлового дескриптора 0 в посимвольный режим и режим * noecho * метод: использование разрядов в структуре termios
7 {
struct termios ttystate; tcgetattr(0, &ttystate); /* читать текущие установки 7 ttystate.c_lflag &= - ICANON; /* отказ от буферирования */ ttystate.c jflag &= - ECHO; /* а также отказ от echo 7 ttystate.c_cc|VMIN] = 1; /"получение при однократном вводе одного символа * tcsetattr(0, TCSANOW, &ttystate); /* инсталляция установок */
} set nodelay mode()
Г
* назначение: установка файлового дескриптора 0 в режим no-delay * метод: использование fcntl для установки управляющих разрядов * замечания: нечто подобное выполняет tcsetattr(), но она более сложна
7 {
}
int termflags; termflags = fcntl(0, F_GETFL); f* читать текущие установки */ termflags |= 0_NDELAY; /* переустановка разряда nodelay */ fcntl(0, F SETFL, termflags); Г и его инсталляция 7
Г how == 0 =>сохранение текущего режима,how == 1 =>восстановление режима */ Г В этой версии происходит управление с помощью структуры termios и флагов * fcntl
*/
6.6. Процессы смертны
223
tty_mode(int how)
{'
static struct termios original_mode; static int original_flags; static int stored = 0; if (how == 0){ tcgetattr(0, &original_mode); originlljlags = fcntl(0, F_GETFL); stored = 1;
}
else if (stored) { tcsetattr(0, TCSANOW, &origina!_mode); fcntl(0, F SETFL, original flags);
} }
void Ctrl c_handler(int signum)
Г
* назначение: вызывается, если поступит сигнал SIGINT * действие: сброс tty и scram
7 {
tty_mode(1); exit(1);
} Другие проекты на эту тему остаются в качестве упражнений.
6.6. Процессы смертны Программа использует системный вызов signal для передачи ядру требования, что она хотела бы игнорировать сигнал. А что произойдет, если кто-либо напишет программу, где будет установлена диспозиция SIGJGN для всех процессов и если в программе будет исполняться бесконечный цикл? К радости системного администратора (и программистов), в Unix невозможно сделать программу бессмертной. Есть два сигнала, которые нельзя перехватить или проигнориро вать. Почитайте документацию или список сигналов в заголовочном файле, чтобы опреде лит, какие сигналы проходят через любые преграды.
6.7. Программирование для устройств Мы рассмотрели три аспекта написания программы, которая управляет терминалом. Сначала мы изучили атрибуты драйвера и возможности по управлению соединениями. Затем мы рассмотрели конкретные потребности приложений и разработали драйвер, который удовлетворял бы этим требованиям. Наконец, мы изучили, как можно управлять сигналами, которые являются одной из форм прерывания процессов. Эти три аспекта применимы ко всем устройствам. Рассмотрим звуковую карту или диско вод. Устройство имеет различные установки, которые контролируются драйвером устрой ства. Вам необходимо изучение материала, касающегося этих установок. Итак, есть про грамма, которая работает определенным образом. Наконец, многие драйверы устройств вырабатывают сигналы, когда возникают ошибки или определенные ситуации. Дисковод может послать сигнал, когда заканчивается копирование блока данных с диска в память. Программа должна реагировать на поступление этих сигналов.
224
Программирование дружественного способа управления терминалом и сигналы
Заключение Основные щей •
•
•
•
Некоторые программы обрабатывают данные, поступающие от конкретных устройств. Эти программы, ориентированные на работу с устройствами, могут управлять соединениями с такими устройствами. Наиболее общим устройством для систем Unix является терминал. Драйвер терминала имеет много установок. Набор установок образует режим работы драйвера терминала. Программы пользователей часто устанавливают требуемые режимы драйвера терминала. Клавиши, на которые нажимают пользователи, можно сгруппировать по трем категориям. Драйвер терминала управляет этими категориями по-разному. Большинство клавиш служат для представления обычных данных. При нажатии на эти ключи драйвер передает эти данные программе. Некоторые клавиши служат для инициализации функций редактирования в самом драйвере. Если нажать на клавишу erase, то драйвер удалит набранный предшествующий символ из его буфера строки и пошлет необходимый код на экран терминала для удаления этого символа с экрана. Наконец, при нажатии на некоторые ключи вызываются функции управления процессом. Ключ Ctrl-C при нажатии требует от ядра вызвать некоторую функцию ядра, а эта функция пошлет сигнал процессу. В драйвере терминала поддерживается ключи для вызова нескольких функций управления процессами. Все они предполагают посылку сигналов процессу. Сигнал является коротким сообщением, которое передается от ядра процессу. Инициировать появление сигнала могут пользователи, другие процессы и само ядро. Процесс сообщает ядру, как он хотел бы реагировать на появление сигнала.
Что дальше? Unix-машина все время принимает данные от многих терминалов и других устройств. Пользователи производят данные, передаваемые через терминал, в непредсказуемые моменты времени. Ядро должно обрабатывать данные, которые возникают при нажатии на клавиши. На Unix-машине сразу исполняется несколько программ. Как ядро поддержи вает одновременно развитие программ и правильно реагирует на множество непредска зуемых во времени прерываний? Мы изучим эти вопросы при написании видеоигры.
Исследования 6.1 Многие программные инструментальные средства в Unix читают данные из файлов, имена которых задаются в командной строке. В команде tr этого нет. Каково назначе ние команды tr? Каковы причины, на ваш взгляд, того, что в этой команде явно не ука зываются имена файлов? Есть ли еще другие средства в Unix, где чтение происходит только из стандартного ввода, а не из поименованных файлов? Большинство команд в Unix находятся в каталогах, которые называются /bin, /usr/bin и /usr/local/bin.
Заключение
225
6.2 Режим без блокирования для других файлов. Атрибут ONDELAY можно, использо вать для произвольного дескриптора, а не только для драйвера терминала. Это означа ет, что такой атрибут может быть использован в отношении дисковых файлов, а также в отношении файлов устройств. Что будет означать режим без блокирования при работе с дисковым файлом? Что бу дет означать режим без блокирования при работе с устройствами, которые не являют ся терминалами?
Программные упражнения 6.3 Режим some-delay. Файловый дескриптор можно установить в блокированный режим или в режим no-delay. Драйвер терминала обеспечивает прекрасный контроль. Он по зволяет вам установить величину таймаута на ввод данных. В массиве управляющих символов с_сс[], который находится в структуре struct termios драйвера, есть элемент в позиции VTIME. В нем устанавливается значение периода таймаута в десятых до лях секунды. Таким образом, присвоение значения вида s.c_cc[VTIME] = 20 означает установку для драйвера таймаута в две секунды. Модифицируйте play_again3.c так, чтобы программа использовала средство таймаута в драйвере, а не переводила бы файловый дескриптор в неблокируемый режим. 6-4 Управление сигналами в программе play_again. (a) Модифицируйте программу play_again3.c так, чтобы сигналы от клавиатуры игнорировались, а программа реагировала бы только на ввод сообщений вида: yes или по. (b) Модифицируйте программу play_again3.c так, чтобы по мере приема сигнала от клавиатуры она переустанавливала бы установки терминала и осуществляла выход с кодом возврата 2. 6.5 Модифицируйте программу rotate, с так, чтобы она сама изменяла бы режимы терми нала. Модифицированная программа должна выключать канонический режим и вы ключать режим эхоотображения. Затем она должны читать символы и для каждого введенного символа выводить следующую по алфавиту букву. При нажатии пользова телем на клавишу “Q” программа должна восстановить установки терминала и за кончить работу. Ваша программа должна игнорировать сигналы от клавиатуры и управлять ими по средством переустановки^ драйвере перед выходом. 6.6 Напишите строковый редактор. Одна из проблем при написании программы, которая работает в неканоническом режиме, заключается в следующем. Становится невозможным вести редактирование при вводе данных. Модифицируйте вашу измененную версию про граммы rotate.c так, чтобы можно было вести посимвольное и построчное редактирова ние. В частности, когда программа принимает символы backspace или delete, то она долж на стирать предшествующий символ с экрана. Для стирания символа ваша программа должна вывести символ backspace, символ пробела и опять символ backspace. Также модифицируйте программу с тем, чтобы она отрабатывала бы символ уничто жения строки (line-kill) так же, как это делает драйвер терминала, т. е. программа должна уничтожать на экране все символы, которые были набраны в текущей строке. Что вам потребуется, чтобы реализовать функцию удаления слова, которая поддержи вается в драйвере?
226
Программирование дружественного способа управления терминалом и сигналы
6.1 Модифицируйте программу sigdemol .с так, чтобы она производила бы подсчет числа нажатий пользователем на ключ Ctrl-C. Модифицированная версия должна выводить сообщение OUCH!, затем OUCH!! и т. д., где число восклицательных знаков равно числу нажатий на ключ в данный момент. Кроме вывода на экран возрастающего числа восклицательных знаков программа должна воспринимать целочисленный аргумент, значение которого задается при обращении к программе. После того как пользователь нажал на ключ Ctrl-C столько раз, сколько было задано с помощью аргумента, программа должна закончить работу. 6.8 Вы уверены? Модифицируйте программу sigdemol .с так, чтобы она запрашивала бы у
пользователя ответ, на самом ли деле он хочет завершить программу. Результат рабо ты с программой может выглядеть так: hello hello Interrupted! OK to quit (у/n)? n hello hello Interrupted! OK to quit (у/п)? у
$ Что произойдет, если пользователь нажмет ключ Ctrl-C, когда программа находится в ожидании ответа на вопрос OK to quit (у/п)? Напишите код и посмотрите, что проис ходит при его работе. (Данное упражнение не своевременно. В тексте еще не говори лось о том, что происходит, если во время обработки сигнала этот же сигнал поступа ет еще раз. Причем, и решение может быть разным - в зависимости от того, как реа лизована логика системного вызова signal. - Примем. ред.) 6.9 Программа может использовать системный вызов signal, чтобы сообщить ядру, что она хотела бы проигнорировать определенные сигналы, такие как SIGINT и SIGQUIT. Есть разные стратегии по предотвращению выработки этих сигналов. Драйвер терми нала имеет флаг ISIG. Обратитесь к документации и ознакомьтесь с назначением это го флага. Затем перепишите программу sigdemo2.c так, чтобы она использовала бы этот флаг. Что будет делать модифицированный вариант программы, если при ее ис полнении будет принят сигнал SIGINT, который поступил откуда-то, а не с клавиа туры? Ознакомьтесь с командой kill и используйте команду kill для посылки сигнала SIGINT процессу, где выполняется версия программы, в которой сброшен флаг ISIG. 6.1Q Прерывания не всегда являются деструктивными. Представьте себе, что вы работаете над проектом в течение нескольких дней. Вы можете получать телефонные звонки от вашего босса, который интересуется, как у вас идут дела. Такого рода прерывания разработаны с целью запуска подпрограммы оповещения о состоянии, а не с целью убить процесс. Напишите программу на С, которая выполняет задачу, требующую много времени. Например, напишите программу, которая находит простые числа с ис пользованием некоторого медленного метода. Программа должна отслеживать получение самого большого простого числа на текущий момент. Добавьте к этой про грамме функцию обработки сигнала SIGINT, которая выводит краткий отчет, где показывается, сколько чисел она проверила и самое большое простое число, которое она обнаружила. Как может быть использована эта идея в системных программах?
Заключение 6.11 Возврат к программе more. В главе 1 мы написали несколько версий утилиты more.
В тот момент мы не знали, как работает драйвер терминала. Усовершенствуйте эту программу так, чтобы она работала в режиме no-echo, в неканоническом режиме и правильно реагировала бы на сигнал прерывания и сигнал kill. 6.12 Сигналы и окна. Пользователь может вырабатывать сигналы не только по мере нажа тия на определенные ключи, но также и при изменении размера окна терминала. Каж дый раз, когда меняется размер окна, процессу посылается сигнал SIGWINCH. ' По умолчанию процесс игнорирует сигнал SIGWINCH. Напишите программу, которая заполняет экран терминала выводом большого количества символов аА” в от дельные позиции экрана. Например, если окно имеет десять строк и двадцать коло нок, то программа должна вывести символ “А” двести раз. Когда будут изменены раз меры окна, то программа должна будет заполнить окно символами к‘В”. При сле дующем изменении размера следует использовать символ “С” и т. д. Когда пользова тель нажмет на клавишу “Q”, то окно должно быть очищено, а программа должна быть закончена. Когда пользователь нажмет любую клавишу, то программа начнет заполнять окно символом “А”.
Глава 7 Событийно-ориентированное программирование. Разработка видеоигры
Цели Идеи и средства • • • • • •
Программы управляют асинхронными событиями. Библиотека curses: назначение и использование. Интервальные таймеры и будильник. Управление надежными сигналами. Повторно входной код, критические секции. Асинхронный ввод.
Системные вызовы и функции • • • •
alarm, setitimer, getitimer kill, pause sigaction, sigprocmask fcntl, aio_read
7.1. Видеоигры и операционные системы Деннису Ричи и Кену Томпсону из Bell Labs захотелось поиграть в видеоигру Space Travel (Космическое путешествие). Поэтому они и создали Unix. Ричи писал: Да, в течение 1969 года Томсон разработал игру Space Travel. Сначала игра была напи сана в среде Multics, а затем была переписана на Фортране в среде GECOS (операцион ная система для машины GE, позже - Honeywell 635). Игра была ничем иным, как эму
7.1. Видеоигры и операционные системы
229
ляцией движения крупных тел в Солнечной системе, где под управлением игрока пере мещался космический корабль в соответствии со сценарием, и который нужно попы таться посадить на различные планеты и луны. Версия GECOS была неудовлетвори тельной в части двух важных позиций: во-первых, отображение состояний в игре проис ходило скачкообразно, а дисплей был слишком грубым для управления, поскольку для этого использовались команды; so-вторых, для ведения игры нужно было заплатить около $75 за время использования центрального процессора большого компьютера. Но это продолжалось недолго, т. к. Томпсон нашел почти неиспользуемый компьютер PDP-7 с прекрасным дисплейным процессором, где была установлена система GraphicII terminal. Он и я переписали Space Travel с тем, чтобы можно было работать на этой машине. Сделанное оказалось по значимости большим, чем вначале казалось. Посколь ку мы отказались от всего имеющегося программного обеспечения, то вынуждены были написать пакет для плавающей арифметики, спецификацию графических символов для дисплея и подсистему отладки, которая постоянно выводила содержимое в области для отображения в углу экрана. Все это было написано на ассемблере для кросс-ассемблера, который работал под GECOS и выдавал результат на перфоленты, которые можно был переносить на PDP-7. Программа Space Travel, хотя и была сделана как весьма привле кательная игра, рассматривалась в основном как нововведение в неуклюжую техноло гию подготовки программ для PDP-7. Вскоре Томпсон начал реализацию “бумажной” файловой системы (возможно, более точно было бы ее назвать ‘‘меловой” файловой системой), которая была разработана ранее. Файловая система без экспериментального использования - это всего лишь чистое предположение. Так что он реализовал во плоти то, что удовлетворяло различным требованиям работающей операционной системы, в частности удовлетворяло введен ному понятию процесса. Затем появился небольшой набор утилит пользовательского уровня (средства для копирования, печати, удаления и редактирования файлов), и, естественно, появился простой командный интерпретатор (shell). До этого момента все программы писались в среде GECOS, а файлы переносились на PDP-7 с помощью перфоленты. Но поскольку была завершена разработка ассемблера, то система стала в состоянии поддерживать сама себя. Несмотря ни на что, но после того как в 1970 году, Brian Kemighan предложил имя "Unix”, в определенном смысле язвительный каламбур в отношении "Multics", система, которую мы сегодня знаем, родилась1. Видеоигры и операционные системы имеют много общего. В этой главе мы напишем про стую видеоигру. При разработке этой игры нам потребуется использовать определенные виды Unix-сервисов, а также использовать базовые принципы и средства, свойственные собственно проекту операционных систем. Что делает видеоигра Рассмотрим видеоигру space travel (космическое путешествие) с двумя игроками. Про грамма создает образы планет, астероидов, космических кораблей и поддерживает ряд образов перемещений. Каждый объект характеризуется скоростью, положением, направ лением движения, моментом движения и другими атрибутами. Астероид может столк нуться с космическим кораблем или с другим астероидом.
1. AT&T Bell Laboratories Technical Journal 63 No. 6 Part 2, October 1984, p. 1577-1593.
230
Событийно-ориентированное программирование. Разработка видеоигры
В игре также поддерживается ввод информации от пользователя. Игроки оперируют кноп ками, мышью и трек-боллами. При этом в непредсказуемые моменты времени поступают входные данные, а программа должна быстро на это реагировать. Такие входные события действуют на атрибуты объектов в игре. При нажатии на кнопку пользователь может уве личить скорость и уменьшить массу корабля. От изменений атрибутов корабля будет зави сеть, как он будет взаимодействовать с другими объектами.
Как работает видеоигра В видеоигре реализуется несколько базовых идей и принципов: Пространство В игре можно перемещать образы в определенные места на экране ком пьютера. Как программа выполняет управление дисплеем? Время Образы могут перемещаться по экрану с различными скоростями. Изме нение положения производится с определенной периодичностью. Как программа отслеживает течение времени и планирует выполнение опре деленных действий в определенное время? Прерывания Программа непрерывно перемещает объекты по экрану, но пользователи могут посылать свои входные данные тогда, когда они пожелают. Как программа реагирует на прерывания?
Выполнение нескольких действий В игре поддерживается перемещение нескольких объектов и одновременно поддержива ется обработка прерываний. Как программа может управлять множеством активностей и при этом не запутаться?
В операционных системах решаются те же вопросы В операционных системах возникают те же самые четыре проблемы. Ядро загружает програм мы в пространство памяти и учитывает места распределения для каждой программы. Ядро планирует порядок выполнения программ на коротких интервалах времени, а также планирует выполнение ряда внутренних задач, которые должны быть выполнены к определенному моменту времени. Пользователи и другие внешние устройства посылают свои входные данные в непредсказуемые моменты времени, а ядро должно быстро реагировать на их появление. Одновременное выполнение нескольких работ может быть весьма сложным. Как ядро предо храняет обработку данных от возможности наступления беспорядка и хаоса?2
Управление экраном, время, сигналы, разделение ресурсов В этой главе мы будем изучать вопросы управления экраном, временем, сигналами, а так же вопросы, которые связаны с успешным выполнением нескольких параллельных работ. Мы напишем ряд анимированных игр для текстового терминала, чтобы изучить проблема тику указанных ранее четырех фундаментальных тем.
Почему символьно ориентированная графика? Почему не используется более мощное программирование в среде XII или графические средства Java? Есть несколько^соображений по этому поводу. Во-первых, символьно ори ентированные игры аналогичны по сути графическим играм с высоким разрешением. Они отличаются лишь “более объемными” пикселями. Следующее соображение - перемещае мость. Символьно ориентированные игры требуют только терминального эмулятора и со единения с ним, что является доступным на любой вычислительной системе. В-третьих, 2. Обратитесь к Web для поиска тем по ключевым словам DEC Wars и Unix Wars. После чего вы можете ознако миться с информацией, где прослеживается аналогия между Unix и космическими кораблями.
7.2. Проект: Разработка pong-игры в настольный теннис для одного игрока
231
при сокращении времени на рассмотрение графических вопросов увеличивается время на рассмотрение вопросов системного программирования. Если вам это необходимо, то мож но обратиться к Web-сайту, где есть версии программ, которые работают с графической системой X windows.
7.2. Проект: Разработка ропд-игры в настольный теннис для одного игрока
Рисунок 7.1 Видеоигра для одного игрока Давайте начнем. Основной проект в этой главе - игра pong, которая является версией с од ним игроком из набора классических аркадных и развлекательных игр. На рисунке 7.1 изображены три основных элемента: стенки, шарик и ракетка. Обобщенная схема работы программы такая: (a) Шарик движется с некоторой скоростью. (b) Шарик отскакивает от стенок и ракетки. (c) Пользователь, нажимая на ключи, может перемещать ракетку вверх и вниз. Разработка такой игры потребует понимания вопросов управления экраном, временем, прерываниями, а также потребует решения вопросов одновременного выполнения не скольких действий. Каждая из названных тем будет изучена позже.
7.3. Программирование пространства: Библиотека curses Библиотека curses - это набор функций, с помощью которых программист может устанавли вать положение курсора и управлять выводом текста на экране терминала. Библиотека curses или просто curses, как она была вначале названа, была разработана в UCB Olden Days ®. Боль шая часть программ, которые управляют экраном терминала, используют curses. Изначально это был просто набор функций. Теперь curses обладает многими изощренными средствами. Мы будем использовать лишь небольшую часть этих средств.
7.3.1. Введение в curses Curses поддерживает представление терминального экрана в виде двумерного массива, состоящего из символьных ячеек, каждая из которых идентифицируется на экране парой {строка, колонка). Началом координат является верхний левый угол экрана. Номера строк возрастают в направлении сверху вниз, а номера колонок возрастают в направлении слева направо. На рисунке 7.2 представлен экран, образ которого поддерживает curses.
Событийно-ориентированное программирование. Разработка видеоигры
232
В составе curses есть функции для перемещения курсора в любую точку экрана, для добав ления символов и стирания символов с экрана. Кроме того, есть функции для установле ния атрибутов символов, таких, как цвет и яркость. Имеются функции для создания и управления окнами и другими текстовыми областями. Все функции описаны в докумен тации. Мы будем использовать девять из них:
Базовые функции curses initscr()
Инициализация библиотеки curses и терминала
endwinQ
Выключение curses и сброс терминала
refresh()
Воспроизведение экрана в таком виде, как вы желаете
movefr.c)
Перемещение курсора в позицию на экране f t t c )
addstr(s)
Прорисовка строки s на экране от текущей позиции
addch(c)
Прорисовка символа £на экране в текущей позиции
clear()
Очистка экрана
standout!)
Включение режима standout (обычно обратное изображение)
standendQ
Выключение режима standout
Curses, пример 1: hellol.c В этой первой программе показывается базовая логика curses-программ: Л hellol.c * назначение - представление минимального числа средств, которые используются в>curses для инициализации, прорисовки, ожидания ввода и для выхода
7 «include «include main()
<stdio.h> <curses.h>
{ initscrO;
I* включить curses 7
233
7.3.Программированиепространства:Библиотекаcurses /* послать запросы */ clear(); addstrf'Hello, world"); move( LINES-1,0); refresh!); getch(); endwinf);
/*. очистить экран */ Г добавить текст строки */ Г переход к LL*/
Г обновит*, экран */ /* ожидать ввода от пользователя */ /* выключить curses */
Компиляция и запуск программы на исполнение г: толняются весьма просто: $ сс hellol .с -Icurses -о hellol $./hello1 Выходной экран показан на рисунке 7.3. Программа работает с любым терминальным соединением на любом компьютере с любой версией Unix
Curses, пример 2: hello2.c
Построение более сложных изображений достигается совместным использованием циклов, переменных и различных функций curses. Попробуйте предугадать - какой будет результат работы вот такого второго примера:
г hello2.c * *
*/
назначение - представление использования функций curses совместно с циклами при инициализации, прорисовке, завершении
«include «include main()
<stdio.h> <curses.h>
int initscr();
/* включение curses */ clearQ; Г прорисовка чего-либо */ for(i=0; KLINES; i++){ /* в цикле */
move(i, i+i); if (i%2 ==1) standout!); addstrf’Hello, world");
234
Событийно-ориентированное программирование. Разработка видеоигры standend(); .
} refresh(); getch(); endwin();
/* обновить экран */ /* ожидать ввода от пользователя */ /* сброс tty и прочее */
} Откомпилируйте и запустите программу на исполнение. Насколько правильными оказа лись ваши прогнозы?
7.3.2. Внутренняя архитектура curses: Виртуальный и реальный экраны Что делает функция refresh? Поэкспериментируем. Закомментируйте эту строку (В при мере hello2.c строка refresh(). - Примеч. пер.), повторно откомпилируйте и запустите про грамму. На экране ничего не появится. Curses был разработан так, чтобы можно было изменять содержимое текстового экрана без “засорения” коммуникационной линии. Curses минимизирует поток данных за счет то го, что работает с виртуальными экранами (смотри рисунок 7.4).
Реальный экран - это массив символов, который находится непосредственно перед глаза ми пользователя. В curses поддерживаются две разновидности экрана. Первый внутрен ний экран - это копия реального экрана. Второй внутренний экран - это рабочее простран ство, где записываются изменения на экране. Каждая из функций move, addstr и т. д. моди фицирует символы, находящиеся в пределах экрана рабочего пространства. Большинство функций библиотеки curses действует только в отношении рабочего пространства, что на поминает буферирование для диска. Функция refresh сравнивает экран рабочего пространства с копией реального экрана. Функция refresh помимо этого выдает с помощью драйвера терминала символы и коды по управлению экраном, которые необходимы для установления соответствия между реаль ным экраном и рабочим экраном. Пусть, например, в левом верхнем углу на реальном экране в текущий момент визуализирована строка Smith, James. Если вы затем используе те функцию addstr, чтобы поместить на это же место строку Smith, Jane, то вызов функции refresh приведет только к тому, что в слове James будет заменен символ т на символ п, а символ s будет заменен пробелом. Такая техника, когда происходит передача не самих образов, а только изменений в образах, используется в потоковом видео.
7.4. Программирование времени: sleep
235
7.4. Программирование времени: sleep При разработке видеоигры будем помещать образы в определенные места и будем делать эго в определенное время. Для размещения образов в определенных местах мы будем ис пользовать curses. Добавим теперь в наши программы средства, которые позволяют разре шать временные вопросы. Сначала используем системную функцию sleep. Анимация, пример 1: ЬеПоЗ.с г hello3.c * назначение - использование refresh и sleep для поддержки анимационных эффектов * представление инициализации, прорисовки, завершения
7 «include «include main()
<stdio.h> <curses.h>
{ int i; initscr(); clear(); for(i=0; KUNES; i++){ move(i, i+i); if (i%2 — 1) standout!); addstrf'Hello, world"); if (i%2 ==1) standendf); sleep(1); refresh!);
*
} endwin();
} После компиляции и запуска этой программы вы увидите приветственное сообщение, которое каскадно будет перемещаться вниз по экрану со скоростью одна строка в секунду. Сообщение будет отображаться в инверсном режиме. Зачем нам необходимо вызывать re fresh в каждой итерации цикла? Анимация, пример 2: hello4.c /* hello4.c * назначение - показать, как используются erase, time и draw при анимации
7 «include <stdio.h> «include <curses.h> Hello, world main()
{ int i; initscr(); clearO;
236
Событийно-ориентированное программирование. Разработка видеоигры
for(i=0; KUNES; i++){ move(i, i+i); if (i%2 ==1) standout!); addstr("Hello, world"); if (i%2 == 1) standend(); 4 refresh!); sleep! 1); move(i,i+i); addstrf'");
/* переместиться обратно в ту же позицию * / Л стереть строку */
) endwin();
} Программа helio4 создает иллюзию движения. Сообщения выводятся постепенно, в на правлении вниз по диагонали. Наш секрет заключается в том, что происходит прорисовка сообщения в одном месте, затем визуализируется состояние экрана в течение одной секун ды, потом на место, где было выведен текст сообщения, выводится пустая строка, чтобы стереть это сообщение. Затем происходит смена места вывода сообщения. Заметим, что вызовом refresh после двух запросов (Запросов на включение режима. - Примеч. пер.), мы гарантируем, что старое сообщение исчезнет и появится новое со г 5щ“ние в одном прохо де. На рисунке 7.5 приведен “snapshot ” (мгновенный снимок) экрана.
Анимация, пример 3: hello5.c /* hello5.c * назначение - показать, как сообщение отражается от границы и опять * движется по экрану * компиляция: сс hello5.c -Icurses -о hello5
7 «include <curses.h> «define LEFTEDGE «define RIGHTEDGE
10 30
7.4.Программированиевремени:sleep
237
«define ROW 10 main()
{ char messaged = "Hello"; char blankQ =" ”; int dir = +1; int pos = LEFTEDGE; initscr(); clear(); while(1){ move(ROW.pos); addstr(message); /* прорисовать строку */ move( LINES-1 rCOLS-1); /* “парковка" курсора */ refresh(); f* показать строку */ sleep(1); move(ROW.pos); /* стереть строку */ addstr(blank); pos += dir; /* сменить позицию для вывода */ if (pos >= RIGHTEDGE) /* проверить на необходимость отражения */ dir = -1; if (pos <= LEFTEDGE) dir = +1;
}
} Переменная dir используется для управления скоростью вывода сообщения. Если значе ние переменной dir равно +1, то изображение текста сообщения смещается каждую секун ду на одну позицию вправо. Когда значение переменной dir равно -1, то изображение тек ста сообщения смещается каждую секунду на одну позицию влево. При изменении знака у значения переменной dir происходит и смена направления перемещения текста сообще ния. На рисунке 7.6 показан “снимок” экрана в некоторый момент времени.
Как мы все делали? Достаточно ли мы узнали, для того чтобы написать видеоигру со скромным перечнем дей ствий? Мы узнали, как воспроизвести символ в указанном месте строки на экране. Мы знаем, как добиться анимационного эффекта с помощью введения временных за держек между прорисовками, стираниями и перерисовками. Наша программа хороша, но:
Событийно-ориентированное программирование. Разработка видеоигры
238
(a) Односекундные задержки являются слишком большими. Нам требуется более лучший способ управления временем. (b) Нам необходимо добавить средство ввода информации от пользователя. Эти две проблемы приводят нас к рассмотрению двух новых тем: программирование вре мени и техника расширенных сигналов. Через несколько страниц текста мы вернемся к игре.
7.5. Программирование времени 1: ALARMS (Перевод alarm далее сознательно не делается, хотя часто встречается буквальный перевод "аларм” или “сигнал тревоги” или “будильник”. - Примеч. пер.) Работа со временем мо жет проводиться в программах по-разному. В программе в поток управления может быть введена временная задержка. В последних трех примерах для введения задержки была ис пользована функция sleep. Другая возможность использовать время - планирование вы полнения некоторого действия через некоторое время. Это техника, которая базируется на бытовом использовании таймера при варке яиц. Вы можете заниматься какими-то своими делами до тех пор, пока таймер не подаст звуковой сигнал. В Unix для этой цели исполь зуют системный вызов alarm.
7.5.1. Добавление задержки: sleep Для того чтобы добавить в программу временную задержку, используют функцию sleep: sleep(n)
Функция sleep(n) обеспечивает задержку развития текущего процесса на п секунд или до момента, когда процессу будет передан сигнал, который не будет им проигнорирован.
7.5.2. Как работает s/eep(J: Использование alarms в Unix Функция sleep работает по сценарию, которым вы пользуетесь, когда ложитесь спать и хо тите проснуться через установленное время: (a) Следует установить будильник на выдачу сигнала побудки через желаемое число секунд, в течение которых вы будете спать. (b) Наступает пауза до тех пор, пока не истечет временная уставка будильника. На рисунке 7.7 проиллюстрирована основная идея. Каждый процесс в системе имеет соб ственный будильник. Этот будильник, аналогичный кухонному таймеру, можно заставить зазвонить после того, как истечет заказанный временной интервал в секундах.
Как работает функция sleep:
signal(SIGALRM,handler); alarm(n); pause(); Каждый процесс имеет собственный таймер
Рисунок 7.7 Процесс устанавливает alarm, в течение которого он приостанавливает свое развитие
7.5. Программирование времени 1: ALARMS
239
Когда истечет установленное время, будильник посылает процессу сигнал SIG ALRM. Если в процессе не был установлен обработчик сигнала SIGALRM, то этот сигнал убьет про цесс. Таким образом, функция sleep состоит из трех шагов: 1. Установка обработчика сигнала SIGALRM. 2. Вызов alarm(num_seconds). 3. Вызов pause. Системный вызов pause приостанавливает процесс до тех пор, пока процесс не примет сиг нал. Причем любой сигнал, а не обязательно сигнал SIGALRM. Теперь соединим воедино эти идеи и напишем такой код: Л sleep 1.с назначение - показать, как работает sleep * обращение - sleep 1 * Программа представляет, как устанавливается обработчик сигналов, как * устанавливается alarm, как устанавливается пауза, а затем происходит * ’ продолжение действия.
7 tinclude #include // tdefine main()
<stdio.h> <signal.h> SHHHH
{ void wakeup(int); printff’about to sleep for 4 seconds\n"); signal (SIGALRM, wakeup); alarm(4); pausef); printff'Moming so soon?\n”);
/* перехватить сигнал */ Г установить будильник */ /* здесь замереть 7 Г возобновить работу */
} void wakeup(int signum)
{ #ifndef SHHHH printff'AIarm received from kernel\n"); #endif
} Мы обратились к signal, чтобы установить функцию обработки сигнала SIGALRM. Затем, с помощью обращения к alarm, была установлена уставка таймера, равная четырем секун дам. Наконец, при обращении к pause произведена задержка развития процесса до момента поступления сигнала от таймера. По мере истечения интервала в четыре секунды, что кон тролируется таймером, ядро пошлет процессу сигнал SIGALRM. В результате происходит передача управления от строки pause на обработчик сигнала. После выполнения кода обра ботчика сигналов происходит возврат управления. Выполнение действий по перехвату сигнала приводит к выходу из pause, и процесс возобновляет свое развитие. На рисунке 7.8 иллюстрируется обобщенное представление о работе системного вызова pause.
240
Событийно-ориентированное программирование. Разработка видеоигры
Ниже приводится более детальная информация, относящаяся к alarm и pause: alarm НАЗНАЧЕНИЕ
Установить уставку таймера для выдачи сигнала
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
unsigned old = alarm(unsigned seconds)
АРГУМЕНТЫ
seconds - длительность интервала ожидания
КОДЫ ВОЗВРАТА
-1 при ошибке остаток времени до окончания предшествующей уставки
С помощью alarm вызывающий процесс производит установку таймера на указанное число секунд seconds. Когда это время истечет, ядро пошлет процессу сигнал SIGALRM. Если тай мер уже был установлен, то после выполнения вызова alarm возвращается число оставшихся секунд до истечения установленного ранее интервала. (Замечание. При обращении вида alarm(O) имеющаяся на таймере уставка сбрасывается.) pause НАЗНАЧЕНИЕ
Ожидать прихода сигнала
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
result = pauseQ
АРГУМЕНТЫ
Нет аргументов
КОДЫ ВОЗВРАТА
Всегда -1
Системный вызов pause задерживает вызывающий процесс до момента поступления сиг нала. Если вызывающий процесс будет закончен при поступлении сигнала, то возврата из pause не произойдет. Если вызывающий процесс перехватывает сигнал с помощью обра ботчика, то после обработки сигнала управление передается на команду, которая следует за pause. В этом случае значение переменной errno становится равным EINTR.
7.6. Программирование времени II: Интервальные таймеры
241
7.5.3. Планирование действий на будущее Другой вариант использования времени - произвести планирование выполнения действия через какое-то время, а пока заняться чем-либо. Планирование выполнения действия в буду щем выполняется весьма просто. Достаточно установить требуемое время начала выполнения действия на таймере с помощью обращения к alarm, а затем выполнять какую-либо работу. Когда время, отслеживаемое таймером, истечет, т. е. достигнет нуля, то процессу будет послан сигнал. Далее будет произведена передача управления обработчику сигнала.
7.6. Программирование времени II: Интервальные таймеры Системные вызовы sleep и alarm были включены в Unix очень давно. Они обеспечивают временное разрешение с точностью в одну секунду, что является весьма грубым для мно гих приложений. По мере развития Unix была добавлена более мощная и всеобъемлющая таймерная система. Эта новая система, где была использована концепция интервального таймера, имеет более высокое разрешение и допускает использование не одного, а трех таймеров для каждого процесса. И это еще не все. Для каждого из этих таймеров допусти мо использовать две установки: установка режима единичного alarm и установка режима периодичного использования (зацикливания) таймера. Кроме того, все еще поддержи вается системные вызовы alarm и sleep, поскольку они вполне удовлетворяют по своим возможностям многие приложения. Идея иллюстрируется на рисунке 7.9.
Мы можем использовать эту новуиэ систему для добавления задержек и для планирования событий.
7.6.1. Добавление улучшеной задержки: us/eep В программе можно использовать возможности улучшенной задержки usleep: usleep(n)
При использовании usleep(n) происходит задержка текущего процесса на п микросекунд или до момента поступления не перехваченного сигнала.
7.6.2. Три вида таймеров: реальные, процессные и профильные Процессы имеют возможность измерять три вида времени. Рассмотрим программу, ко торая заканчивается через 30 секунд после своего запуска на исполнение. Программа не будет исполняться в течение всего этого интервала времени, если она исполняется в сис теме разделения времени. Процессор будут исполнять также и другие программы, испол нять в определенной очередности во времени. На рисунке 7.10 показана диаграмма разви тия событий, которые могли произойти в течение этих 30 секунд:
242
Событийно-ориентированное программирование. Разработка видеоигры
На диаграмме показано, что на интервале от 0 до 5 секунд процесс работает в пользова тельском режиме. Затем он переходит в состояние ожидания и спит на интервале от 5 до 15 секунд. После происходит переход в режим ядра на интервале, который длится до 20 секунд. Далее происходит возврат в режим ожидания и т. д. На этом интервале в 30 секунд от начала программы до ее окончания программа использовала 10 секунд пользователь ского времени и 5 секунд системного времени. На диаграмме представлены три разновид ности времени: реальное, пользовательское и пользовательское+системное. Ядро позво ляет измерять время для каждой из этих трех разновидностей. Имена этих трех таймеров такие:
1TIMERJREAL Этот таймер “тикает” в реальном времени, т. е. просто измеряет время независимо от того, сколько при этом процесс использовал времени процессора, находясь в пользовательском или системном режимах. По истечении утановлепного временного значения для этого таймера он посылает сигнал SIGALRM.
ITIMER_ VIRTUAL Этот таймер работает аналогично использованию часов во время футбольного матча. Он “тикает” только, когда процесс находится в пользовательском режиме. Тридцать секунд, которые будут отсчитаны на виртуальном таймере, будут длиннее, чем тридцать секунд реального времени. Виртуальный таймер посылает сигнал SIGVTALRM по истечении уста новленного для него значения временного интервала.
ITIMERJPROF Этот таймер работает и измеряет время, когда процесс находится в пользовательском ре жиме, а также, когда ядро исполняет системные вызовы, которые были запущены по ини циативе процесса. Когда для этого таймера будет исчерпано установленное значение вре менного интервала, то таймер посылает сигнал SIGPROF.
7.6.3. Два вида интервалов: начальный и период Доктор дает вам пилюлю и говорит ’’примите лекарство через час и далее будете прини мать через каждые четыре часа”. В ответ на это вам нужно установить таймер так, чтобы он сработал через час, а затем, когда он в очередной раз сработает, вы будете переустанав ливать его каждые четыре часа. В каждом интервальном таймере можно выполнить уста новку этих двух временных значений: начальное значение интервала и значение периода
7.6. Программирование времени И: Интервальные таймеры
243
повторения. В структуре, которая используется интервальным таймером, начальный ин тервал задается с помощью члена структуры it__value, а значения периода задаются с помо щью члена структуры itjnterval. Если вам не нужна последующая периодичность работы терминала, то нужно установить значение itjnterval в ноль. Для сброса обоих уставок нуж но установить в ноль значение it_value.
7.6.4. Программирование с помощью интервальных таймеров Программировать на основе использования alarm достаточно легко. Вы просто задаете при вызове alarm требуемое число секунд. Программирование с помощью интервального тай мера является более сложным. Вам будет необходимо выбрать вид времени, а затем решить, каково должно быть значение начального интервала и значение периода. Кроме того, вы должны будете записать выбранные значения времен в структуру struct itimerval. Например, чтобы интервальный таймер напоминал бы вам о порядке приема лекарства в соответствии с планом, который был описан в предшествующем параграфе, нужно будет присвоить члену структуры it_value значение, равное одному часу, а члену структуры itjnterval присвоить значение 4 часа. Затем следует переслать эту структуру таймеру, используя для этого setitimer. Для того чтобы прочитать значения установок таймера, Сле дует обратиться к getitimer. Иллюстрация этой концепции представлена на рисунке 7.11.
Пример использования интервального таймера: ticker_demo.c В программе ticker_demo.c показывается, как можно использовать интервальный таймер: Г ticker_demo.c * демонстрируется использование интервального таймера для выработки * последовательности сигналов, которые в свою очередь перехватываются и * используются для декремента счетчика
7 «include «include «include int main{)
{
<stdio.h> <sys/time.h> <signal.h>
void countdown(int); signal(SIGALRM, countdown); if (set_ticker(500) ==-1)
244
Событийно-ориентированное программирование. Разработка видеоигры perror(”set_ticker"); else while(1) pause(); return 0;
} void countdown(int signum)
{ static int num = 10; printf("%d..", num-); fflush(stdout); if (num < 0){ printf("DONE!\n"); exit(0);
} } /* [из setticker.c] * set_ticker(number_of_milliseconds) * настраивает интервальный таймер на выдачу сигнала SIGALRM с * установленной периодичностью * код возврата -1 - при ошибке, 0 - при успешном окончании * Значение аргумента, которое задается в миллисекундах, преобразуется в * секунды и микросекунды * Замечание: обращение вида set_ticker(0) сбрасывает установки для ticker
*/ int set_ticker(int n_msecs)
{ struct itimerval new_timeset; long n_sec, n_usecs; % n_sec = n_msecs /1000; [* целая часть */ n_usecs = (n_msecs % 1000) * 1000L; /* остаток */ new_timeset.it_interval.tv_sec = n_sec; /* установка перезагрузки */ new_timeset.it_value.tv_sec = n_sec; /* сохранить это */ new_timeset.it_value.tv_usec = n_usecs; /* и это */ return setitimer(ITIMER_REAL, &new_timeset, NULL);
), Отследим поток управления для программы ticker_demo.c. Сначала мы использовали signal для установки функции countdown, которая должна обрабатывать сигнал SIGALRM. Сигнал должен возникнуть по истечении интервала, длительность которого задается при обраще нии к set_ticker. Функция set_ticker устанавливает интервальный таймер, что делается загрузкой значения начального интервала и значения периода. Каждый из этих интервалов представлен значе ниями, каждое из которых хранится в двух разных видах: значение, измеряемое в секун дах, и значение, измеряемое в микросекундах, что эквивалентно представлению вещест венного числа с помощью целой и дробной частей. После того как таймер ’’затикает”, управление передается в функцию main.
7.6. Программирование времени И: Интервальные таймеры
245
После передачи управления в main программа ticker_demo.c входит в бесконечный цикл, в котором вызывается pause. После окончания каждого интервала в 500 миллисекунд про исходит передача управления на функцию countdown. Функция countdown производит декремент статической переменной, печатает сообщение и передает управление обратно тому, кто эту функцию вызвал. Когда значение переменной num достигнет нуля, то функ ция countdown выполняет exit. Естественно, функция main не обязана выполнять вызов pause. Вместо этого основная про грамма может делать что-либо более интересное для себя. При этом после каждого тика таймера управление будет передаваться функции countdown. Детали структуры данных. Внутренние установки таймера передаются в структуру struct itimerval. Эта структура содержит значение начального интервала и значение периода повторения. Оба значения хранятся в структуре struct timeval: struct itimerval
{ struct timeval it_value; struct timeval itjnterval;
Г время, когда должен кончиться интервал */ /* это значение загружается в itvalue */ •
}; struct timeval { time_t tv_sec; susecondsj tv_usec;
/* секунды */ /* и микросекунды */
}; Детали в представлении структуры struct timeval могут быть разными от одной версии Unix к другой. Поэтому обратитесь к документации и заголовочным файлам на вашей системе. На рисунке 7.12 изображены структуры внутри структур, а на рисунке 7.13 показано, как будет произведена загрузка структуры так, чтобы первый alarm возник через 60.5 секунды, а затем повторялся через каждые 240.25 секунды.
246
Событийно-ориентированное программирование. Разработка видеоигры
Обобщение fio системным вызовам getitimer,setitimer НАЗНАЧЕНИЕ
Получить или установить значение уставки интервального таймера
INCLUDE
#include < sysAime.h >
ИСПОЛЬЗОВАНИЕ
result = getitimer(int which,struct tome» val *val); result = setitimer(int which,const struct itimerval *newval, struct itimerval *oldval);
АРГУМЕНТЫ
which - таймер, который устанавливается или установки которого читаются val - указатель на текущие установки newval - указатель на установки, которые будут инсталлированы oldval - указатель для установок, которые будут заменены
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Системный вызов getitimer позволяет читать значения текущих установок для конкретного таймера и помещать считанные значения в структуру, на которую направлен указатель val. Системный вызов setitimer позволяет производить установку таймерных значений, на которые направлен указатель newval. Если указатель oldval ненулевой, то предшествующие значения установок для этого таймера копируются в структуру, на которую направлен ука затель oldval. * ... С помощью значения аргумента which определяется таймер, который будет устанавли ваться или у которого будут прочитаны его установки. Коды для таймеров таковы: ITIMER_REAL, ITIMER_VIRTUAL, ITIMERJPROF.
7.6.5. Сколько часов можно иметь на компьютере? Что нужно сделать в системе, чтобы каждый процесс в системе имел бы в своем распоря жении трое отдельных часов? Ведь в некоторых системах одновременно развиваются сот ни процессов. Означает ли это, что на компьютере следует поддерживать сотни отдельных часов? Нет, в системе необходимы только одни часы для поддержки общей синхронизации (для задания темпа работы): Подобно постоянному тиканью только одного метронома, задающего такт для струнного квартета, или подобно только одному маятнику, который
7. 6. Программирование времени //: Интервальные таймеры
247
двигает стрелки на старинных часах, так и тиканье (пульсация) одних аппаратных часов будет достаточным для реализации на компьютере только одного таймера. Но при на личии только одного таймера, каким образом сможет один процесс установить собствен ный таймер на выработку сигнала через 5 секунд, а при этом другой процесс сможет уста новить собственный таймер на выработку сигнала через 12 секунд? А как все происходит в старинных часах, где часовая стрелка движется с одной скоростью, минутная стрелка движется с другой скоростью, секундная стрелка движется с третьей скоростью, а индикатор лунных фаз движется с четвертой скоростью? Ответ на все по ставленные вопросы будет один и тот же. Для каждого исполнителя в квартете, для про цесса и для шестеренки часов необходима установка личного счетчика. Операционная система должна будет декрементировать все эти счетчики после каждого тика системных часов. Идею поможет пояснить следующий пример. Конкретный пример. Рассмотрим два процесса, процесс А и процесс В. Процесс А уста новил свой реальный таймер на срабатывание через 5 секунд, а процесс В установил свой реальный таймер на срабатывание через 12 секунд. Для того чтобы иметь дело с более простыми числами, представим, что системные часы тикают в темпе 100 тиков в секунду. Когда процесс А установил свой таймер, ядро установило значение счетчика этого процесса равным 500. Когда процесс В установил свой таймер, то ядро установило значе ние счетчика для этого процесса равным 1200. Пока все хорошо, не так ли? Взгляните на рисунок 7.14.
При каждом тике системных часов ядро обращается к набору интервальных таймеров и уменьшает значение каждого счетчика на единицу. Когда значение счетчика процесса А достигнет нуля (это произойдет после того, как от часов поступит 500 тиков), ядро пошлет процессу А сигнал SIGALRM. Если процесс А установил значение itjnterval для этого тай мера, то ядро скопирует это значение в счетчик it_value. В противном случае ядро сбросит таймер. *
248
Событийно-ориентированное программирование. Разработка видеоигры
Несколько позже ядро декрементирует значение счетчика процесса В на 1200-м тике и обнару жит. что значение счетчика стало равным нулю. Это заставит ядро послать сигнал процессу В. Если в процессе В была запланирована переустановка значения таймера, то ядро выполнит перезагрузку значения it_value и перейдет к обслуживанию следующего таймера. Такой простой механизм позволяет каждому процессу установить собственный будиль ник. Даже когда процесс спйт, таймер будет “тикать” - декрементировать счетчик време ни, оставшегося до побудки. А как работают оставшиеся два вида таймеров? Они декрементируют значение счетчика не постоянно, а делают это только тогда, когда процесс будет находиться в нужном состоя нии. По исходному коду Linux ясно видно, как работают эти таймеры.
7.6.6. Итоговые замечания по таймерам Программа в Unix использует таймеры для приостановки своего исполнения и для пла нирования выполнения действий в будущем. Таймер представляет собой механизм ядра, который посылает сигнал процессу после того, как истечет заданный интервал времени. С помощью системного вызова alarm процессу посылается сигнал SIGALRM после того, как пройдет заданное число секунд реального времени. Системный вызов setitimer позво ляет управлять таймером с высоким временным разрешением и использовать возмож ность вырабатывать сигналы через регулярные интервалы времени. Мы знаем теперь, как можно использовать время в наших программах. В видеоигре требу ется использование еще одного механизма: управление прерываниями.
7.7.
Управление сигналами I: Использование signal
Наша игра должна управляться с помощью прерываний. Игра может быть в таком состоя нии: образ перемещается по экрану, а в это время пользователь нажал на клавишу. Или мо жет случиться, что игра находится в таком состоянии, что производится обработка поль зовательского ввода, и в это время приходит сигнал от таймера. Если в игре могут прини мать участие два игрока, то может случиться, что при отработке ответа одному игроку другой игрок нажал на клавишу. Управление прерываниями является существенной частью операционной системы и систем ных программ. В Unix прерывания, вызванные программным образом, воспринимаются как сигналы. Рассмотрим теперь более детально тему управления сигналами. Сначала сделаем обзор начальной модели управления сигналами в Unix. Затем определим проблемы, свойст венные этой модели. Наконец, мы изучим POSIX-модель управления сигналами.
7.7.1. Управление сигналами в старом стиле Ядро посылает сигналы процессу в ответ на некоторые события, включая нажатия на кла виши, недопустимое поведение процесса, окончание отсчета времени на таймере. В главе 6 была введена для рассмотрения начальная модель управления сигналами. Процесс обра щается к signal для выбора одного из трех возможных вариантов реакции на сигнал: (a) Действие по умолчанию (обычно это окончание процесса). Например, signal (SIGALRM, SIGDFL). (b) Игнорирование сигнала. Например, signal(SIGALRM, SIGJGN). (c) Введение функции для обработки сигнала. Например, signal (SIGALRM, handler).
ZZУправление сигналами/:Использование signal
249
, Управление множеством сигналов
7.7.2
Базовая модель управления сигналами прекрасно работает, если будет поступать только один сигнал. А что будет происходить, если процессу будет посылаться множество сигна лов? С реакцией типа окончание процесса и типа игнорирование все ясно. А вот с реакцией типа перехват сигнала для последующей обработки с помощью функции ответ не ясен и не очевиден. Проблема мышеловки Обработчик сигнала подобен мышеловке. Сигнал появляется, чтобы известить о возник новении некоторой опасности. И щелк! Мышь или сигнал перехватываются. Но такая обработка сигналов неэффективна. В Olden Days ® механизм перехвата сигналов был похож на мышеловку еще и в другом смысле: вы должны были восстанавливать этот механизм в исходное состояние после каж дого перехвата сигнала. Например, обработку сигнала SIGINT можно было выполнить так: void handler(int s)
{ Г процесс в этом месте уязвим signal(SIGINT, handler);
*/ Г восстановление обработчика */
... Г здесь все работает
V
} Даже если вы все будете делать быстро, все равно пройдет какое-то время от начала про цесса до восстановления обработчика, т. е. до момента, когда мы могли бы поймать дру гую мышь. Это зона уязвимости делает начальную модель управления сигналами нена дежной. Как это ни странно, но многие используют термин ненадежные сигнапы. Это звучит так же неправильно, как если бы мы говорили - ненадежные мыши. Планирование работы в улучшенной системе Проблема мышеловки - это только одно из слабых мест начальной модели управления сигналами. Для понимания сложности этой темы рассмотрим реальные примеры: Множество сигналов, предназначенных человеку В реальном мире существует множество сигналов, т. е. непредсказуемых прерываний. Представьте себе, что вы работаете в вашем офисе. Может зазвонить телефон, кто-то мо жет постучать в дверь, может зазвучать сирена пожарной тревоги. Каждое из этих собы тий является для вас требованием на прерывание вашей работы. Каждое такое требование можно или проигнорировать, или как-то на него отреагировать. Управление, связанное с телефонным звонком, сводится к тому, что вы должны будете отложить на время вашу текущую работу, ответить на звонок абонента, поговорить с ним, положить трубку и затем продолжить вашу работу. Ваша реакция на стук в дверь, по сути, приведет к выполнению действий по той же схеме. А что произойдет, если к вам стучатся, когда вы отзечаете на телефонный звонок? Вы на момент прервете разговор по телефону, отложите трубку, ответите на стук в дверь, пого ворите с визитером и вновь продолжите разговор по телефону. Потом, когда вы закончите телефонный разговор, вы опять продолжаете свою работу за столом. В данном случае мы говорим, что второй сигнал прервал обработку первого сигнала. Далее. А что случится, если появится еще один визитер, когда вы разговариваете с первым визитером? Часто про исходит так, что первый визитер закрывает дверь. Поэтому второй визитер будет вынуж ден ожидать, пока вы не поговорите с первым. Когда вы закончите пе еговоры с первым
250
Событийно-ориентированное программирование. Разработка видеоигры
визитером, то второй визитер может постучать в дверь. В этом случае мы будем говорить, что второй визитер был блокирован до того момента, пока не заканчивается беседа с первым визитером. Итак, вернемся к вопросу что, если визитер прерывает вас, когда вы разговариваете с кемлибо по телефону? Когда визитер выходит из комнаты, то сможете ли вы правильно вос становить (или вспомнить), на чем прервался ваш разговор по телефону, или же вы скаже те абоненту, что забыли, о чем вы говорили с ним? Наконец, ваша жизнь может зависеть от понимания следующего примера. Что случится, если зазвонил телефон или кто-то постучад в дверь в момент возникновения сигнала пожарной тревоги? При возникновении критического сигнала пожарной тревоги вы, вероятно, блокируете другие сигналы, такие, как телефон и дверь, когда будете что-то делать по сигналу пожарной тревоги. Наверное, могут быть и другие случаи, когда возникает потребность блокировать все сигналы, хотя и нет необходимости отрабатывать действия по сигналу пожарной тревоги.
Множество сигналов для процесса Жизнь ваших процессов мало отличается от вашей жизни. Представьте себе процесс, который развивается в своей маленькой виртуальной клетушке где-то в памяти компьютера (см. рису нок 7.15). Пользователь может нажать ключ Ctrl-C и выработать сигнал SIGINT. Или нажать на ключ Ctrl-\ и выработать сигнал SIGQUIT Или поступит сигнал от таймера SIGALM, когда закончится установленный интервал времени. Все эти сигналы могут одновременно поступить процессу, что аналогично ситуации с телефонным звонком и со стуком поситителей в дверь. Как в Unix процесс будет производить обработку сразу нескольких сигналов?
1.
Нужно ли восстанавливать работоспособность обработчика после каждого его ис пользования? (Модель мышеловки) 2. Что произойдет, если поступит сигнал S1GY, когда процесс занят обработкой сигнала SIGX? 3 Что произойдет, если поступает второй сигнал SIGX, когда процесс занят обработкой предшествующего сигнала SIGX? Или что будет, если поступит в этот момент еще и третий сигнал SIGX? 4. Что произойдет, когда поступает сигнал, а программа блокирована по входу, поскольку быполняет getchar или read? В различных версиях Unix ответы на эти вопросы будут разными. Написать программу, в которой работали бы все возможные варианты, трудно.
7.7. Управление сигналами/:Использование signal
251
7.7.3. Тестирование множества сигналов Как в вашей системе решаются указанные выше задачи? Скомпилируем и запустим на ис полнение программу sigdemo3.c для того, чтобы ответить на вопрос, как процессы вашей системы будут реагировать на различные комбинации сигналов: Г sigdemo3.c * назначение: иллюстрация ответов на вопросы о сигналах вопрос 1: остается ли в рабочем состоянии обработчик после того, как был * принят сигнал? * вопрос 2: что происходит, если сигнал signalX приходит, когда происходит * обработка предшествующего сигнала signalX? вопрос 3: что произойдет, если сигнал signalX поступает, когда процесс * занят обработкой сигнала signalY? вопрос 4: что произойдет с выполнением read(), если поступает сигнал?
*/ «include <stdio.h> #include <signal.h> «define INPUTLEN 100 main(intac, char *av[])
{ void inthandler(int); void quithandler(int); char input[INPUTLEN]; int nchars; signal(SiGINT, inthandler); /* установка обработчика */ signal(SIGQUIT, quithandler); /* установка обработчика */ do { printff "\nType a message\n"); nchars = read(0, input, (INPUTLEN-1)); if (nchars == -1) perrorfread returned an error''); . else { inputfnchars] = '\0'; printffYou typed: %s", input);
} } while(strncmp(input, "quit", 4) != 0);
} void inthandlerfint s)
{ printff" Received signal %d.. waiting\n", s); sleep(2); printff' Leaving inthandler \n");
} void quithandler(int s)
252
Событийно-ориентированное программирование. Разработка видеоигры
' { printff’ Received signal %d.. waiting\n", s); sleep(3); printf(M Leaving quithandler \nn);
} Поэкспериментируйте с выполнением некоторых тестовых последовательностей, что сво дится к обычному вводу текста и нажатию в определенном порядке на два ключа: Ctrl-C и Ctri-\. В частности, проверьте действие следующих далее комбинаций, используя раз личные задержки между нажатиями на ключи. Проследите трассу потока управления через функции обработки сигналов, как показано на рисунке 7.16. (a) А С А С Л С А С (b) Л\Л СЛ\А С (c) hello A C Return (d) hello Return A C (e) A \ A \hello A C
Результаты этих экспериментов покажут, как ваша система управляет комбинацией сигна лов: ^ 1.
Ненадежные сигналы (мышеловка).
Если посылка двух сигналов SIGINT приведет к уничтожению процесса, то вы имеете дело с ненадежными сигналами: после очередной обработки сигнала обработчик должен быть восстановлен. Если при поступлении нескольких сигналов SIGINT процесс не будет убит, то это означает, что обработчик остается в рабочем состоянии после очередной обработки сигнала. В современных механизмах обработки сигналов можно встретить тот и другой вариант обработки. 2.
Сигнал SIGY прерывает работу обработчика сигнала SIGX (сначала телефонный зво нок, потом стук в дверь).
Когда вы нажали на ключ Ctrl-C, а потом нажали на ключ Ctrl-\, то можете заметить, что сначала в вашей программе было передано управление на обработчик inthandler, затем управление будет передано на quithandler, а затем опять управление будет передано функ ции inthandler. И, наконец, управление будет передано опять в цикл функции main. А что показал ваш эксперимент? 3. Сигнал SIGX прерывает работу обработчика сигнала SIGX (в дверь постучали дважды).
ZZУправление сигналами /: Использование signal
253
Этот случай аналогичнен той ситуации, когда двое человек подряд хотят к вам войти. Рассмотрим три возможные метода решения этой проблемы: 1. Рекурсивный, вызывается один и тот же обработчик3. 2. Проигнорировать второй сигнал, что аналогично ситуации, когда телефон занят. 3. Блокировать второй сигнал, пока не будет закончена обработка первого сигнала. В первоначальных системах по обработке сигналов использовался первый метод, где до пускались рекурсивные вызовы. Метод 3 является методом защиты. Все происходит так, как в ситуации со вторым посетителем у двери. Второй сигнал блокируется, а не иг норируется. Блокируется до тех пор, пока обработчик не закончит обработку, связанную с появлением первого сигнала. В вашей системе происходила блокировка второго прихода сигнала или производился рекурсивный вызов обработчика? Может ли ваша система ста вить несколько сигналов в очередь на обработку? 4. Прерываемые системные вызовы (стук в дверь во время телефонного разговора) Это один из вероятных случаев. Программы часто принимают сигналы во время ожидания ввода. В тестовой программе, которая была приведена выше, основной цикл блокировал ся, когда системный вызов read ожидал ввода данных с клавиатуры. Если при этом вы на жмете на ключ прерывания или на ключ выхода (ключ quit), то управление в программе будет передано соответствующему обработчику сигнала. После того, как обработчик за кончит работу, управление вновь будет передано в функцию main, предположительно в то место, где функция была прервана. Правильно ли это? Что произойдет, если вы набрали на клавиатуре “hel”, затем нажали ключ Ctrl-C, потом дополнительно набрали “1о” и нажали Enter? Как поступит программа - произведет рестарт read или будет закончено выполне ние read и в переменную errno будет занесено значение EINTR? Этот вопрос, рестарт или return, может быть решен двумя способами. Либо по схеме AT&T (возврат из read с кодом -1 и со значением в errno, равным EINTR. Это классическая модель), либо по схеме UCB (автоматический рестарт).
7.7.4. Слабые места схемы управления множеством сигналов В начальной схеме управления сигналами есть еще два слабых места: Вы не знаете - почему был послан сигнал. Обработчик сигнала - эта функция, которая вызывается, когда поступает сигнал. Ядро передает обработчику номер сигнала. В программе sigdemo3.c функция inthandler вызывает ся с аргументом SIGINT. Получение через аргумент номера сигнала позволяет одной функ ции управлять обработкой нескольких сигналов. Например, в программе sigdemo3.c мы мо жем заменить два обработчика одним обработчиком, который будет использовать аргу мент для определения, какое сообщение вывести на печать. В первоначальной модели обработчик извещается, какой сигнал он принял, но обработчику ничего не сообщается, почему был выработан сигнал. Например, возникшая ситуация исключения по плавающей точке {floating-point exception) может привести к выработке сигнала, когда происходят любая из нескольких видов арифметических ошибок - таких как деление на ноль, це лочисленное переполнение, потеря значимости. Обработчику необходимо знать о причи не возникновения данной проблемы, т. е. о причине исключения.
3. Эго - непреднамеренная рекурсия, поскольку обработчик сам себя не вызывает, но проявление будет анало гично обычной рекурсии.
254
Событийно-ориентированное программирование. Разработка видеоигры
Вы не можете надежно произвести блокирование других сигналов, когда происходит работа обработчика сигналов. Когда вы что-либо делаете в ответ на сигнал пожарной тревоги, то обычно вы игнорируете звонки телефона. Пусть мы решили, что наша программа будет игнорировать сигнал SIGQUIT, если он приходит при обработке сигнала SIGINT. Используя классические сигна лы, модифицируем обработчик inthandler, который теперь будет выглядеть так: void inthandlerfint s)
{ int rv; void (*prev_qhandler)(); prev_qhandler = signal(SIGQUIT, SIGJGN);
. /^сохранить связь с обработчиком */ /* игнорировать сигналы QUIT */
signal(SlGQUIT, prev qhandler);
/* восстановить действие обработчика */
} To есть мы отключаем обработчик сигнала выхода (quit handler) при входе в обработчик сигнала прерывания (interrupt handler) и восстанавливаем доступность к обработчику сиг нала выхода при окончании работы обработчика сигнала прерывания. В связи с таким ре шением возникают две проблемы. Во-первых, возникает уязвимое пространство, которое расположено от момента начала исполнения inthandler до вызова signal. А нам хотелось, чтобы одновременно был бы и вызов обработчика inthandler, и было бы установлено иг норирование сигнала SIGQUIT. Во-вторых, мы не хотели бы просто проигнорировать сигнал SIGQUIT. Мы хотели бы толь ко блокировать этот сигнал до тех пор, пока не будут выполнены действия по сигналу по жарной тревоги. Нам хотелось бы благополучно получить опять возможность обработки сигнала SIGQUIT, когда закончится критическое событие.
7.8.
Управление сигналами II: sigaction
В течение ряда лет различные группы разработали варианты решения вопросов и про блем, которые были связаны с первоначальной моделью управления сигналами. Мы бу дем изучать только POSIX-модель и набор необходимых для нее системных вызовов. Между тем классический системный вызов signal все еще поддерживается. Он вполне при емлем для некоторых приложений.
7.8.1. Управление сигналами: sigaction В POSIX системный вызов signal заменен системным вызовом sigaction. При этом назначе ние аргументов осталось практически тем же. Вы определяете, какой сигнал будет обра батываться и как вам хотелось бы управлять этим сигналом. При желании вы можете узнать, каковы были предшествующие установки по обработке сигнала. int sigaction(signalnumber, action, prevaction)
Обобщенно характеристики будет выглядеть так: sigaction НАЗНАЧЕНИЕ
Определить способ управления сигналом
INCLUDE
tinclude < signal.h >
7.8. Управление сигналами П: sigaction
255
sigaction ИСПОЛЬЗОВАНИЕ
res = sigactionfint signum, const struct sigaction ^action, struct sigaction *prevaction)
АРГУМЕНТЫ
signum - сигнал, которым следует управлять action - указатель на структуру, где описано действие по управлению prevaction - указатель на структуру, куда помещается описание старого действия по управлению
.
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Первый аргумент signum служит для задания сигнала, которым следует управлять. Второй аргумент action указывает на структуру, где описано, как следует реагировать на появле ние сигнала. Третий аргумент prevaction, если он не равен нулю, указывает на структуру, куда будут помещены старые установки по обработке сигнала. Системный вызов возвра щает 0, если новое действие было успешно инсталлировано, и -1 в случае неудачи. Настройка процедуры обработки сигналов: struct sigaction На ранних этапах развития Unix ваш выбор варианта по управлению сигналами был прост: SIG_DFL, SIGJGN или функция обработки. В новой модели за вами сохраняется выбор варианта управления. Но это лишь часть возможностей структуры struct sigaction, с помощью которой задается, как будет обрабатываться сигнал. Далее представлена эта структура полностью: struct sigaction { Г используется только одна из этих двух возможностей */ void (*sa_handler)(); /* SIG_DFL, SIGJGN или функция обработки */ void (*sa_sigaction)(int, siginfoj *, void *); /* НОВЫЙ обработчик */ sigsetj sa.mask; /* сигналы, которые будут блокированы во время обработки*/ int sajlags; /* установка различных режимов поведения */
} sajiandler ^sa_sigaction
Прежде всего вам необходимо принять решение и выбрать либо старый метод управления сигналами, либо новый, более мощный. Если вы выбрали старый метод - SIG_DFL, SIGJGN, функция обработки, то следует установить в sajiandler одну из трех, перечислен ных выше установок. Естественно, если вы выбрали старый стиль управления сигналами, то вам будет достаточно сообщить о номере управляемого сигнала. Если же вы вместо этого варианта указываете в sa_sigaction имя обработчика, то это будет обработчик, ко торый получит номер сигнала, а также получит информацию о причине и о проблемном контексте. Различие между этими двумя видами обработчиков можно в целом выразить так: Использование обработчика старого стиля Использование обработчика нового стиля struct sigaction action; struct sigaction action; action, sajiandler = handler_old; action.sa_sigaction = handler_new;
А как сообщить ядру о том, чтовы хотели бы использовать обработчик нового стиля? Легко. Надо установить бит SAJSIGINFO в члене структуры sajlags. sajlags
256
Событийно-ориентированное программирование. Разработка видеоигры
Вы должны решить, как ваш обработчик будет отвечать на четыре вопроса, сформулиро ванные в предшествующем разделе. Член sa_flags представляет собой битовый набор. С его помощью и происходит управление режимами работы обработчика так, что' можно ответить на эти четыре вопроса. Обратитесь к вашему справочнику для получения полной информации по этому поводу. Вот некоторая часть информации об этом битовом наборе: Флаг SA_RESETHAND
Назначение Сброс обработчика после вызова. Этим достигается режим мышеловки.
SA_NODEFER
Выключение автоматической блокировки сигнала, когда он будет обрабатываться. Этим обеспечивается возможность рекурсивных обращений к планировщику
SA_RESTART
Рестарт, а не return, выполнения системных вызовов в отношении медленных устройств и необходимые для этого системные вызовы. Этим обеспечивается поддержка режима BSD
SA.SIGINFO
Использование значения в sa_sigaction в качестве функции обработчика. Если этот бит не установлен, то используется значение sa_handler. Если используется значение sa_sigaction,то этой функции обработчика будет передаваться не только номер сигнала, но также указатели на структуры, где содержится информация, которая позволяет определить, почему и как был выработан сигнал
sa_mask
Наконец, вы должны решить, нужно ли блокировать другие сигналы, которые могут поя виться при работе обработчика. Указать сигналы, которые требуется блокировать, можно с помощью разрядов в sa_mask. С помощью sa_mask вы можете блокировать телефонные звонки и стуки в дверь визитеров до тех пор, пока не ликвидируете пожарную ситуацию. Значением sa_mask будет набор сигналов, которые необходимо блокировать. Блокирова ние сигналов - это средство для предотвращения искажений данных. В следующем разде ле мы рассмотрим эту тему более детально. Пример: Использование sigaction В данной программе демонстрируется использование sigaction (заметьте, как производится блокирование сигнала SIGQUIT на периоде обработки сигнала SIGINT). Г sigactdemo.c * назначение: показывает, как используется sigaction() свойство: блокирование нажатия на ключ Л\, когда обрабатывается сигнал от нажатия ЛС * Обработчик ЛС не восстанавливается, поэтому второй сигнал * убивает процесс 7 #include <stdio.h> #include <signal.h> #define INPUTLEN 100 main()
{ struct sigaction newhandler; /* новые установки */ sigsetj blocked; /* набор блокированных сигналов 7 void inthandler(); /* обработчик */ charx [INPUTLEN]; Г сначала загружаются эти два члена 7
7.9. Предотвращение искажений данных
257
newhaffidtef^ajiandler = inthandler; Г Функций обработчика */ newhandter.sajlags = SA_RESETHAND I SA.RESTART; /* опции */ Г затем строится список блокируемых сигналов */ sigemptyset(&blocked); /* очистить все разряды */ sigaddset(&blocked, SIGQUIT); /* добавить в список сигнал SIGQUIT */ newhandler.sa_mask = blocked; /* сохранить маску блокирования сигналов */ if (sigaction(SiG)NT, &newhandler, NULL) == -1) perrorf’sigaction"); else while(1){ fgets(x, INPUTLEN, stdin); printff'input: %s", x);
} } void inthandler(int s)
{ printff'Called with signal %d\n", s); sleep(s); printffdone handling signal %d\n", s);
} Попробуйте поработать с этой программой. Если вы нажмете на ключ Ctrl-C, а затем бы стро на ключ Ctrl-\, то сигнал quit будет блокирован до тех пор, пока обработчик не за вершит обработку сигнала interrupt. Добейтесь того, чтобы вы получили на практике то, что хотели бы видеть. Если дважды нажать на ключ Ctrl-C, то по второму сигналу процесс исполнения нашей программы будет убит. Если вы предпочтете перехват всех сигналов, которые поступят при нажатии ключа Ctrl-C, то следует убрать маску SA RESETHAND из члена sa_flags.
7.8.2. Заключительные замечания по сигналам Процесс может быть прерван сигналами, которые поступают из разных источников. Сиг налы могут поступать в произвольном порядке и в произвольное время. Системный вызов signal предоставляет возможность использовать простой, неполно определенный метод управления сигналами. В POSIX интерфейс sigaction предоставляет исчерпывающий, ясно определенный метод для управления реакциями процессов при поступлении различных комбинаций сигналов. Мы знаем теперь, как управлять временными интервалами и прерываниями в наших про граммах. Наша видеоигра требует рассмотрения последнего вопроса: предотвращение неразберихи.
7.9. Предотвращение искажений данных Когда вы одновременно заняты исполнением сразу нескольких дел, то не приводит ли это вас к неразберихе и не делаете ли вы при этом ошибки? Представьте, что вы готовите к от правке письмо и ищете марку для письма. В это время раздается звонок в дверь. В резуль тате такой помехи вы можете забыть, чем вы занимались до звонка, и отправить письмо без марки. В программах может произойти то же. Если программа находилась где-то на половине выполнения некой работы и неожиданно ее прерывают. Программа от такой ППМРГУИ МПЖРТ Р.^МТКГ.ЯГ ЧТП nnUMRftnftT К МГ.КЯЖРНИШ ПЯННЫУ
258
Событийно-ориентированное программирование. Разработка видеоигры
Обратимся к бытовым примерам, чтобы проиллюстрировать, как прерывания могут при вести к ошибкам в данных. Потом обратимся к программистским идеям и средствам, с помощью которых можно предотвратить появление проблемы.
7.9.1. Примеры, иллюстрирующие искажение данных Продолжим рассмотрение ситуации, которая может сложиться в вашем офисе, когда вашу работу прерывает телефонный звонок и стук в дверь. Пусть посетители, которые стучат в дверь офиса, будут заносить свои фамилии и адреса в список. Каждый посетитель дол жен заносить в конец списка три строки: фамилия, улица, город, штат и индекс (zip). Рассмотрим две проблемы. Во-первых. Визитер в текущий момент занят тем, что добавляет в список информацию о себе. В это время кто-то звонит по телефону и запрашивает фамилии и адреса из списка. Если вы в ответ возьмете список и прочтете своему телефонному абоненту список, то вы предоставите ему данные, которые полностью не сформированы. Вы можете предотвра тить появление ошибок такого рода, если блокируете телефонные звонки на время, пока происходит оформление посетителей. Далее. Рассмотрим другую проблему. Пусть один из посетителей только что добавил в список одну строку о себе, и в этот момент поступил второй сигнал SIGKNOCK. Если вами используется рекурсивный метод управления сигналами, то вы приостанавливаете оформление первого посетителя и допускаете к оформлению второго, который записывает три строки в список о себе, а затем уходит. После этого первый посетитель продолжает регистрацию, дописывая свои данные к концу имеющегося списка. Он добавляет записи об улице, городе, штате и индексе. После этого в учетном списке будут содержаться неправильные данные. Получилось, что одна учетная запись попала внутрь другой. Вы можете предотвратить появление этого типа ошибок за счет последовательного, а не рекурсивного обслуживания при оформлении. Эти два примера продемонстрировали, что необходимо предотвращение прерывания одним действием другого действия. Структура данных (это в нашем случае список учета) модифицировалась, когда производилась работа с этой структурой. До тех пор, пока не будут выполнены все изменения в структуре, все другие функции не должны читать и из менять структуру данных. Естественно, средство отработки сигнала пожарной опасности сохраняет свою работоспособность, поскольку этот обработчик не читает и не пишет в список учета.
7.9.2. Критические секции Секцию кода, где производится модификация структуры данных, называют критической секцией, если прерывания кода в этой секции могут привести к появлению неполных или “опасных” данных. Когда ваша программа работает с сигналами, то вы должны опреде лить, какие части вашего кода принадлежат критическим секциям, и предпринять меры по защите этих секций. Критические секции не обязательно должны быть в составе обра ботчиков сигналов. Многие из них находятся в обычном потоке управления программы. Самый простой способ защиты критических секций заключается в блокировке или игнорировании сигналов, по которым происходит обращение к обработчикам сигналов, где используются или изменяются данные.
7.9. Предотвращение искажений данных
7.9.3.
259
Блокирование сигналов: sigprocmaskи sigsetops
Вы можете блокировать сигналы как на уровне обработчика сигналов, так и на уровне процесса.
Блокирование сигналов в обработчике сигналов Для блокирования сигналов по мере обработки сигнала следует установить член sa mask в структуре struct sigaction. Это выполняется при передаче структуры для использования в sigac tion, когда производится инсталляция обработчика. Член sa_mask имеет тип sigset_t. Здесь хра нится набор сигналов. Мы кратко рассмотрим назначение этого набора.
Блокирование сигналов в процессе Для процесса можно установить набор сигналов, которые для процесса будут блокирова ны в любой момент его развития. Это не игнорируемые сигналы, а блокируемые. Такой набор блокируемых сигналов называют маской сигналов. Чтобы модифицировать этот на бор блокированных сигналов, необходимо использовать sigprocmask. Вызов sigprocmask вы бирает набор сигналов и использует этот набор (выполняет это как атомарное действие) для изменения текущего состава блокированных сигналов: sigprocmask НАЗНАЧЕНИЕ
Модифицировать текущую маску сигналов
INCLUDE
tinclude < signal.h >
ИСПОЛЬЗОВАНИЕ
int res = sigprocmask(int how, const sigsetj *sigs, sigsetj *prev);
АРГУМЕНТЫ
how - как модифицировать маску сигналов sigs - указатель на список сигналов, которые будут использованы prev - указатель на предшествующую маску сигналов (или NULL)
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
С помощью sigprocmask модифицируется текущая маска сигналов - или добавляются, или удаляются, или замещаются сигналы в *sigs. Действие определяется с помощью значения how, которое может быть одним из значений: SIG_BLOCK, S1GJJNBLOCK, SIG_SET. Если значение prev не равно нулю, то в *prev копируется предшествующая сигнальная маска.
Создание сигнальных наборов с помощью sigsetops sigset_t - это абстрактный набор сигналов, который можно обрабатывать определенным образом - добавлять и удалять сигналы. Основные возможности: sigemptyset(sigset_t *setp)
Очистка всех сигналов в списке, на который указывает setp. sigfillset(sigsetj *setp)
Добавление всех сигналов к списку, на который указывает setp. (Более точно - включение всех сигналов, известных системе. - Примеч. пер.) sigaddset(sigset_t *setp, int signum)
260
Событийно-ориентированное программирование. Разработка видеоигры
Добавление signum к набору, на который указывает setp. ^
^
~
sigdelset(sigsetj *setp, int signum)
Удаление signum из набора, на который указывает setp. Детали можно найти в справочнике по ключу sigsetops. Пример: Временное блокирование пользовательских сигналов Программа может временно блокировать сигналы SIGINT и SIGQUIT с помощью такого ко да: sigset_t sigs, prevsigs; /* определение двух наборов сигналов */ sigemptyset(&sigs); Г сбросить все биты в наборе */ sigaddset(&sigs, SIGINT); /* включить бит SIGINT 7 sigaddset(&sigs, SIGQUIT); j* включить бит SIGQUIT 7 , sigprocmask(SIG_BLOCK, &sigs, &prevsigs); /*добавить это к маске процесса7 //.. здесь производится модификация структуры данных. sigprocmask(SIG_SET, &prevsigs, NULL); /^восстановить предшествующую маску7
Заметьте, что метод блокирования сигналов реализован по той же схеме, какая была ис пользована при изменении установок в драйвере терминала или для файлового дескрип тора. Мы сохраняем предшествующие установки и потом используем эти значения уста новок для восстановления маски сигналов. Это хороший стиль. Необходимо оставлять программный ресурс таким, каким вы его застали в начале работы.
7.9.4. Повторно входной код: Опасность рекурсии Пример с одним посетителем, прерывающим процедуру учета другого посетителя, и по мещение фамилии и адреса в середину учетной записи первого посетителя - все это при водит нас еще к одной концепции, касающейся искажения данных: повторно вызываемая
функция. Обработчик сигнала или любая функция, которые могут быть повторно вызваны, когда они уже активны, и которые при повторном вызове не создадут каких-либо проблем, на зывают повторно входными. (В ранних переводах по тематике ОС функции или програм мы с такими свойствами так и называли - реентерабельными. - Примеч. пер.) С помощью sigaction можно включить рекурсивный режим управления. Это делается с по мощью установки флага SA NODEFER. Или можно включить блокировку, что делается с помощью сброса флага. Что вы из этого выберете? Если обработчик не является повторно входным, то вы должны использовать блокировку. Но если вы блокировали сигналы, то можете их потерять. Сигналы не заносятся в стеко вую память, подобно свойству телефона while you were out (режим “пока вас не было”). Потерянные сигналы могут быть важными. Сохранять или пропускать что-то? Что выбрать - потерю сигналов или возможность смешения данных? Что из этого хуже? Нет ли возможности избежать возникновения этих проблем? Когда вы разрабатываете программу, где используются сигналы, то эти вопросы необходимо принимать во внима ние. Ошибки, допущенные при управлении сигналами, проявляют себя нерегулярно. Обычно они возникают, когда система очень сильно занята и необходимо тщательно учи тывать возможности по производительности. Отладка таких систем требует понимания особенностей работы обработчиков сигналов и понимания, где в них могла возникнуть проблема.
Z10. kill: Посылка сигнала процессом
261
7.9*5. Критические секции в видеоиграх Шарик движется по экрану с постоянной скоростью, отталкиваясь от стенок и ракетки. Пользователь, нажимая на клавиши, перемещает ракетку вверх и вниз. Поддержка посто янного перемещения шарика - это задание, которое исполняется под управлением ин тервального таймера. Пользовательский ввод, который управляет перемещением ракетки, происходит как последовательность непредсказуемых событий, как последовательность сигналов. В какие моменты нам нужно будет блокировать пользовательский ввод? Где на ходятся критические секции в игре, когда ракетка не должна перемещаться? Прежде чем мы применим все эти новые знания к нашему проекту видеоигры, рассмотрим еще один источник сигналов: другие процессы.
7.10. kill: Посылка сигнала процессом Сигналы приходят процессу от интервальных таймеров, драйвера терминала, ядра и от процессов. Процесс может послать сигнал другому процессу с помощью системного вы зова kill: kill НАЗНАЧЕНИЕ
Посылка сигнала процессу
INCLUDE
#indude < sys/types.h > tinclude < signal.h >
ИСПОЛЬЗОВАНИЕ
int kill(pidj pid, int sig)
АРГУМЕНТЫ
pid - идентификатор целевого процесса sig - сигнал, который передается
КОДЫ ВОЗВРАТА
-1 - при ошибке 0 - при успехе
Системный вызов kill посылает сигнал процессу. Процесс, который посылает сигнал, дол жен иметь тот же пользовательский идентификатор (UID), что и у целевого процесса. Или собственником посылающего процесса должен быть суперпользователь. Процесс может послать сигнал сам себе. Процесс может посылать другим процессам любые сигналы, в том числе сигналы, ко торые обычно приходят от клавиатуры, интервальных таймеров или ядра. Например, про цесс может послать другому процессу сигнал SIGSEGV, что равносильно ситуации, когда в процессе-адресате была попытка недопустимого обращения к памяти.
SIGINT
PID129
kill(129, SIGINT)
гт Ш-
II
Рисунок 7.17 Процесс для посылки сигнала использует kill()
В Unix команде kill используется системный вызов kill (см. рисунок 7.17).
262
Событийно-ориентированное программирование. Разработка видеоигры
Возможности использования механизма передачи сигналов для межпроцессных ком муникаций ч Принимающий процесс может установить обработчики сигналов почти для всех сигна лов. Рассмотрим наш пример с программой, которая выводит сообщение OUCH! при по ступлении сигнала SIGINT. Что произойдет, если какая-то другая программа пошлет про грамме OUCH! сигнал SIGINT? Наша программа перехватит сигнал, будет передано управ ление обработчику сигнала и будет выведено сообщение ОиСНЦсм. рисунок 7.18). Продолжим далее рассмотрение сути этой идеи. Пусть в первой программе установили интервальный таймер, который в некоторый момент вызывает обработчик сигнала от тай мера в этой программе, а обработчик посылает сигнал SIGINT программе OUCH!. Этот сиг нал будет передан обработчику сигнала другой программы. В данном случае таймер одно го процесса может управлять действием другого процесса. По сути, происходит то же, что и в футбольном матче, когда игроки перемещаются по полю и борются за мяч. Игроки это процессы, которые могут выдавать сигналы.
Рисунок 7.18 Сложное использование сигналов
Сигналы, предназначенные для IPC (Это краткое, стандартное обозначение механизма межпроцессных взаимодействий - InterProcess Communication, IPC - Примеч. пер.) SIGUSR1, SIGUSR2 В Unix есть два сигнала, которые вы можете использовать для заказных приложений. Для сигналов SIGUSR1 и SIGUSR2 не установлено определенное функциональное назначение. Вы можете использовать эти сигналы вместо тех сигналов, для которых определенное функциональное значение уже установлено. Мы рассмотрим технику межпроцессных взаимодействий в следующих главах. Комбина ция системных вызовов kill и sigaction предполагает достижение с их помощью многих заманчивых программистских возможностей.
7.11. Использование таймеров и сигналов: видеоигры Теперь вернемся к проекту видеоигры. В играх есть два главных элемента: анимация и пользовательский ввод. Средства анимации должны обеспечивать “гладкое” преобразование изображения, а пользовательский ввод должен предоставлять возможность изменять движе ние. В программе bounceld.c пользователь может управлять текстом сообщения: он может из менять перемещение текста в прямом или в обратном направлениях.
7.11. Использование таймеров и сигналов: видеоигры
263
7.1 1.1. bounce 1 с/, с: Управляемая анимация на строке
Сначала рассмотрим, что и как делает программа bounceld. Изображение на экране будет выглядеть аналогично тому, что приведено на рисунке 7.19. Программа bounceld.c плавно перемещает по экрану одно слово. Когда пользователь нажмет клавишу пробела, то это слово станет перемещаться в обратном направлении. При нажатии на клавишу “s” или на kt f’ произойдет увеличение скорости перемещения и уменьшение скорости перемещения слова соответственно. При нажатии на клавишу “Q” игра будет закончена.
Как построить такую программу? Мы знаем, как достигается анимационный эффект. Строка воспроизводится в одном месте экрана. Воспроизведение длится несколько мил лисекунд, а затем изображение затирается. После чего изображение строки перерисовыва ется слева или справа относительно первоначального места воспроизведения. Нам потре буется стирать изображение и перерисовывать его через регулярные интервалы времени. Поэтому мы используем интервальный таймер, который будет посылать сигналы обра ботчику. Введем две переменные, значения которых будут определять направление и скорость перемещения. Значением переменной направления может быть +1 или -1. Это означает соответственно, что сообщение движется влево или вправо. Более длинные задержки между тиками таймера будут приводить к более медленному перемещению слова, а более короткие задержки между тиками будут приводить к более быстрому перемещению. Добавим теперь в программу возможность для пользователя управлять направлением и скоростью перемещения слова. Мы будем читать символы при нажатии клавиш пользо вателем и в зависимости от значения введенного символа будем модифицировать пере менную направления или переменную скорости перемещения. Логика программы изобра жена на рисунке 7.20. В алгоритме программы bounceld поддерживаются две важных идеи: переменные состояния и управление событиями. Состояние анимации описывается с помощью переменной положения, переменной направления движения и переменной задержки. Ввод символов от пользователя (пользовательский ввод) и тики от таймера это события, при наступлении которых происходит модификация этих переменные состояния. После каждого тика таймера вызывается код, который должен изменять поло жение слова на экране. При каждом нажатии пользователем на клавиши .вызывается код, который изменяет значение переменнойГнаправления или значение переменной скорости перемещения. Этот код имеет вид:
264
Событийно-ориентированное программирование. Разработка видеоигр
Рисунок 7.20 Изменение значений через пользовательский ввод. Значения управляют действием /* bounceld.c * назначение - анимация с возможностью изменения пользователем скорости и * направления движения * замечание - обработчик сигналов реализует функции анимации. * В программе main производится чтение символов при нажатии клавиш * компиляция: сс bounceld.c set ticker.c -Icurses -о bounceld
7 «include <stdio.h> «include <curses.h> #include <signal.h> Г ряд глобальных установок и установка обработчика сигнала */ «define MESSAGE "hello” «define BLANK"" int row; Г текущая строка */ int col; f* текущая позиция на строке */ int dir; /* куда перемещаемся */ int main()
{ int delay;
/* больше => медленнее
*/
1. Использование таймеров и сигналов: видеоигры int ndelay; int с; . void move_msg(int); initscr(); crmode(); noecho(); clear(); row= 10; col = 0; dir = 1; delay = 200; move(row,col); addstr(MESSAGE); signal (SIGALRM, move_msg); set_ticker( delay); whiie(1)
/* новая задержка 7 /* пользовательский ввод */ Г обработчик сигнала от таймера */
Г отсюда происходит старт */ Г добавить 1 к счетчику строк */ /* 200ms = 0.2 секунды */ Г перейти в позицию */ Г нарисовать сообщение */
{ ndelay = 0; с = getch(); if (c == 'Q') break; if(c==")dir = -dir; delay/2; if (c == T && delay > 2) ndelay if (c == ’s') ndelay = delay * 2; if (ndelay > 0) set ticker(delay = ndelay);
} endwin(); return 0;
} void move_msg(int signum)
{ signal(S!GALRM, move_msg); move(row, col); addstr(BLANK); col += dir; move(row, col); addstr(MESSAGE); refresh();
/* сразу же восстановление */
Г перейти в новую позицию на строю Г затем установить курсор */ Г перерисовать изображение 7 /* и показать его */
/* * теперь управление на границах
*/ if (dir ==-1 && col <= 0) dir = 1; else if (dir == 1 && col+strlen(MESSAGE) >= COLS) dir = -1;
266
Событийно-ориентированное программирование. Разработка видеоигры
Рекурсия или блокировка: Реальный пример Когда мы рассматривали пример с искажением данных при использовании обработчиков сигналов, мы упоминали о повторно входных функциях. Программа bounceld заставляет вспомнить о таких функциях.. В начале обработчик сигнала move_msg вызывается пять раз за секунду. При нажатии на клавишу “f ’ скорость анимации возростает, поскольку умень шается значение временного интервала таймера. Если нажимать на клавишу “f ” много раз подряд, то интервал между сигналами может стать короче интервала времени, в течение которого выполняется обработчик. А что произойдет, если следующий сигнал от таймера будет поступать тогда, когда обработчик будет занят стиранием и/или перерисовкой тек ста сообщения и/или модификаций переменной положения? Проведение такого анализа остается в качестве упражнения. Мы использовали в програм ме интерфейс signal. Поэтому, произойдет ли вызов обработчика по рекурсивной схеме или по схеме с блокировкой - все будет зависеть от конкретной реализации на вашей системе.
Что дальше? Как нам преобразовать программу bounceld в игру пинг-понг? Прежде всего мы можем за менить строку “hello” символом “О”, который будет заменять изображение шарика. Вовторых, нам необходимо, чтобы шарик мог перемещаться вверх и вниз, а также влево и вправо. Добавление возможности перемещения вверх и вниз потребует введения допол нительных переменных состояния. Мы уже имеем две переменные col и row, с помощью которых описывается положение шарика, а также переменную dir, значение которой зада ет направление перемещения по горизонтали. Какие новые переменные следует нам доба вить для обеспечения перемещения по вертикали?
7.11.2. bounce2d.c: Двухмерная анимация В следующей программе bounce2d.c воспроизводится двухмерная анимация с предоставле нием пользователю возможности менять горизонтальную и вертикальную скорость. На рисунке 7.21 приведена соответствующая иллюстрация.
Программа bounce2d использует тот же трехшаговый проект, какой был использован при построении программы bounce Id:
7.11. Использование таймеров и сигналов: видеоигры
267
Управление таймером Производится установка интервального таймера, который должен посылать постоянно про цессу поток сигналов SIGALRM. При поступлении очередного сигнала шарик необходимо переместить.
Блокирование клавиатуры Программа блокирует ввод с клавиатуры. Она сразу воспринимает и обрабатывает симво лы по мере их набора.
Переменные состояния Место расположения шарика и его скорость хранятся в качестве значений в переменных. С помощью пользовательского ввода производится модификация переменных, значения которых представляют скорость. Значения скорости и места расположения используются обработчиком сигнала от таймера для управления шариком. Эта схема выглядцт аналогично bounceld, но появился один новый, важный вопрос: Как будет происходить перемещение шарика по диагонали? Воспроизведение движения по диагонали - это новая проблема. В одномерной программе при каждом очередном тике происходило перемещение образа на один шаг. Все происхо дило очень просто: один тик - один шаг смещения. Но при двухмерном перемещении дело обстоит уже не так просто. Рассмотрим траекторию, изображенную на рисунке 7.22. Эта траектория состоит из смещения на одну строку вверх и одновременно смещения вправо на три позиции на строке. Данная техника перемещения из точки А в точку В должна быть использована при возникновении каждого очередного тика. Переход из одной точки в дру гую может оказаться достаточно большим и зависит от размеров сторон треугольника. Например, при соотношении сторон 3/4 перемещение по наклонной линии будет равно пяти позициям.
В г0
А
О*
Вопрос: Как обеспечить “гладкое" перемещение символа ‘0’ из точки А в точку В?
Рисунок 7.22 Траектория под углом 1/3 При перемещении от одной точки к другой по наклонной линии маршрут будет выглядеть аналогично тому, как это изображено на рисунке 7.2*3. Заметим, что для перехода по на клонной траектории из точки А в точку В образ должен быть перемещен на три шага по горизонтали и на один шаг по вертикали. Горизонтальная скорость должна быть в три раза большей, чем вертикальная.
268
Событийно-ориентированное программирование. Разработка видеоигры
Для аппроксимации диагонального перемещения нужно: переместиться вправо на каждые два таймерньос тика; переместиться вверх на каждые шесть таймерных тиков. Эта техника предполагает использование двух счетчиков, один из них считает тики для горизонтального перемещения, другой считает тики для вертикального перемещения
РИСУНОК 7.23
Перемещение по наклонной на один шаг за такт выглядит лучше
Схема выглядит так, будто используются два таймера. Так оно и есть. Понаблюдаем за одним работающим таймером. После каждых двух тиков программа перемещает образ на один шаг вправо. После каждых шести тиков программа перемещает образ на один шаг вверх. Шарик будет перемещаться по этой же траектории, если интервалы были указаны в 10 и 30 тиков. Но движение при этом будет более медленным. Программа имеет только один интервальный таймер реального времени. Поэтому нам нужно построить два интервальных таймера и один интервальный таймер, который будет использоваться для управления этими таймерами. Мы будем использовать логику, изобра женную на рисунке 7.23 для организации двухмерной анимации. Код Для организации двухмерного перемещения заведем два счетчика, которые будут высту пать в роли таймеров. Каждый из этих счетчиков будет характеризоваться двумя состав ляющими ~ значением и интервалом. Работа с ними будет выглядеть так же, как при рабо те с системными интервальными таймерами. Значение счетчика - это число тиков, ко торое осталось до наступления следующей перерисовки. Значения интервального счетчика - это число тиков между очередными перерисовками. Для изображения этих двух элементов будут использованы аббревиатуры ttg и ttm. Код программы будет выгля деть так: Г bounce2d 1.0 * *
перемещение символа (по умолчанию это 'о') по экрану, которое задается с помощью определенных параметров
*
”пользовательский ввод: s - замедлить перемещение по оси х, S: замедлить * перемещение по оси у * f - ускорить перемещение по оси х, F: ускорить * перемещение по оси у * Q г выход к *
* *
блокируется чтение, но таймер посылает сигнал SIGALRM, который перехватывается функцией ball_move трансляция: сс bounce2d.c set ticker.c -Icurses -о bounce2d
7 tinclude <curses.h> tinclude <signal.h> tinclude "bounce, h” struct ppball the_ball; /** основной цикл **/
f 1. Использование таймерови сигналов: видеоигры void set_up(); void wrap_up(); int maih()
{ int c; set_up(); while ((c = getchar()) != ’Q'){ if (c == ’f) the_ball.x_ttm-; else if (c == ’s') the_ball.x_ttm++; else if (c == 'F) the_ball.y_ttm- -; else if (c == ’S') the_ball.y_ttm++;
} wrap up();
} void set up()
Г *
инициализация структуры и других элементов
7 {
' void ball_move(int); the ball.y pos = YJNIT; thelball.xlpos = XJNIT; the_ball.y_ttg = the_ball.y_ttm = Y_TTM; the_bail.xjtg = the_ball.x_ttm = XJTM; the_ball.y_dir = 1; the ball.x dir = 1; the'ball .symbol = DFL.SYMBOL; initscr(); noecho(); crmode(); signal(SIGINT, SIGJGN); mvaddch(the_ball.y_pos, the_ball.x_pos, the_ball.symbol); refresh(); signal(SIGALRM, ball.move); set Jicker( 1000 / TICKS_PER_SEC); /* установить значение в Г миллисекундах на один тик */
} void wrap up()
{ setJicker(O); endwin(); /* нормальный возврат */
} void ball move(int signum)
{ int y_cur, x_cur, moved; signal(SIGALRM, SIG_IGN); /* после этого не перехватывать y_cur = the_ball.y_pos; Г старое расположение 7 x_cur = the_ball.x_pos; moved = 0; if (the_ball.yjtm > 0 && the_ball.y_ttg- == 1){
270
Событийно-ориентированное программирование. Разработка видеоигры the_ball.y_pos += the_ball.y_dir; /* перемещение */ the_ball.y_ttg = the_ball.y_ttm; /* переустановка*/ moved = 1;
if (the_ball.x_ttm > 0 && the_ball.x_ttg— == 1){ the_ball.x_pos += the_ball.x_dir; J* перемещение */ the_ball.x_ttg = the_ball.x_ttm; /* переустановка*/ moved = 1;
} if (moved){ mvaddch(y_cur, xcur, BLANK); mvaddch(y_cur, x_cur, BLANK); mvaddch(the_ball.y_pos,the_ball.x_pos, the_ball.symbol); bounce or_lose(&the ball); move(UNES-1 .COLS-1); refresh();
} signal (SIGALRM, ball move); /* для систем с ненадежными сигналами */
} int bounce or lose(struct ppball *bp)
{ int return val = 0; if (bp->y_pos == TOP_ROW){ bp->y_dir = 1; return val = 1; } else if (bp->y_pos == BOT_ROW){ bp->y_dir = -1; return val = 1;
} if (bp->x_pos == LEF_EDGE){ bp- >x_dir = 1; returnval = 1; } else if (bp- >x_pos == RIGHT_EDGE){ bp->x_dir = -1; return val = 1;
} return return_val;
} Заголовочный файл будет иметь вид: Г bounce.h */ Г некоторые установки для игры */ «define BLANK' ’ «define DFL SYMBOL ’o’ «define TOP ROW 5 «define ВОТ ROW 20 «define LEFT" EDGE 10 «define RIGHT EDGE 70
7.12. Сигналы при вводе: Асинхронный ввод/вывод «define XINIT «define YINIT «define TICKS PER SEC «define X ИМ «define Y.TTM /** шарик для пинг-понга struct ppball { int
char
10 10 50 5 8 **/
\
271
Г начальная позиция на строке */ /* начальная строка */ Г составляющие'скорости */
y_pos, x_pos, y_ttm, x_ttm, y.ttg, xjtg, y_dir, x_dir; symbol;
7.11.3. Вся игра целиком Оставшуюся часть работы по созданию игры следует выполнить в качестве упражнения. Вам необходимо добавить механизм управления ракеткой, логику для описания отскаки вания шарика от ракетки, логику для определения выхода шарика из игры. Мы детально рассмотрели все идеи, которые необходимы для завершения проекта. Поду майте о возможное,ти использования повторно входного кода. Где можно было бы исполь зовать такой код? Какой режим управления таймером вы предпочитаете - блокирование или рекурсивный?
7.12. Сигналы при вводе: Асинхронный ввод/вывод При рассмотрении средств анимации и игры в этой главе были использованы два типа со бытий: тики от таймера и ввод с клавиатуры. Мы настроили обработчик так, чтобы он управлял анимационными эффектами с помощью тиков от таймера, а также блокировали ввод с клавиатуры с помощью getch. Нельзя ли вместо блокировки использовать пользова тельский ввод с помощью сигналов аналогично тому, как это делается при работе с сигна лом от таймера? Да, можно. Программы могут затребовать, чтобы ядро присылало процессу сигнал тогда, когда произойдет событие на входе. Это будет аналогично ситуации, когда почтальон бу дет звонить в вашу квартиру, если он принес вам письмо. В этом случае вам не понадобит ся садиться в прихожей и часами пристально наблюдать за почтовым ящиком. Вы можете заниматься чем угодно или даже лечь поспать. Когда придет почтальон с письмом, то вы услышите сигнал от дверного звонка. В Unix, есть две системы для поддержки асинхронного ввода. В одной системе использует ся метод, когда сигнал посылается, если на входе появились данные, готовые для чтения. В другой системе сигнал будет послан после прочтения входных данных. Для использова ния первого метода необходимо установить в файловом дескрипторе бит 0_ASYNC. Эта методика была принята для использования в UCB. Во втором методе, который удовле творяет стандарту POSIX, необходимо использовать aio_read. Мы далее продемонстриру ем возможности двух этих методов. Но сначала рассмотрим идею.
272
Событийно-ориентированное программирование. Разработка видеоигры
7.12.1. Организация перемещения с помощью асинхронного ввода/вывода Новый проект программы перемещения образа схематически иллюстрируется на рисунке 7.24. Поскольку предполагается использование сигналов двух видов: SIGIO и SIGALRM, то мы соз дадим два обработчика сигналов. Обработчик сигнала SIGIO читает данные, которые посту пают от клавиатуры, и обрабатывает эти данные. Обработчик сигнала SIGALRM управляет анимацией и отвечает за организацию перемещений в разных направлениях. Для упрощения программы удалим из нее управление скоростью перемещения.
7.12.2. Метод 1:ИспользованиеOASYNC Использование бита O ASYNC требует внесения четырех изменений в программу переме щений. Во-первых, необходимо создать и инсталлировать обработчик, который будет вы зываться в момент, когда становится доступным ввод с клавиатуры. Во-вторых, необходи мо использовать команду FJSETOWN в системном вызове fcntl, чтобы потребовать от ядра передать установленные входные сигналы нашему процессу. С клавиатурой могут быть связаны и другие процессы, но мы не хотим, чтобы этим процессам ^гакже посылались бы эти входные сигналы. В-третьих, необходимо включить входные сигналы посредством обращения к fcntl и установления с его помощью атрибута 0_ASYNC в файловом дескрип торе 0. Наконец, необходимо выполнение системного вызова pause в цикле для того, чтобы обеспечить ожидание поступления сигналов от таймера или от клавиатуры. Когда от кла виатуры поступает символ, то ядро посылает процессу сигнал SIGIO. Обработчик сигнала SIGIO использует стандартную curses функцию getch для чтения символа. Когда истечет интервал времени таймера, ядро посылает сигнал SIGALRM, управление которым будет происходить так, как было рассмотрено ранее. Вот какой будет исходный код: /* bounceasync.c * Назначение - анимация с возможностью управления со стороны пользователя. * Это делается с помощью установки в файловом дескрипторе fd бита OASYNC * Замечание: set_ticker() посылает сигнал SIGALRM, а обработчик организует
12. Сигналы при ввцце: Асинхронный ввод/вывод
* * *
действия ro анимации. Клавиатура посылает сигнал SIGIO.a в main только происходит вызов paused Компиляция: сс bounce async.c set ticker.c -Icurses -о bounce async
7 «include. <stdio.h> «include <curses.h> «include <signal.h> «include /* Состояние игры */ «define MESSAGE "hello" «define BLANK"" int row =10; /* текущая строка 7 int col = 0; /* текущая позиция на строке */ int dir = 1; /* где мы находимся */ int delay = 200; /* и как долго ждем */ int done = 0; mainQ
{ void on_alarm(int); void on_input(int); void enable_kbd_signals(); initscr(); crmode(); noecho(); clear(); signal(SIGIO, on_input); enable_kbd_signals(); signal(SIGALRM, on_alarm); setjicker(delay); move(row.col); addstr(MESSAGE); while(ldone) pause(); endwin();
/* обработчик сигнала от таймера */ Г обработчик сигнала от клавиатуры */ Г установка экрана 7
Г инсталляция обработчика 7 Г разрешение на включение сигналов от клавиатуры Г инсталляция обработчика сигналов таймера 7 /* начало отсчета времени на таймере 7 Г позиционирование */ Г начальная прорисовка образа 7 /* основной цикл */
} void on input(int signum)
{ int с = getch(); if (c == ’O' j| с == EOF) done = 1; else if (с: ') dir = -dir;
} void on_alarm(int signum)
Г захватить символ */
274
Событийно-ориентированное программирование. Разработка видеоигры
signal(SIGALRM, on_alarm); mvaddstr(row, col, BLANK); col += dir; mvaddstrfrow, col, MESSAGE); refresh();
Г сразу же восстановление реакции */ Г обратиться к mvaddstrf) */ /* перемещение в новую позицию на строке */ Г перестроить образ сообщения */ /* и показать этот образ */
Г * здесь управление на границах
7 if (dir ==-1 &&col <= 0) dir = 1; else ifjfdir == 1 && col+strlen(MESSAGE) >= COLS) ,dir = -1;
Г * инсталляция обработчика, обращение к ядру для установки уведомления сигнала * на входе, установление разрешения на поступление сигналов
7 void enable kbd_signals()
{ int fd_flags; fcntl(0, F_SETOWN, getpidO); fd flags = fcntl(0, F GETFL); fcntl(0, F.SETFL, (fd’ flagsjO ASYNC));
7.12.3. Метод 2: Использование aio_read Метод использования aio_read является более сложным, но и более гибким, чем метод установки бита 0_ASYNC в файловом дескрипторе. Сделаем четыре изменения в програм ме перемещения. Во-первых, инсталлируем onjnput, поскольку мы предполагаем, что обработчик будет вызываться, когда уже произошло чтение ввода. Во-вторых, установим необходимые значения в структуре struct kbcbuf, чтобы описать, какой ожидается ввод и какой сигнал следует послать, когда будет прочитан этот ввод. В нашем простом приложении нужно ждать поступления только одного символа от фай лового дескриптора 0. Мы хотели бы получать в программе сигнал SIGIO после того, как этот символ будет прочитан. Мы может задать любой сигнал о наступлении этого собы тия. Даже SIGARLM или SIGINT. В-третьих, мы выставляем требование на чтение посредством передачи этой структуры при вызове aio_read. В отличие от обычного системного вызова read при использовании aio_read процесс не блокируется. Вместо этого происходит посылка сигнала после за вершения aio_read. Наша программа теперь свободна делать все, что ей необходимо (А не следить непрерыв но за появлением данных на входе, как в случае с почтовым ящиком. - Примеч. пер.). В данном случае мы будем просто вызывать pause, чтобы в паузе ожидать поступление сигнала. Когда пользователь нажмет на клавишу клавиатуры, то aio_read пошлет процессу сигнал SIGIO, который приведет к вызову обработчика.
7.12. Сигналы при вводе: Асинхронный ввод/вывод
Наконец, мы напишем обработчик, чтобы он мог получать входной символ при вызове aio_return, а затем смог бы обрабатывать его. Г bounce_aio.c * Назначение - анимация с возможностью управления со стороны пользователя посредством обращения к aio_read() и т. д. * Замечание: set_ticker() посылает сигнал SIGALRM, а обработчик организует действия по анимации. Клавиатура посылает сигнал SIGIO, а в main только происходит * вызов pause() * Компиляция: * сс bounce_aio.c set ticker.c -Irt -Icurses -о bounce aio
7
«include <stdio.h> «include <curses.h> «include <signal.h> «include Г Состояние игры 7 «define MESSAGE "hello" «define BLANK" ” int row =10; int col = 0; int dir = 1; int delay = 200; int done = 0; struct aiocb kbcbuf; main()
•{
void on_alarm(int); void on_input(int); void setup_aio_buffer(); initscr(); crmode(); noecho(); clear(); signal(SIGIO, onjnput); setup_aio_buffer(); aio_read(&kbcbuf); signal(SIGALRM, on_alarm); set_ticker(delay); mvaddstrfrow, col, MESSAGE); while(ldone) pause(); endwin{);
Г текущая строка 7 Г текущая позиция на строке /* где мы находимся 7 I* сколько, необходимо ждать7 /* управляющий буфер aio 7
Г обработчик сигнала от таймера 7 Г обработчик сигнала от клавиатуры Г установка экрана 7
Г инсталляция обработчика 7 I* инициализация управляющего буфера aio 7 Г выдача требования на чтение 7 Г инсталляция обработчика сигнала от таймера 7 /* начало отсчета временного интервала 7 /* прорисовка начального образа 7 /* основной цикл */
Обработчик, который вызывается, когда aio_read() что-либо прочитал Сначала проверяется наличие ошибок, и если их нет, то получаем код возврата
7
void on input()
{
int с; char *cp: (char *) kbcbuf.aio.buf; /* обращение к char * 7
275
Событийно-ориентированное программирование. Разработка видео /* проверка наличия ошибок */ if (aio_error(&kbcbuf) != 0) perrorf'reading failed"); else Г получить число прочитанных символов */ if (aio_return( &kbcbuf) == 1)
{ с = *cp; if (с == 'СТ || с == EOF) done= 1; else if (c =='') dir = -dir;
}. Г установка нового требования */ aio_read(&kbcbuf);
} void on_alarm()
{
signal(SIGALRM, on_alarm); mvaddstr(row, col, BLANK); col += dir; mvadcfetr(row, col, MESSAGE); refresh!);
Г сразу же здесь переустановка реакции */ /* очистка старой строки */ /* перемещение в новую позицию */ Г прорисовка новой строки */ Г и показ ее */
/* * теперь управление на границах
7 if (dir ==-1 && col <= 0) dir = 1; else if (dir == 1 && col+strlen(MESSAGE) >= COLS) dir = -1;
)
Г * Установление значений членов структуры. * Сначала определить аргументы, как это делается при вызове * read(fd, buf, num), и потом - смещение. * Затем определить, что нужно делать (послать сигнал) и какой сигнал * послать (SIGIO)
7 void setup_aio buffer))
{
)
static char input[1]; /* 1 символ на входе 7 /* описание того, что нужно читать */ Г стандартный ввод 7 ’ kbcbuf.aiojldes = 0; kbcbuf. aio_buf = input; Г буфер 7 f* количество читаемых символов */ kbcbuf.aio.nbytes = 1; kbcbuf.aio_offset = 0; /* смещение в файле */ /* описание того, что нужно делать, после того, как произойдет чтение */ kbcbuf .aio_sigevent.sigev_notify = SIGEV_SIGNAL; kbcbuf.aio_sigevent.sigev_signо = SIGIO; /* послать SIGIO 7
7.12. Сигналы при вводе: Асинхронный ввод/вывод
277
7.12.4. А нужно ли нам производить асинхронное чтение для организации перемещения? Нет. Программы, в которых организуется перемещение, прекрасно работают, используя бло кировку пользовательского ввода и организуя перемещение с помощью тиков интервального таймера. Преимущество асинхронного чтения заключается в том, что программа не блокиру ет ввод и может заниматься чем угодно, пока не наступят входные события. Например, в более изощренных играх программа может воспроизводить музыку, порож дать какие-то звуковые эффекты, рассчитывать какой-то усложненный фоновый образ, даже может выполнять некую общественную работу. Компьютеры все больше и больше привлекают для того, чтобы их свободное время использовать для выполнения больших вычислительных проектов в областях математики, астрономии и медицины. Программа перемещения может потратить свое свободное время для подсчета значения р/, которое содержало бы произвольное число десятичных знаков. При этом программа будет использовать асинхронный пользовательский ввод, чтобы определить, когда пользо ватель нажмет на клавишу. Изменим цикл в функции main следующим образом: До: while(ldone) pause(); endwin();
После: compute_pi(); endwin();
В модифицированной программе происходит вызов функции, которая подсчитывает значение pi. Когда на входе программы появится символ, то происходит передача управ ления обработчику, происходит обработка входных данных, а затем происходит возврат и продолжение расчетов значения pi. Когда возникает сигнал от таймера, то программа передает управление обработчику этого сигнала, происходит обработка таймерного тика, а затем опять передается управление на продолжение вычислений. В программе необходим другой способ обработки ключа “Q”. Как нужно модифицировать программу, чтобы достичь этого?
7.12.5. Асинхронный ввод, видеоигры и операционные системы Мы начали эту главу со сравнения видеоигры и операционной системы. В нашей програм ме перемещения не был задействован асинхронный ввод, в операционной системе он ис пользуется. Ядро запускает программы на исполнение и не может тратить время процес сора на ожидание ввода от пользователя. Ядро устанавливает обработчики, которые будут вызываться, когда будут обнаружены входные данные от клавиатуры, последовательных линий или от сетевой карты. Тогда ядро передаст управление от исполняемой программы к обработчику, далее происходит обработка ввода, а затем управление возвращается обратно и возобновляется исполнение программы, Ядро блокирует сигналы в пределах критических секций. Ядро использует аппаратные версии асинхронного ввода, а процессы используют про граммные версии. Какая связь между этими версиями? Пусть исполняется программа видеоигры. Если пользователь нажмет на клавишу, то это приведет к посылке электриче ского сигнала в порт клавиатуры. Порт клавиатуры вырабатывает реальный, аппаратный сигнал, который приводит к передаче управления из некоторого места видеоигры к драй веру клавиатуры.
278
Событийно-ориентированное программирование. Разработка видеоигры
Код драйвера находится в ядре. Драйвер читает символ на входной линии, передает сим вол на многошаговую обработку в драйвере терминала. Если файловый дескриптор, при соединенный к этому драйверу, установлен в режим асинхронного ввода, то ядро пошлет процессу Unix сигнал. Когда процесс становится активным, то управление будет передано обработчику сигнала в составе этого процесса.
Заключение Основные идеи •
• •
•
•
В некоторых программах поддерживается простой поток управления. В некоторых программах отслеживаются моменты наступления внешних событий. В видеоигре отслеживается изменение времени и появление пользовательского ввода. Операцион ная система также отслеживает изменение времени и поступление входных данных от внешних устройств. Библиотека curses представляет собой набор функций, которые программа может вызывать для управления текстовым выводом на экране. Процесс планирует обслуживание событий с помощью установок таймеров. Каждый процесс может использовать три различных вида таймеров. Когда таймер ’’зазвенит”, то он посылает сигнал. Каждый таймер может “звонить” или однократно, или перио дически, через установленный интервал времени. Управление одним сигналом производится достаточно просто. Управление несколькими сигналами, которые возникают одновременно, уже более сложное. Процесс может принять решение об игнорировании сигналов или о блокировании сигналов. Кроме того, он может указать ядру, какие сигналы требуется блокировать или игнорировать на некоторое время. Ряд функций выполняют сложные действия, которые не должны прерываться. Программа может защитить эти критические секции от прерываний с помощью правильного использования масок сигналов.
Что дальше? В этой главе мы рассмотрели, как видеоигра выполняет сразу несколько действий. Это достигается за счет использования техники приема и обработки сигналов. В Unix можно запускать на исполнение сразу несколько программ. Как происходит развитие процессов? Где находятся процессы? Далее переключим наше внимание с базовых вопросов в облас ти операционных систем и с общих принципов на конкретные вопросы, касающиеся по строения и управления процессами в Unix.
Исследования 7.1 При выполнении системного вызова pause происходит ожидание поступления любого сигнала, включая сигналы, которые вырабатываются при нажатии ключей клавиа туры, таких как, SIGINT. (a) Запустите sleep 1 и нажмите на ключ Ctrl-С. Что происходит? Почему? (b) Модифицируйте программу sleep 1, чтобы было бы можно управлять сигналом SIGINT. (c) Запустите теперь программу на исполнение и нажмите на ключ Ctrl-C. Что проис ходит? Почему?
Заключение
279
12 Медленные устройства. Системный вызов read может быть в ряде случаев прерван. Например, пользователь может нажать ключ Ctrl-C, когда в программе происходило чтение ввода с клавиатуры. Но, с другой стороны, когда программа выполняет вызов read для чтения данных с диска, то при нажатии на ключ Ctrl-C прерывания системно го вызова не произойдет. Обратитесь к электронному справочнику или поищете с помощью Web материал, используя для поиска термин медленное устройство (slow device). Какие обращения к read могут быть прерваны, а какие не могут быть прерваны? Почему? 7.3 Сравнение sigprocmask с ISIG. Другой вариант достижения гарантии непрерываемой работы критической секцией в коде при поступлении сигнала клавиатуры - выключе ние в драйвере терминала флага ISIG. Насколько это решение отличается от варианта, когда необходимые сигналы устанавливаются с помощью маски сигналов? 7.4 Разработайте повторно входную систему для посетителей, которая обеспечивает до бавление их имен и адресов к учетному списку в вашем офисе. Сможете ли вы пре образовать эту систему в алгоритм для добавления трех строк данных в конец тексто вого файла при каждом вызове обработчика? Обратитесь к секции в главе 5, где рас смотрен режим auto-append, и поэкспериментируйте с блокировками файла при ис пользовании link. 7.5 Рассмотрите, каково будет поведение программы bounceld.c, если длина таймерного интервала оказывается короче, чем время, которое необходимо для исполнения move_msg." Что произойдет со значением переменной pos? Что произойдет с экраном? Найдите ответы на эти вопросы при использовании режима блокирования и режима рекурсии. Есть ли возможность предотвратить искажение данных и не потерять сиг налы? 7.6 Прерываемые системные вызовы. В некоторых версиях Unix выполнение getch прерывается, когда поступает на обработку сигнал от таймера. В таких системах вы зов getch после каждого таймерного тика возвращает EOF. Для чего это делается в программе? В чем проблема? Можно ли как-то изменить ситуацию? 7.7 Режим блокирования и режим рекурсии с асинхронным вводом/выводом. В версии программы перемещения, которая использует асинхронный ввод, необходимо ис пользовать два обработчика сигналов. Что произойдет, если приходит сигнал S1GIO в тот момент, когда в программе исполняется обработчик сигнала SIGALRM? Что произойдет в противоположной ситуации? Может ли каждый из этих двух обработчи ков повлиять на работу другого обработчика? Следует ли блокировать сигналы, когда в текущий момент происходит обработка сигналов? Каково ваше мнение о возможно сти использования рекурсивных вызовов? Возникнет ли проблема, если на вход про граммы поступает новый символ, когда программа занята обработкой сигнала SIGIO? Просмотрите все возможные комбинации и составьте список проблем, которые при этом могут возникнуть.
280
Событийно-ориентированное программирование.Разработка видеоигры
Программные упражнения 7.8 Мерцающий текст. В некоторых Web-броузерах поддерживается вывод мерцающего текста и возможность вывода текста в режиме theater-marquee. Модифицируйте про грамму hellol.c так, чтобы она отображала, мерцающее сообщение. Если пользова тель передает сообщение через командную строку, то ваша программа должна отобра жать это сообщение. В противном случае программа должна отображать сообщение по умолчанию. Используйте функцию sleep для реализации паузы в программе между выводом сообщения и последующим стиранием его. 7.9 Режим Theater Marquee, или режим Телеграфной ленты. Напишите программу, которая использует curses для создания режима отображения theater-marquee. Этот режим должен использоваться для отображения содержимого файла. В режиме Theater Marquee (или отображения в режиме телеграфной ленты) используется горизонтальная область для отображения текста. В области возможно горизонтальное скроллирование в режиме посимвольного перемещения по экрану. Ваша программа должна через командную стро ку принимать имя файла и его длину, позицию и скорость отображения. 7.10 Модифицируйте программу hello5.c так, чтобы заменить в ней вызов sleep на вызов usleep. Выберите интервал, который задает сглаженное, но не очень быстрое дейст вие. Модифицируйте программу так, чтобы сообщение замедлялось бы при достиже нии левой или правой границы по пути перемещения, и так, чтобы сообщение ускоря лось бы при перемещении к середине экрана. Представьте себе, что правая сторона экрана ~ планета, а сообщение падает из космоса на ее поверхность. Модифицируйте программу так, что при падении имитируется гравита ционное ускорение. Для усиления эффекта промоделируйте аварийную посадку, когда сообщение достигнет поверхности планеты. Выполните это с помощью слов, которые расщепляются на отдельные буквы. 7.11 В программе tickerdemo.c выход производится из обработчика сигнала. Можно ли сделать выход из функции main, а не из обработчика сигнала? Добавьте в программу глобальную переменную done. Далее сделайте два изменения в программе с тем, что бы был обеспечен выход из main. Какие преимущества и недостатки первого варианта решения и этого нового варианта? 7.12 Аргумент для обработчика сигнала. Модифицируйте программу sigdemoS.c так, что бы объединить два обработчика сигналов в один обработчик, который проводит про верку значения аргумента, для определения, какой сигнал поступил на обработку. Как это изменение повлияло на поведение программы в вашей системе? 7.13 Автоматическое окончание сессии. Случалось ли вам забывать о выходе из сессии при работе с удаленной машиной? Было бы полезным иметь программу, которая рабо тала бы в фоновом режиме и которая после окончания установленного периода време ни посылала бы сигнал SIGKILL вашему log-in shell.
Заключение
281
Напишите программу timeout.c, которой передаются через аргументы командной строки идентификатор процесса (PID) и число секунд. Программа переходит в состояние сна в течение указанного числа секунд, а затем посылает сигнал SIGKILL процессу с задан ным значением PID. Вы можете запустить программу из вашего log-in shell по команде timeout $$ 3600 &. Здесь символ $$ обозначает идентификатор процесса shell. Проблема, которая возникает при работе с программой timeout.c заключается в том, что для вас закрывается сессия, даже если хотели бы продолжать работать дальше. Измените программу так, чтобы она заканчивала бы вашу сессию, если только не бу дет производиться ввод или вывод с вашего терминала в течение десяти минут. (Подсказка: время модификации для файла устройства /dev/ttyxx обозначает время, когда были прочитаны или записаны туда данные. Измените программу так, чтобы она воспринимала имя терминала через аргумент при обращении к программе.) 7.14 Для этого упражнения вы должны смоделировать на пользовательском уровне ситуа цию, изображенную на рисунке 7.14.Там показано, как с помощью одних реальных часов происходит управление двумя различными таймерами. Сначала напишите программу ouch.с на основе программы sigdemol.с, которая в гла ве 6 была названа программой OUCH. При запуске программы ouch.c ей будут передаваться через аргументы командной строки два аргумента: сообщение, которое обработчик сигналов будет выводить, и значение интервала времени, по истечении которого периодически должен быть вы зван обработчик сигнала для вывода сообщения. Например, при обращении: $ouch hello 10 & программа будет запущена на исполнение в фоновом режиме. Программа будет выво дить сообщение “hello” каждые десять единиц времени после приема сигнала SIGINT. Затем напишите программу-метроном, которая будет называться metronome.c. Програм ма принимает из командной строки список идентификаторов процессов. Эта программа должна использовать интервальный таймер, чтобы вырабатывать каждую секунду сиг нал SIGALRM. Обработчик этого сигнала должен использовать системный вызов kill для посылки сигнала SIGINT всем процессам, идентификаторы которых были заданы при обращении к программе в командной строке. Например, при обращении вида: $ metronome 1 3456 7777 2345 будет каждую секунду посылаться сигнал SIGINT процессам 3456, 7777 и 2345. Запустите на исполнение в фоновом режиме три раза программу ouch. При каждом за пуске указывайте через аргументы различные сообщения и различное значение ин тервала. Запомните идентификаторы этих трех процессов. Затем запустите програм му metronome, передавая ей в качестве аргумента число 1 и три запомненных иденти фикатора процессов. 7.15 Блокирование на usleepQ, управление вводом. В программе bounceld.с основной цикл блокировался на getch, а программа управляла анимационными действиями в обра ботчике сигнала. Измените роли пользовательского ввода и анимации в новой версии программы, в которой главный цикл должен блокироваться на usleep, а программа должна управлять пользовательским вводом в обработчике сигнала.
282
Событийно-ориентированное программирование. Разработка видеоигры
7.16 Тестер времени реакции. Напишите программу, которая проводит измерение, насколько быстро отвечает пользователь. Программа ожидает в течение случайного интервала вре мени и затем выводит на экране одну цифру. Пользователь должен быстро, насколько это возможно, набрать на клавиатуре эту же цифру. Программа должна записывать, сколько времени она ожидала ответа от пользователя. Программа должна выполнить десять таких тестов и выдать значения минимального, максимально и среднего времени ответа. (Подсказка: обратитесь к справочной странице gettimeofday). 7.17 Завершите разработку игры в пинг-понг, которая была начата в предшествующем тек сте. Добавьте возможность вести счет, возможность игры нескольких игроков, огра ничители и все, что считаете нужным для того, чтобы сделать игру более привлека тельной.
Проекты На основе материала, который был представлен в этой главе, вы можете изучить и на писать версии следующих Unix-программ: snake, worms
Глава 8 Процессы и программы. Изучение sh
Цели Идеи и средства • • • • •
Что делает Unix shell. Модель процесса в Unix. Как программа запускается на исполнение. Как создается процесс. Как взаимодействуют родительский и дочерний процессы.
Системные вызовы и функции •
fork
•
exec
•
wait exit
Команды • •
sh ps
8.1. Процессы = программы в исполнении Как в Unix происходит запуск программы на исполнение? Все происходит достаточно просто. Вы входите в систему, ваш shell выводит пришашение (prompt), вы набираете текст команды и нажимаете на Enter. В результате будет запущена на исполнение желае мая программа. После того как закончится исполнение программы, ваш shell выведет но вое приглашение. А как это все работает? Что такое shell? Что делает shell? Что делает ядро? Что такое программа и что подразумевается, когда требуется запустить программу на исполнение?
284
Процессы и программы. Изучение sh
Программа - это последовательность команд машинно-ориентированного языка. Эта по следовательность хранится в файле, который обычно получается в результате компиляции исходного кода в двоичный код. Запуск программы на исполнение - это загрузка этого списка машинно-ориентированных команд в память, после чего процессор (CPU) начина ет покомандно выполнять этот список. В терминологии Unix список из машинно-ориентированных команд и данных называют исполняемой программой. А процесс -это пространство в памяти и установки, в соответ ствии с которыми происходит выполнение программы. На рисунке 8.1 показаны програм мы и процессы.
Данные и программы хранятся в файлах на диске. Программы выполняются в составе процессов. Мы будем изучать концепцию процесса в нескольких главах. Начнем исследо вание с эксперимента - рассмотрим возможности команд ps и sh. Далее разработаем нашу собственную версию Unix shell.
8.2. Изучение процессов с помощью команды ps Процесс “живет” в пользовательском пространстве, которое представляет собой часть памя ти компьютера, где находятся исполняемые программы и их данные (см. рисунок 8.2). Мы можем получить информацию о процессах в пользовательском пространстве с помощью команды ps (сокращение от process status, т. е. статус процесса), которая предоставляет список текущих процессов.
8.2. Изучение процессов с помощью команды ps $ps RDTTY 1755 pts/1 1981 pts/1
285
TIME CMD 00:00:17 bash 00:00:00 ps
Я запустил два процесса: bash (т. е. shell) и команду ps. Каждый процесс имеет уникальный идентификатор, который называется идентификатором процесса (process ID), или наиболее часто - просто PID. Все эти процессы соединены с терминалом. В данном случае терминал /dev/pts/1. Для каждого процесса указано время его работы. Заметим, что время работы для процесса ps указано в краткой форме и равно нулю секунд. В команде ps используется много опций. В команде , как и в команде Is, используется опция -а: $ps-a PID TTY 1779 pts/0 1780 pts/0 1781 pts/0 2013 pts/2 2017 pts/2 2018 pts/1
TIME CMD 00:00:13 gv 00:00:07 gs 00:00:01 vi 00:00:23 xpaint 00:00:02 mail 00:00:00 ps
При использовании опции -а выводится список, где содержится большее число процессов, в том числе выводится информация о тех процессах, которые были запущены другими пользователями и с других терминалов. Однако в списке, который выдается при работе с опцией -а, не выводится информация о командных интерпретаторах shells. В команде ps также можно использовать опцию -1 для получения более длинного, более информативно го представления строк в выводимом списке: $ps- la UID FS 000 S 504 000 S 504 000 s 504 000 s 519 000 s 519 000 R 500
PID 1779 1780 1781 2013 2017 2023
PPID 1731 1779 1731 1993 1993 1755
С 0 0 0 0 0 0
PRI 69 69 72 69 69 79
Nl 0 0 0 19 0 0
ADDR SZ 1086 2309 1320 1300 363 750
WCHAN do_sel do_sel do_sel do.sel read_c -
TTY pts/0 pts/0 pts/0 pts/2 pts/2 pts/1
TIME CMD 00:00:13 gv 00:00:07 gs 00:00:01 vi 00:00:23 xpain 00:00:02 mail 00:00:00 ps
В колонке, которая помечена символом S, показывается статус (состояние) каждого процесса. Для команды ps процесс развивается (running), о чем и свидетельствует символ R в этой ко лонке. Остальные процессы находятся в пассивном состоянии (состоянии сна - sleeping), о чем свидетельствует символ S в этой колонке. Каждый процесс принадлежит какому-то пользователю. Для каждого процесса в выводимом списке указывается идентификатор поль зователя UID. Каждый процесс имеет PID. Кроме того, как мы видим, для каждого процесса указывается идентификатор родительского процесса (PPID - parent process ID). В колонках, которые помечены обозначениями PRI и N1, содержится приоритет и поправ ка к приоритету niceness, с помощью чего обозначаются уровни процессов. Ядро исполь зует эти значения, чтобы выбрать в определенные моменты времени процессы, которым следует предоставить процессор (т. е. сделать процесс активным). Процесс может увеличить значение свого niceness уровня. Это аналогично ситуации, когда вы стоите в очереди и разрешаете кому-то становиться в очереди перед вами. Только суперпользова телю разрешено уменьшать значение niceness уровня. Это равносильно тому, что вам пре доставляется право переместиться ближе к голове очереди.
286
Процессы и программы. Изучение sh
В колонке SZ указывается размер процесса. Это значение, которое показывает объем па мяти, которую использует процесс. В данном примере программа mail использует много меньше памяти, чем программа xpaint, в которой необходимы большие объемы памяти для хранения изображений. Значение размера процесса может меняться по мере развития про цесса. В колонке WCHAN представлена информация о причине, по которой процесс находится в состоянии сна. В данном примере все процессы ожидают ввода. Обозначения в колонке вида readj: и dojsel - это адресные ссылки к ядру. Значения в колонках ADDR и F больше не используются, но выводятся в листинг для обеспечения совместимости с программами, которые предполагали вывод значений в этих колонках. При использовании опции -1у по лучается листинг с набором значений, которые используются в более современных верси ях систем. Опции, которые используются в команде ps, значительно разнятся от одной версии Unix к другой. Опции -а и -1, о которых шла речь в предшествующем параграфе, могут на вашей системе не работать или работать, но не так, как было представлено. Следует поэтому прочитать документацию по этой команде на вашей системе. Примеры, которые были здесь приведены, получены при работе с версией, которая называется procps 2.0.6. При меры лишь иллюстрируют большой объем информации, который можно получить с помо щью команды ps. Команда ps весьма разносторонняя. При использовании опций -fa можно получить такой результат: $ ps -fa UID PID betsy 1779 betsy 1780 betsy 1781 yuriko2013 yuriko2017 bruce 401
РРЮ 1731 1779 1731 1993 1993 1755
С 0 0 0 0 0 0
STIME TTY 19:53 pts/0 19:53 pts/0 19:54 pts/0 20:15 pts/2 20:16 pts/2 20:36 pts/1
TIME CMD 00:00:01 gv dinner.ps 00:00:07 gs-dNOPLATFONTS 00:00:02 vi dinner 00:00:00 xpaint 00:00:00 mail bruce 00:00:00 ps -afGh
При использовании опции -f получаем листинг в формате, который легче читать. Здесь вместо UID отображается пользовательское имя. В колонке CMD выводится полный текст командной строки.
8.2.1. Системные процессы Помимо процессов, которые были запущены пользователями, вы можете обнаружить в Unix процессы, которые выполняют системные функции. $ps-ax|head -25 PID TTY ? 1' ? 2 ? 3 ? 4 ? 5 9 35 ? 36 420 ? 423 ? 437 ?
STAT S SW SW SW SW SW SW S S SW
TIME COMMAND init 0:05 3:54 * [kflushd] 0:38 [kupdate] [kpiod] 0:00 2:13 [kswapd] [uhci-control] 0:00 0:00 [khubd] 0:25 syslogd klogd -k /boot/System.map-2.2.14 0:36 0:00 [inetd]
8.2. Изучение процессов с помощью команды ps 449 461 466 471 476 484 500 504 506 512 514 561 562 563 $ps 82
? S ? SW ? S ? S ? S 9 SW ?. S ? SW ? SW ? SW ? SW ttyl SW tty2 SW tty3 SW -ax| wc -1
0:02 0:00 0:00 0:00 0:00 0:00 0:46 0:00 0:00 0:00 0:00 0:00 0:00 0:00
287
amd -F /etc/am. d/conf [rpciod] cron atd sendmail: accepting connections on port 25 [rpc.rstatd] sshd [caiserver] [keyserver] [portsentry] [portsentry] [getty] [getty] [getty]
В приведенном выше примере показаны первые 24 из 82 процессов, которые были запу щены к текущему моменту в системе. Некоторые из них являются системными процесса ми. У большинства системных процессов нет связи с терминалом. Они были порождены при старте системы и недоступны пользователю с уровня командной строки. Что делают все эти системные процессы? Несколько первых процессов в списке управляют различными частями памяти, в том чис ле буферами ядра и страницами виртуальной памяти. Другие процессы (klogd, syslogd) в этом списке управляют системными учетными файлами (logfiles). Процессы cron, atd предназначены для управления пакетными заданиями (batch jobs). Процесс portsentry дол жен выявлять потенциальных злоумышленников. Процессы sshd, getty предоставляют обычным пользователям возможность входа в систему. Вы сможете больше узнать о воз можностях системы Unix, если изучите результаты работы команды ps -ах и прочтете со ответствующие документы в электронном справочнике. Использование команды ps напо минает ситуацию с разглядыванием через микроскоп капельки воды из озера. Вы можете вести наблюдение как за составом, так и за определенным разнообразием процессов, ко торые “живут” в вашем компьютере.
8.2.2. Управление процессами и управление файлами Наши эксперименты с командой ps показали, что процессы имеют много атрибутов. У каждого процесса есть UID, процесс имеет размер, для процесса отмечается, когда он начал работать и сколько времени уже работает. Кроме того, у него есть приоритет и текущее значение nicenessуровня. Некоторые процессы имеют присоединенный терминал, а некоторые не имеют. Где должны храниться все эти свойства процесса? Мы должны отвечать нате же вопросы, которые ставились при рассмотрении файлов. Ядро управляет процессами в памяти и файлами на дис ке. Насколько похожи эти управляющие действия? В файлах содержатся данные, а в процессах находится исполняемый код. Файлы имеют атрибуты, и процессы имеют атрибуты. Ядро создает и уничтожает файлы. То же самое справедливо и в отношении процессов. Ядро хранит несколько процессов в памяти, что аналогично ситуации хранения ядром ряда файлов на диске. Ядро учитывает, какие блоки находятся в памяти. Это необходимо для распределения пространства памяти и поддержа ния порядка вызова процессов при их работе. Насколько похожи методы управления оперативной памятью с методами управления дисковой памятью?
288
Процессы и программы. Изучение sh
8.2.3. Память компьютера и память для программ Концептуально процесс представляет собой абстракцию. Но иногда данное понятие носит весьма конкретный смысл: это объединение некоторого числа байт в памяти. На рисунке 8.3 показаны три модели памяти в компьютере.
Память в Unix разделяется на пользовательское пространство и пространство ядра. Про цессы развиваются в пользовательском пространстве. Память - это, прежде всего, после довательность байтов, но это также и большой массив. Если на вашей машине есть 64 Мбайт памяти, то в таком массиве памяти будет находиться около 67 миллионов ячеек памяти. В некоторых из этих ячеек памяти находятся машинно-ориентированные коман ды и данные, которые и составляют ядро. В определенных ячейках памяти находятся команды и данные процесса. Процесс не обяза тельно занимает один участок (chunk) памяти. Обычно процессы состоят из более мелких участков, аналогично тому, как дисковые файлы состоят из дисковых блоков. И опять же ана логично тому, как для файла поддерживается список распределенных блоков на диске, для процесса заводится структура, где содержится список распределения страниц памяти. Поэтому абстрактным будет такое представление, где каждый процесс представлен как не который бокс в составе пользовательского пространства. Абстрактным представление памяти можно считать непрерывный байтовый массив. При этом известно, что современная память обычно представляет собой ряд чипов памяти, которые расположены на небольшой плате. Создание процесса аналогично созданию файла. Ядро должно найти сначала необходи мое число свободных страниц в памяти, чтобы разместить в них коды команды и данные программы. Ядро также создает в памяти некоторые структуры данных, в которых со держится информация о распределении пространства памяти и атрибуты процесса. Магическим свойством операционной системы является то, что она преобразует струк туру файловой системы в последовательность секторов, расположенных на поверхностях наборов пластин. В результате файловая система предстает в виде дерева с определенным составом взаимосвязанных каталогов. Таким же образом ОС поступает и при управлении процессами. Производится некое преобразование последовательностей битов памяти,
8.3. SHELL: Инструмент для управления процессами и программами
289
которые расположены в чипах памяти. В результате получается нечто, воспринимаемое как некое сообщество процессов, в котором каждому процессу присущи такие свойства, как развитие, взаимодействие, сотрудничество, порождение, возможность выполнять определенные задания, гибель. Полная аналогия с муравейником. Для того чтобы понять свойства процессов, мы изучим и разработаем Unix shell, т. е. про грамму, которая управляет процессами и запускает программы на исполнение.
8.3. SHELL: Инструмент для управления процессами и программами Shell - это программа, которая управляет процессами и запускает программы на испол нение. Существуют несколько shell, которые могут работать в Unix. Это напоминает ситуацию с использованием различных языков программирования. Каждый из них отличается стилем и возможностями. Во всех популярных shell поддерживаются три ос новные функции: (a) Каждый shell запускает программы на исполнение. (b) Каждый shell управляет вводом и выводом. (c) В каждом shell можно вести программирование. Рассмотрим последовательность команд shell: $ grep Ip /etc/passwd lp:x:4:7:lp:/Var/spool/lpd: $TZ=PST8POT; export TZ; date; TZ=EST5EDT Sat Jul 28 02:10:05 PDT 2001
$ date Sat Jul 28 05:10:14 EDT 2001
$ Is -I /etc > etc.listing $ NAME=lp $ if grep $NAME /etc/passwd >then > echo hello | mail $NAME >fi lp:x:4:7:lp:/var/spool/lpd:
$
*
Запуск программ на исполнение Команды grep, date, Is, echo, mail - это обычные программы, которые написаны на языке С и были оттранслированы в коды машинного языка. Командный интерпретатор shell загру жает эти программы в память и затем запускает их на исполнение. Многие пользователи рассматривают shell в качестве программы-стартера.
Управление вводом и выводом Shell делает гораздо больше, чем просто запуск программ на исполнение. Когда пользова тель в командной строке набирает символы >, <, |, чтобы указать на необходимость пере направления ввода/вывода, то shell воспринимает эти символы как требование присоеди нения ввода и вывода процессов к дисковым файлам или к другим процессам.
Процессы и программы. Изучение sh
290
Программирование Shell можно рассматривать и как язык программирования, в котором используются пере менные и средства управления потоком (if while и т. д.). В предшествующем примере бы ло продемонстрировано два случая использования переменных. Во-первых, было при своено значение переменной TZ. Это значение определяло временную зону для западной части U. S. и было использовано командой date при выводе текущей даты и времени. Далее в примере мы видим использование оператора if..then. Переменной NAME в качест ве значения присваивается строка “1р”. Значение этой переменной, т. е. SNAME, использу ется в команде grep, а результат этой команды проверяется в операторе if. Если команда вы полнилась успешно при организации поиска строки “1р” в файле /etc/passwd, то shell выпол нит команду echo hello | mail $NAME. В противном случае управление будет передано сле дующей команде. В этой главе мы рассмотрим, как shell запускает программы на исполнение. В после дующих главах обсудим вопрос использования переменных в shell, а также вопросы управления порядком выполнения, вопросы перенаправления ввода и вывода.
8.4. Как SHELL запускает программы на исполнение Shell выводит приглашение, вы набираете текст команды, shell запускает команду на ис полнение, а затем shell опять выводит приглашение. И далее все повторяется. А что про исходит за кулисами?
Shell выполняет следующие шаги, которые составляют основной цикл работы shell (см. рису нок 8.4). A. Пользователь набирает a.out. B. Shell создает новый процесс для запуска программы на исполнение. C. Shell загружает программу с диска в пространство процесса. D. Программа исполняется в своем процессе, пока не закончится.
8.4.1. Основной цикл shell Shell работает в таком цикле: while (! endofjnput) получить команду выполнить команду ожидать, когда закончится команда
8.4. Как SHELL запускает программы на исполнение
291
Рассмотрим типичное взаимодействие пользователя с shell:
$ls Chap.bak Story08.tr chap08.ps chap08.tr outline.08 Makefile chap08 chap08. short code pix $ps PID TTY TIME CMD 29182 pts/5 00:00:00 bash 29183 pts/5 00:00:00 ps
$ Рассмотрим последовательность событий, которые возникали во времени в том порядке, как это указано на рисунке 8.5. На рисунке направление оси времени слева направо. Shell на рисунке представлен боксом с пометкой “sh”. Бокс начинает перемещение по оси вре мени слева направо. Shell читает строку “Is”, которую набрал пользователь. Shell создает новый процесс, а затем запускает на исполнение программу Is в этом процессе. Далее он ждет, когда будет завершен процесс.
Затем shell читает новую строку на входе, создает новый процесс, запускает программу в этом процессе и ожидает, пока будет закончен процесс. Когда shell обнаружит признак “конец ввода”, то он заканчивает работу. Для того чтобы написать shell, нам необходимо изучить вопросы: 1. 2. 3.
Как запустить программу на исполнение. Как создать процесс. Как организовать ожидания, когда будет выполнен exit().
Когда нам станут ясны ответы на эти вопросы, то мы сможем их объединить и на этой основе написать свой собственный shell.
292
Процессы и программы. Изучение sh
8.4.2. Вопрос 1: Каким образом производится запуск программы? Ответ: С помощью системного вызова execvp. На рисунке 8.6 показано, как одна программа запускает другую программу на исполнение. Например, чтобы запустить на исполнение команду Is -la, программа обращается к вызову execvp("ls", arglist), где arglist - это массив строковых аргументов командной строки. Ядро загружает с диска программу в память. Аргументы командной строки Is и -1а передаются про грамме, и программа запускается на исполнение. В кратком представлении это выглядит так: 1.
Программа вызывает execvp.
2.
Ядро загружает программу с диска в процесс.
3.
Ядро копирует список аргументов arglist в процесс.
4.
Ядро вызывает main(argc,argv).
Далее представлена программа, которая запускает на исполнение команду Is -1,
Г ехес1 .с - показывает, насколько легко из программы запустить на исполнение другую программу
7 main()
{ ............................................................................. .... char *arglist[3]; arglist[0] = "Is"; arglist[1] = "-Г; arglist[2] = 0; printff** * About to exec Is -l\n”); . execvp("ls", arglist); printff* * * Is is done. bye\n");
8.4. Как SHELL запускает программы на исполнение
293
execvp предполагает задание двух аргументов: имя программы, которая будет запускаться, и массив аргументов командной строки для этой программы. Массив аргументов команд ной строки представлен как argv[], когда происходит запуск программы. Заметим, что в качестве первого строкового аргумента мы устанавливаем имя программы. Заметим так же, что этот массив должен иметь в качестве последнего элемента указатель MULL. Откомпилируем и запустим программу: $ссехес1.с -oexecl $ ./ехес1 * * ж About to exec Is -I total 28 drwxr-x— 2 bruce users 1024 Jul 14 21:02 a drwxr-x— 3 bruce users 1024 Jul 16 03:16 с -rw-r-r1 bruce users 0 Jul 14 21:03 у
'
$
Куда подевалось второе сообщение из программы? Просмотрите еще раз текст програм мы. В программе выдается сообщение о том, что будет выполняться по ехес программа Is. Далее выполняется вызов с помощью ехес команды Is. И затем предполагается выдача со общения, которое следует в программе за системным вызовом execvp. Но почему тогда нет на экране второго сообщения? Программа исполняется в процессе, который представлен участком памяти (chunk) и структурами данных ядра для поддержки работы процесса. Таким образом, вызов execvp загружает программу с диска в процесс для того, чтобы процесс смог ее выполнять. Но в какой процесс происходит загрузка? Здесь происходят довольно странные веши: ядро загружает новую программу в текущий процесс, замещая код и данные этого процесса. execvp действует, как действуют при трансплантации мозга. Кто-то может пожелать: ”Я хотел бы решить эту проблему, используя мозг Альберта Эйнштейна, а после ^гого пойти и танцевать твист”. Один из вариантов выполнения этого желания - удалить ваш мозг и поместить в вашу голову на его место мозг Альберта Эйнштейна. Мысли и анали тические способности, которые стали присущи вашей голове, будут как у Альберта Эйн штейна. Однако ваш план сходить на танцы1 исчез вместе с вашим собственным мозгом. Системный вызов ехес очищает память текущего процесса от машинно-ориентированного кода текущей программы, помещает в память код программы, которая была указана в систем ном вызове ехес, а затем запускает новую программу на исполнение. При выполнении ехес из меняется распределение памяти процесса в соответствии с требованиями новой программы. Процесс при этом остался тем же самым, а его содержание оказалось новым. Обобщенная информация о execvpQ execvp НАЗНАЧЕНИЕ
Выполнить файл, который ищется с использованием PATH
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
result = execvp(const char *file, const char *argv[])
АРГУМЕНТЫ
file - имя файла, который будет исполняться argv - строковый массив
КОДЫ ВОЗВРАТА
-1 - при ошибке
1. или сделать что-либо еще по вашему плану действий. 2. execvp - это один из членов семейства функций execv. Все объединение функций обобщенно называют ехес.
Процессы а программы. Изучение sh
294
execvp загружает в текущий процесс программу, которая представлена в системе как файл file, и пытается выполнить программу. Вызов execvp передает в программу список строк в виде массива argv с NULL в конце массива, execvp ищет программу file в каталогах, которые перечислены в переменной среды PATH. При успешном окончании execvp не формируется код возврата. Просто текущая программа удаляется из процесса, а новая программа исполняется в текущем процессе. Пример 2: Формирование приглашения в shell. Мы достаточно узнали для того, чтобы написать первую версию shell. Мы знаем, как можно„запустить программу на исполнение и как передать ей аргументы с уровня командной строки. Сначала shell по приглашению запросит имя программы и аргументы, а затем запустит программу на исполнение. Наша версия программы pshl.c будет реализовывать shell, формирующий приглашения: Г shell, формирующий приглашения, версия 1 Приглашения выдаются для ввода имени команды и для ввода ее аргументов. Строится вектор аргументов для вызова execvp. Используется вызов execvp(), из которого выхода не производится.
7 tinclude <stdio.h> «include <signai.h> «include <string.h> «define MAXARGS 20 «define ARGLEN 100 int main()
I* аргументы с уровня командной строки */ /* длина 7
{ char *arglist[MAXARGS+1];V* массив указателей */ int numargs; Г индекс в массиве */ char argbuf [ARGLEN]; /* сюда будем читать */ char *makestring(); /* выделение памяти */ numargs = 0; while (numargs < MAXARGS)
{ printf("Arg[%d]?", numargs); if (fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n') arglist[numargs++] = makestring(argbuf); else
{ if (numargs > 0){ f* есть аргументы? 7 arglist[numargs]=NULL; /* закрыть список */ execute(arglist); f* сделать 7 numargs = 0; /* и сбросить 7
return 0; int execute(char *arglist[j)
Г
8.4.КакSHELLзапускаетпрограммынаисполнение *
295
использовать execvp
*/
{ execvp(arglist[0], arglist); /* сделать это */ perrorfexecvp failed"); exit(1);
} char * makestring(char *buf)
*/ * правильно сформировать новую строку и создать память для строки */ { char *ср, *malloc(); buf[strlen(buf)-1] = '\0'; /* закончить новую строку */ ср = malloc(strlen(buf)+1); Г получить память */ if (ср == NULL){ Г или получить отказ */ fprintf {stderr, "no memory\nM); exit(1);
} strcpyfcp, buf); f* скопировать символы */ return ср; /* возвратить указатель */
} Программа pshl.c-это наша первая попытка создания Unix shell. Каждый раз программа pshl выдает приглашения на новой строке. Сначала запрашивается имя программы, а за тем запрашиваются по отдельности аргументы для нее. Программа работает в два этапа. (1) Строится список аргументов, строчка за строчкой. В конец списка добавляется NULL. (2) Системному вызову execvp передается строка arglistfO] и массив arglist (см. рисунок 8.7).
Откомпилируем и запустим программу: $ccpsh1.c -о pshl $./psh1 Arg[0]? Is
' Процессы и программы. Изучение sh
296 Arg[1]?-I Arg [2]? demodir Arg[3]? total 2 drwxr-x— 2 bruce drwxr-x— 3 bruce -rw-r-r--1 bruce
4
users users users
1024 Jul 14 21:02 a 1024 Jul 1603:16c 0 Jul 14 21:03 у
$ А как развиваться? Наша программа работает хорошо. Но как мы и предполагали, при выполнении execvp происходит замена кода shell на код команды. Затем происходит выход, в результате чего наш shell не может циклически принять на обработку другую команду. Пользователь вынужден будет повторно запустить shell для того, чтобы можно было бы запустить на исполнение другую команду Как сделать так, чтобы shell мог бы запускать команду и далее мог запускать и другую команду? Решением является создание нового процесса, в котором и будет запускаться на исполнение программа.
8.4.3. Вопрос 2: Как получить новый процесс? Ответ: С помощью системного вызова fork процесс производит копирование себя самого. Использование: fork(); /* выполняется без аргументов */ Изучение системного вызова fork Давайте продолжим обсуждение проблемы принятия решения с использованием мозга Эйнштейна. Как мы уже заметили, пересадка мозга Эйнштейна в вашу голову позволит этой голове мыслить, как голове Эйнштейна. Но при этом ваших мыслей в этой голове уже больше не будет. Одно из возможных решений заключается в том, что процесс дублирует самого себя, что напоминает трехмерное копировальное устройство, которое идентифицирует каждый атом вашего тела и собирает при этом точную копию, строя ее атом за атомом. После того как будет создана такая копия, поместите мозг Эйнштейна в голову копии. Теперь эта ко пия может решать мудреные проблемы, используя для этого мозг Эйнштейна. А вы будете продолжать жить по своим планам и останетесь со своими мыслями. Итак, в определен ный момент вашей жизни вы представляли только самого себя. После того как вы нажали на большую зеленую кнопку на копировальном устройстве, вас уже стало двое. Это ваше раздвоение выглядит аналогично развилке (fork) дороги. Сначала кдет одна дорога, а по том их становится уже две, причем две дороги будут одинаковыми. При работе системного вызова fork все происходит так же. На рисунке 8.8 показана систе ма до и после выполнения системного вызова fork. Процесс содержит программу, которая исполняется в определенном месте. В какой-то момент в процессе происходит вызов fork. Управление передается коду fork, который расположен в ядре. Далее в ядре выполняются такие действия: (а) Распределяется новый участок памяти и выделяются структуры данных ядра. (в) С оригинального процесса снимается копия, которая является новым процессом. (c) Новый процесс добавляется в набор развивающихся процессов. (d) Управление возвращается назад в оба прогресса.
8.4.КакSHELLзапускаетпрограммынаисполнение
297
После того как вы нажали на кнопку Go на копирующем устройстве, вы раздвоились, каж дый из вас физически идентичен, оба находятся в одной и той же точке своего мыслитель ного процесса, но каждый, будучи отдельной сущностью, способен идти своим (или его?) путем. Аналогичная картина возникает, когда процесс вызвал fork. После его выполнения процессов становится два. Оба, как цифровые сущности, идентичны, оба находятся в од ной и той же точке программы, и каждый отдельный процесс способен продолжаться да лее своим собственным путем. Давайте рассмотрим ряд простых программ. Пример: forkdemol .с - Создание нового процесса В forkdemol.с содержатся два предложения printf. Одно находится перед вызовом fork, а другое - после.
Г forkdemol .с * *
Показывает, как fork создает два процесса. Их можно различить с помощью различных кодов возврата, которые получаются после выполнения fork()
7 #include main()
<stdio.h>
{ int retjromjork, mypid; mypid = getpid(); printff'Before: my pid is %d\n", mypid); ret_from_fork = fork(); sleep(1); printff'After: my pid is %d, fork() said %d\n", getpid(), retjromjork);
/* кто я такой? 7 /* сообщите об этом всем 7
) Если бы это была обыкновенная программа, то мы должны были увидеть две строки вы вода. По одной для каждого предложения printf. Однако если мы запустим нашу програм му, то мы увидим:
Процессыипрограммы.Изучениеsh
298 $ cc forkdemol .с -о forkdemol $ ./forkdemol Before: my pid is 4170 After: my pid is 4170, fork() said 4171 $ After: my pid is 4171, fork() said 0
Мы увидим три строки в выводе. Одно сообщение Before: и два сообщения After. Второе сообщение After: выводится процессом 4171. Заметим, что процесс 4171 не выводит сооб щение Before:. А почему не выводит? На схеме, изображенной на рисунке 8.9, показано, что происходит в системе до и после выполнения процессом 4171 вызова fork.
Ядро создает процесс 4171 с помощью репликации процесса 4170. При этом новый про цесс создается посредством копирования кода и текущего значения счетчика команд в ко де. Место, на которое указывает счетчик в коде, показано на рисунке стрелкой. Новый про цесс 4171 начинает сразу же исполняться, но не с начала программы, а после fork. Поэто му процесс 4171, возникший в середине процесса 4170, и не выводит сообщение Before:.
Example: forkdemo2.c - Порожденный процесс создает процессы Дочерний процесс начинает свою жизнь, но он начинает выполнять функцию main не с начала, а сразу после системного вызова fork. Предскажите, сколько строк будет выведено при выполнении этой программы:
Г forkdemo2.c - показывает, как дочерние процессы начинают развиваться сразу * после выхода из fork() и могут далее выполнять код, который они пожелают, * даже fork().Пpeдcкaжитe - сколько будет строк вывода.
7 main()
{ printf("my pid is %d\n", getpid()); fork(); fork();
8.4. Как SHELL запускает программы на исполнение
299
fork(); printf(Hmy pid is %d\n", getpidO);
Откомпилируйте и запустите программу для проверки - насколько оправдались ваши предсказания. Ну и как? Пример: forkdemo3.c - Различие между отцом и сыном В программе forkdemol.с мы увидели, что процесс 4170 вызвал fork и создал дочерний про цесс с PID 4171. Оба процесса при развитии выполняют один и тот же код, начиная с од ного и того же места, используя при этом одни и те же данные и атрибуты процесса. Как процесс может определить, кто он такой - либо процесс-отец, либо процесс-сын? Эти два процесса не являются полностью идентичными. Вывод, который происходит из forkdemol.с, показывает, что после выполнения fork значение кода возврата будет разным для разных процессов. В порожденном процесс системный вызов fork возвращает значе ние, равное 0, а в отцовском процессе возвращает значение, равное числу 4171. Простей шим методом для определения отца или сына может быть проверка значения кода возвра та из вызова fork. В нашем следующем примере, в программе forkdemo3.c, показывается, как в программе ис пользуется код возврата для формирования и вывода различных сообщений. Г forkdemo3.c - показывает, как код возврата из fork()
дает возможность процессу узнать - сын он или отец
*/ «include main()
<stdio.h>
int fork_rv; printf("Before: my pid is %d\n", getpidO); forkjv = fork(); /* создание нового процесса */ if (fork_rv == -1) /* проверка на правильность выполнения */
•Далее показан пример запуска программы на исполнение. $ ./forkdemo3 Before: my pid is 5931 I am the parent, my child is 5932 I am the child, my pid=5932
300
Процессы и программы. Изучение sh
Обобщенная информация о fork
fork НАЗНАЧЕНИЕ
Создать процесс
INCLUDE
#include < unistd.h >
ИСПОЛЬЗОВАНИЕ
pidj result = fork(void)
АРГУМЕНТЫ
Нет
КОДЫ ВОЗВРАТА
*1- при ошибке О-в дочернем процессе pid - в отцовском процессе (это pid дочернего процесса)
Системный вызов fork - это то самое средство, с помощью которого мы можем решить проблему создания процессов в нашем shell при работе в командном режиме. Используя fork, мы имеем возможность создать новый процесс, а также мы можем с помощью этого вызова различить, какой из процессов будет новым, а какой порождающим. В новом про цессе далее можно выполнить вызов execvp для запуска на исполнение некоторой про граммы по желанию пользователя. Мы уже разобрали суть двух из трех вопросов, которые необходимо решать при построе нии shell. Нам известно, как создавать новый процесс (fork), & также мы знаем, как запус кать программу на исполнение (execvp). Далее рассмотрим, как организовать ожидание в родительском процессе события, когда дочерний процесс закончит выполнять команды.
8.4.4. Вопрос 3: Как процесс-отец ожидает окончания дочернего процесса? Ответ: Процесс-отец вызывает wait, чтобы ждать окончания дочернего процесса. Использование: pid = wait(&status); Изучение вызова wait() Системный вызов wait предназначен для решения двух задач. Во-первых, при выполнении системного вызова wait вызывающий процесс переходит в состояние ожидания до того момента времени, пока не закончится выполнение дочернего процесса. Во-вторых, с помощью системного вызова wait процесс-отец может получить значение, которое дочерний процесс передал ему с помощью системного вызова exit. На рисунке 8.10 показано, как работает системный вызов wait. Заметим, что ось времени на рисунке направлена слева направо. В левой части представлено в условном виде, что процесс-отец начинает работу и вызывает fork. Ядро создает дочерний процесс, который на рисунке представлен таким же небольшим боксом и который начинает развиваться параллельно с родительским процессом. Родительский процесс вызывает wait. После это го ядро задерживает родительский процесс до тех пор, пока не закончится дочерний про цесс. Процесс-отец находится в состоянии ожидания на отрезке времени, который на ри сунке помечен словом wait. Через некоторое время дочерний процесс заканчивает работу и выполняет exit(n), чтобы передать родительскому процессу числовой аргумент, значение которого может находить ся в диапазоне от 0 до 255.
8.4. Как SHELL запускает программы на исполнение
301
Когда дочерний процесс вызывает exit, то ядро будит родительский процесс и предостав ляет ему значение аргумента вызова exit, полученное от дочернего процесса. Такая проце дура уведомления и передачи аргумента системного вызова exit изображена на рисунке с помощью направленной дуги, которая исходит из скобок в вызове exit и идет к процессуотцу. Таким образом, системный вызов wait служит для выполнения двух действий: уве домление и коммуникация.
Пример: waitdemol.c- Уведомление waitdemol.c показывает, как обращение к exit в дочернем процессе обеспечивает выход из wait в процессе-отце.
Г waitdemol .с - показывает, как процесс-отец ждет, пока не закончится дочерний процесс */ #include «define DELAY 2 main()
<stdio.h>
{ int newpid; void child_code(), parent_code(); printff'before: mypid is %d\n", getpid()); if ((newpid = fork()) == -1) perror("fork"); else if (newpid == 0) child_code( DELAY); else parent code(newpid);
} Г * новый процесс сначала засыпает, а затем выполняет exit
*/
void child_code(int delay)
{ printf("child %d here, will sleep for %d seconds\n”, getpid(), delay); sleep(delay);
302
Процессы и программы. Изучение sh printf("child done, about to exit\n"); exit(17);
Г x процесс-отец ждет, пока дочерний процесс напечатает сообщение
*/ void parent_code( int childpid) int waitjv; waitjv = wait(NULL);
/* код возврата из wait() */
printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);
Результат работы программы waitdemol.c будет выглядеть так: $ ./waitdemol before: mypid is 10328 child 10329 here, will sleep for 2 seconds child done, about to exit done waiting for 10329. Wait returned: 10329
Запустите эту программу На исполнение. Вы увидите, что родительский процесс всегда ждет, пока не закончится дочерний процесс. На рисунке 8.11 показан поток управления и передача данных между двумя процессами. В родительском процессе поток управления начинается с начала текста программы и затем поток будет блокирован на вызове wait. В дочернем процессе поток управления начинается от вызова fork в тексте main и далее про ходит через функцию child_code. Поток заканчивается вызовом exit. Вызов exit в дочернем процессе является сигналом побудки процесса-отца.
8.4. Как SHELL запускает программы на исполнение
303
Выводы по результатом работы с программой waitdemol.c. Данная программа ил люстрирует два важных фактора относительно вызова wait. 1. Вызов wait блокирует выполнение вызывающей программы до окончания дочернего про цесса В этом примере работы программы родительский процесс блокируется до тех пор, по ка в дочернем процессе не будет выполнен вызов exit. Тем самым двум процессам пре доставляется возможность синхронизировать свои действия. Родительский процесс может с помощью fork породить дочерний процесс, который будет выполнять неко торую работу. Например, будет производить сортировку содержимого определенного файла. Родительский процесс будет ждать, пока не будет выполнена такая задача сортировки, с тем чтобы далее выполнять над файлом уже какие-то другие действия. Для синхронизации выполнения таких задач обработки файла может быть использова на указанная пара системных вызовов: exit и wait. 2. Вызов wait после своего окончания возвращает PID закончившегося процесса В этом примере работы программы код возврата системного вызова wait равен значе нию P1D дочернего процесса, который выполнил вызов exit. Как мы увидим в програм ме forkdemo2.c, процесс может создать несколько дочерних процессов. Рассмотрим про грамму, которая обрабатывает данные из двух удаленных баз данных. В программе бу дет порождено с помощью вызова fork два процесса. Один процесс служит для уста новления связи и извлечения данных из одной базы данных, а другой - для извлечения данных из другой базы данных. Процедура извлечения данных из первой базы данных может потребовать некоторой последующей обработки этих данных, а при извлечении данных из второй базы данных не требуется никакой последующей обработки. По коду возврата из вызова wait родительский процесс будет в состоянии определить - какая из задач закончилась. Поэтому далее он может подключать или нет последующую обра ботку данных. Пример: waitdemo2.c - Коммуникация у Одна из целей системного вызова wait - уведомить родительский процесс о том, что дочерний процесс закончился. Другая цель системного вызова wait - сообщить родитель скому процессу, как закончился дочерний процесс. Успех, неудача и гибель.- Процесс может закончиться по одному из трех вариантов. Вопервых, процесс может успешно выполнить решаемую им задачу. По соглашению, при нятому в Unix, при успешном выполнении программа выполняет вызов exit(O) или воз вращает 0 при выходе из main. Во-вторых, программа может не справиться с выполнением своей задачи. Например, про грамма может закончиться раньше положенного, если при своем исполнении у нее не хва тит памяти. По соглашению, принятому в Unix, в программах, где обнаруживаются ошиб ки при выполнении, будет выполняться системный вызов exit с ненулевым аргументом. Программист для каждого из возможных видов ошибок устанавливает значения таких ко дов возврата. В электронном справочнике приводятся значения таких кодов. Наконец, программа может быть закончена по сигналу (см. главы 6 и 7). Сигнал может быть послан с клавиатуры, от внутреннего таймера, из ядра, прийти от других процессов. Обычно если сигнал не игнорируется и не перехватывается в процессе, то при поступле нии он убивает процесс. После выполнения wait полученное значение кода возврата равно PID дочернего процесса, который закончил работу. А как процесс-отец узнает о результативности окончания дочернего процесса - закончился ли он успешно, неудачно или был убит?
304
Процессы и программы. Изучение sh
Получить ответ можно с помощью аргумента системного вызова wait. Процесс-отец вызы вает wait, указывая в аргументе адрес целочисленной переменной. Ядро сохраняет в этой переменной статусную информацию об окончании дочернего процесса. Если дочерний процесс вызывает exit, то ядро поместит аргумент вызова exit в эту целочисленную пере менную. Если дочерний процесс будет убит, то ядро в эту целочисленную переменную по местит номер сигнала. Поле для представления целочисленной переменной состоит функ ционально из трех частей: поле в восемь разрядов для хранения аргумента вызова exit, по ле из семи разрядов для размещения в нем значения номера сигнала, и один разряд служит для индикации того, что был получен дамп образа процесса. На рисунке 8.12 показаны три поля, в которых размещается статусная информация о дочернем процессе.
Наш следующий пример, waitdemo2.c, построен на основе программы waitdemol.c. Програм ма производит отображение статусной информации о закончившемся дочернем процессе.
Г waitdemo2.c - показывает, как процесс-отец получает статусную информацию о дочернем ’ процессе
7 «include <stdio.h> «define DELAY-5 main()
{ int newpid; void child_code(), parent_code(); printf("before: mypid is%d\n", getpid()); if ((newpid = fork()) == -1) perrorffork"); else if (newpid == 0) child.code(DELAY); else parent_code(newpid); i
8.4. Как SHELL запускает программы на исполнение
305
■Г * новый процесс засыпает, а затем выполняет exit
7 void child code(int delay)
{ printf("child %d here, will sleep for %d seconds\n", getpidO, delay); sleep(delay); printf("child done, about to exit\n"); exit(17);
} /* * процесс-отец ждет, когда дочерний процесс отпечатает сообщение 7 void parent code(int childpid)
{' int wait.rv; /* код возврата из wait() */ int child.status; int high_8, low_7, bit_7; wait_rv = wait(&child_status); printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv); high" 8 = child.status » 8; /* 1111 1111 0000 0000 7 low_7 = child.status & 0x7F; /* 0000 0000 0111 1111 7 bit_7 = child.status & 0x80; /* 0000 0000 1000 0000 7 printf("status: exit=%d, sig=%d, core=%d\n", high 8, low_7, bit.7);
} Сначала позволим программе waitdemo2 закончиться нормально. Статусная информация дочернего процесса из системного вызова exit будет скопирована и передана процессуотиу:
$ ,/waitdemo2 before: mypid is 10855 child 10856 here, will sleep for 5 seconds child done, about to exit done waiting for 10856. Wait returned: 10856 status: exit=17, sig=0, core=0
$ Далее мы запустим программу waitdemo2 в фоновом режиме и с помощью команды kill (см. главу 7) пошлем дочернему процессу сигнал SIGTERM:
$ ./waitdemo2 & $ before: mypid is 10857 child 10858 here, will sleep for 5 seconds kill 10858 $ done waiting for 10858. Wait returned: 10858 status: exit=0. sia=15. core=0
306
Процессы и программы. Изучение$>h
Обобщенная информация о waitQ wait НАЗНАЧЕНИЕ
Ожидать окончания процесса
INCLUDE
#include <sys/types.h> #include <sys/wait.h>
ИСПОЛЬЗОВАНИЕ
pidj result = wait(int *statusptr)
АРГУМЕНТЫ
Указатель на статусную информацию дочернего процесса
КОДЫ ВОЗВРАТА
-1 - при ошибке, pid - закончившегося процесса
См. также
waitpid(2), wait3(2)
Системный вызов wait приостанавливает вызывающий процесс, пока не станет доступной ста тусная информация об одном из его дочерних процессов. В качестве такой информации будет или код возврата, или номер сигнала. Если один из дочерних процессов уже был закончен или был^убит сигналом, то происходит немедленный выход из вызова wait. Системный вызов wait возвращает PID закончившегося процесса. Если в системном вызове wait значение аргумента не было равно NULL, то производится копирование статуса из exit или номера сигнала в пере менную, на которую направлен указатель в wait. Значения полей статусной информации можно проверить с помощью макросов, которые находятся в <sys/wait.h>. Системный вызов wait возвращает -1, если у вызывающего процесса не было дочерних процессов или нет статусной информации об окончании.
8.4.5. Итог: Как Shell запускает программы на исполнение Этот раздел начинался с вопроса: “Как shell запускает программы на исполнение?” Теперь мы получили ответ, shell использует системный вызов fork для создания нового процесса. Затем shell применяет системный вызов ехес для того, чтобы запустить в новом процессе программу, которую требует исполнить пользователь. Наконец, shell использует системный вызов wait, чтобы ждать, пока в новом процессе закончится исполнение программы. Кроме того, системный вызов wait получает из ядра статусную информацию из exit или номер сиг нала. На основе этой информации можно оценить, как закончился дочерний процесс. Время
1 .Выдача 2. Прием 3. Создание 4. Ожидание окончания 5. Принять статусную приглашения команды нового процесса дочернего процесса информацию дочернего процесса
4. Запуск на исполнение 5. Исполнение 6. Новая программа новой программы новой программы закончила исполнение
Рисунок 8.13 Последовательность шагов в цикле shell с выполнением fork(), ехес(), wait()
307
8.5. Создание shell: psh2:c
Любой shell в Unix использует модель, которая представлена на рисунке 8.13. Далее мы объединим все три рассмотренных системных вызова и напишем реальный shell.
Создание shell: psh2.c
8.5.
На рисунке 8.14 представлена упрощенная блок-схема выполнения действий в составе shell в Unix. Наш shell, который реализован в составе программы psh2.c, использует логику, которая представлена указанной блок-схемой.
/** shell версия 2
*7
Решается “проблема однократности”, которая была присуща версии 1 Используется системный вызов execvp(), но предварительно используется fork(), с тем чтобы shell мог бы ожидать возможности выполнения следующей команды. Новая проблема: shell перехватывает сигналы. Запустите vi, нажмите лс;
«include «include «define MAXARGS «define ARGLEN mainQ
<stdio.h> <signal.h>
20 100
Г аргументы командной строки */ Г длина аргументов */
{ char *arglist[MAXARGS+1 ]; /* массив указателей 7 int /* индекс в массиве 7 numargs; char argbuf[ARGLEN]; будетбудет происходить чтение */ argbuf [ARGLEN];I* сюда Г сюда происходить чтение char *makestring(); Г выделение памяти */ numargs = 0; while (numargs < MAXARGS)
{ printf("Arg[%d]?", numargs); if (fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n') arglist[numargs++] = makestring(argbuf); else
{ if (numargs > 0){ f* есть аргументы? 7 arglist[numargs]=NULL; /* завершить список 7
308
Процессы и программы. Изучение sh execute(arglist); /* выполнить это */ numargs = 0; /* и выполнить reset */
} } return 0;
} execute(char *arglist[])
Г *
использование fork и execvp, ожидание, пока это будет сделано
7 { int pid = fork(); switch(pid){
pid.exitstatus;
Г PID и статус дочернего процесса */ Г образовать новый процесс 7
case-1: perrorf'fork failed"); exit(1); caseO: execvp(arglist[0], arglist); /* do it */ perror("execvp failed”); exit( 1 ); default: while(wait(&exitstatus) != pid)
i printff'child exited with status %d,%d\n", exitstatus»8, exitstatus&0377);
} } char *makestring(char *buf)
Г * оформить правильно новую строку и отвести память для строки
7 { char *ср, *malloc(); buf[strlen(buf)-1 ] = ’\0'; /* сформировать новую строку 7 ср = malloc(strlen(buf)+1); /* получить память 7 if (ср == NULL){ I* или умереть */ fprintf(stderr,"no memory\n"); exit(1);
} strcpy(cp, buf); Г копирование символов */ return ср; f* возвратить указатель ptr */
} Проверим работу psh2 и убедимся, решается ли “однократная проблема” после внесения изменения в execute.
8.5. Создание shell: psh2.с
309
$ ./psh2 Arg[0]? Is Arg[1]? -I Arg[2]? demodir Arg[3]? total 2 drwxr-x--- 2 bruce users 1024 Jul drwxr-x--- 3 bruce users 1024 Jul -rw-r--r--1 bruce users 0 Jul child exited with status 0,0 Arg[0]? ps Arg[1]? PID TTY TIME CMD 11616 pts/4 00:00:00 bash 11648 pts/4 00:00:00 psh2 11664 pts/4 00:00:00 ps child exited with status 0,0 Arg[0]? psh 1 Посмотрите! Мы смогли запустить psh 1 Arg[1]? Arg[0]?ps Это приглашение для pshl! Arg[1]? PID TTY TIME CMD 11616 pts/4 00:00:00 bash 11648 pts/4 00:00:00 psh2 11683 pts/4 00:00:00 ps child exited with status 0,0 Arg[0]? grep Arg[1]?fred Arg[2]? /etc/passwd Arg[3)? child exited with status 1,0 Arg[0]? Нажмите здесь несколько раз на Л0 ■ Arg[0]? Arg[0]? Arg[0]? exit Arg[1]? execvp failed: No such file or directory child exited with status 1,0 Arg[0]? Нажмите здесь на ЛС
$ Что нужно еще сделать? Программа psh2.c работает хорошо. Этот новый shell воспринимает с уровня командной строки имя программы и список аргументов, запускает программу на исполнение, выво дит сообщения о полученных результатах и опять циклически возвращается к месту прие ма и последующего исполнения очередной программы. Программе psh2.c недостает со вершенства обычных shell, но она представляет собой исходную точку в создании таких shell.
310
Процессы и программы. Изучение sh
Для последующих версий необходимо сделать следующие улучшения: (a) Обеспечить пользователю возможность выхода из shell при нажатии на Ctrl-D или с помощью выполнения команды “exit”. (b) Позволить пользователю набирать сразу все аргументы на одной строке. Эти свойства будут нами добавлены при работе с материалом следующей главы. В этой версии мы добавим переменные и средства управления ходом выполнения в составе shell, что сделает реализацию shell еще более похожей на программу, написанную на языке про граммирования. Однако прежде всего нам требуется справиться с серьезной проблемой в psh2.c.
8.5.1. Сигналы иpsh2.c Как мы увидели при тестировании программы, есть только одна возможность закончить ее йсполнение - нажать на Ctrl-C. Что произойдет, если мы нажмем на Ctrl-C в то время, когда программа psh2 будет ожидать момента окончания дочернего процесса? Например:
Arg[0]?tr Arg[1J? [a-z] Arg[2]? [A-Z] Arg[3]? hello HELLO now to press NOW TO PRESS Ctrl-C НажатьздесьнаЛС $ В данном случае закончится дочерний процесс, а также будет закончен И наш shell. Сигнал SIGINT, который вырабатывается при нажатии на Ctrl-C, убивает процесс выполнения коман ды tr, а также процесс, в котором выполняется программа psh2. Почему так происходит?
Сигналы, которые вырабатываются с помощью клавиатуры, поступают на ВСЕ присоединенные процессы. Обе программы, psh2 и tr, присоединены к терминалу (см. рисунок 8.15). При нажатии на ключ прерывания, драйвер терминала требует от ядра послать сигнал SIGINT всем про цессам, которые связаны с этим терминалом. Процесс tr умирает. Наша программа psh2 также заканчивается.
8.6. Защита: программирование процессов
311
Как можно предотвратить убийство нашего shell, что может произойти при нажатии поль зователем на ключи прерывания или выхода? Эти модификации мы оставим для выполне ния в упражнениях.
8.6. Защита: программирование процессов Мы хотели понять особенности работы с процессами в Unix. Поэтому мы “поиграли ” с ко мандой ps и изучили особенности использования в shell системных вызовов fork, ехес, exit, wait с целью управления процессами и запуска программ. Рассмотрим, в чем заключается сходство между функциями и процессами, execvp/exit и call/return call/return
В программе, написанной на языке С, активно используются функции. Одна функция мо жет вызывать другую и передавать ей при этом список аргументов. Вызываемая функция выполняет некоторые действия и возвращает после выполнения некий результат. В каж дой функции имеются свои собственные локальные переменные. Различные функции взаимодействуют между собой с помощью механизма call/return. Основой структурного программирования является модель функций с приватными дан ными, которые взаимодействуют между собой с помощью передачи списков аргументов и получения в ответ результатов работы функций. В Unix предоставляется возможность распространить эту модель с уровня функций на уровень самих программ. Модель может быть изображена в виде, который представлен на рисунке 8.16. exec/exit
В программе на языке С можно выполнить fork/exec в отношении другой программы и при этом передать новому процессу список аргументов. Вызываемая программа выполняет некоторые действия и может возвратить результат с помощью вызова exit(n). В вызывающем процессе можно принять значение аргумента exit с помощью вызова wait(&result). Значение из вызова exit передается из подпрограммы и оказывается в разря дах 8-15 слова result в вызывающей программе. Стек вызовов практически не имеет ограничений. Вызываемая функция может вызывать другие функции, а программа, которая стала выполняться в процессе после выполнения fork/exec, может вызвать на выполнение другие программы с помощью этого же механиз ма fork/exec. Система Unix разработана так, чтобы имелась возможность быстро и просто создавать новые процессы. Механизм fork/exec и exit/wait, который используется для вызова программ и получения результатов после выполнения, применяется не только при работе в shell. Приложения часто разрабатывают как набор программ, запускающих при исполнении подпрограммы, вместо создания одной большой программы, содержащих большое количество функций. Аргументы, которые передаются с помощью ехес, должны быть строковыми переменны ми. Это одновременно накладывает ограничения и на все коммуникации между подпро граммами. В свою очередь, требование на поддержание текстового интерфейса между программами почти автоматически приводит к такому же требованию в отношении взаи модействий между платформами. Последствия от реализации этого требования могут быть драматическими.
312
Процессы и программы. Изучение sh
Глобальные переменные и fork/exec Использование глобальных переменных принято считать плохим стилем. Использование таких переменных приводит к нарушению принципов инкапсуляции, глобальные пере менные приводят к возникновению сторонних эффектов3, к появлению кодов, неудобных при эксплуатации. Но некоторые альтернативы могут быть еще хуже по своим проявлени ям. Возникает вопрос: каким образом можно будет управлять связкой значений, которые всем нужны, и не загромождать список аргументов, особенно в случае, когда эти значения нужно передавать по иерархическим уровням? В Unix есть метод, который позволяет создавать глобальные значения. Текстовые пере менные, которые передаются по значению дочерним процессам, образуют среду. Являясь устойчивой к возникновению сторонних эффектов, среда оказывается полезным дополне нием для механизма fork/exec, exit/wait. В следующей главе мы рассмотрим, как работает это средство и как его можно использовать.
8.7. Дополнение относительно EXIT и ЕХЕС Основными темами этой главы были процессы, системные вызовы fork, execvp, wait. Но нам понадобилось рассмотреть несколько деталей, касающихся системных вызовов exit и ехес.
8.7.1. Окончание процесса: exitn_exit Процесс может быть закончен с помощью вызова exit, который является по смыслу проти воположным вызову fork. Системный вызов fork создает процесс, а вызов exit удаляет про цесс из системы. Все выглядят вполне логичным. Вызов exit сбрасывает все потоки,. вызывает функции, которые были зарегистрированы с atexit и on_exit, и выполняет те функции, которые были ассоциированы exit в конкретной системе. Далее происходит обращение к системному вызову _exit. Системный вызов exit является функцией ядра, при выполнении которого производится освобождение всей па мяти, выделенной процессу, закрытие всех файлов, открытых процессом. Кроме того, происходит освобождение всех структур данных, которые были задействованы ядром для управления процессом. 3. Некоторые утверждают, что глобальные переменные вызывают появление бородавок.
8.7. Дополнение относительно EXIT и ЕХЕС
313
Что происходит с аргументом, который передается дочернему процессу с помощью exit? Это значение, которое является последним сообщением процесса, сохраняется в ядре до тех пор, пока процесс-отец не воспримет это значение с помощью системного вызова wait. Если процесс-отец в текущий момент не выполнил wait, то значение аргумента в вызове exit остается в ядре до тех пор, пока процесс-отец не выдаст wait. Это событие будет рас ценено как окончание дочернего процесса и прием его последнего сообщения. Процесс, который “умер”, но о котором сохраняется все еще не востребованное значение аргумента exit, называется зомби. В протоколах современных версий команды ps такие процессы помечаются меткой defunct. Обобщенная информация о _exit() _exit НАЗНАЧЕНИЕ
Закончить текущий процесс
INCLUDE
#include #include <stdlib.h>
ИСПОЛЬЗОВАНИЕ
void _exit(irtt status)
АРГУМЕНТЫ
status - возвращаемое значение
КОДЫ ВОЗВРАТА
Нет
См. также
atexit(3), exit(3), on_exit(3)
Системный вызов exit заканчивает текущий Процесс и выполняет все необходимые дей ствия по очистке от пребывания процесса в системе. Эти действия варьируются от одной версии Unix к другой, но во всех этих версиях всегда есть следующие операции: (a) Закрыть все файловые дескрипторы и дескрипторы каталогов. '* (b) Изменить у всех дочерних процессов (для заканчивающегося процесса) значения ро дительского идентификатора процесса (parent PID) на значение, равное PID процесса init. (c) Уведомить порождающий процесс об окончании дочернего, если он выполняет wait или waitpid. (d) Послать процессу-отцу сигнал SIGCHLD. Если процесс заканчивается раньше, чем порожденные им процессы, то эти дочерние про цессы продолжают развиваться в системе. Они не остаются сиротами, а становятся “деть ми” процесса init, который будет выполнять роль опекуна. Заметим также, что даже если процесс-отец не вызовет wait, ядро все равно оповестит порождающий процесс о том, что закончен дочерний процесс, путем посылки сигнала SIGCHLD. Однако по умолчанию сиг нал SIGCHLD в процессе игнорируется. Поэтому если вы желаете, чтобы программа реа гировала на поступление этого сигнала, то в программе необходимо установить обра ботчик этого сигнала.
8.7.2. Семейство вызовов ехес В нашем shell и в наших программах мы использовали вызов execvp, чтобы показать, каким образом процерс может запускать программы на исполнение. Следует заметить, что execvp не является системным вызовом. Это библиотечная функция, которая использует системный вы зов execve, который и обеспечивает доступ к сервисам ядра. Символ е в execve соответствует термину environment. На эту тему мы поговорим в следующей главе. Есть еще ряд полезных функций, которые вызывают execve. Далее приведен еще ряд пред ставителей семейства ехес:
314
Процессы и программы. Изучение sh exedpJfile.argvO.argvlNULL)
execlp не использует массив аргументов, как это было в execvp. Вместо этого аргументы передаются в функцию main с помощью механизма argv[], когда аргументы просто включенььв список аргументов для execlp. Например: execlpf'ls", "Is”, "-a", "demodir”, NULL);
Здесь показан пример запуска программы Is с установленными для этой команды аргумен тами. execlp полезна в тех случаях, когда вы заранее знаете команду, которую хотели бы выполнить, и аргументы для этой команды. Использовать execlp в shell не представляется возможным, поскольку вы не знаете, сколько аргументов будет набирать пользователь при вводе командной строки. execl(fullpath, argvO, argvlNULL)
Символ p в execlp и execvp соответствует термину path. Эти две функции при выполнении организуют поиск программы, имя которой задано значением первого аргумента при обращении к функции. Они ищут программу во всех каталогах, которые перечислены в списке, хранящемся переменной окружения PATH. Если вам известно точное место распо ложения программы, то его можно задать в качестве первого аргумента при обращении к execl. Например: execl(7bin/ls”, ”ls”, ”-а", "demodir”, NULL);
При таком обращении будет запущена на исполнение программа /bin/ls с указанными для нее аргументами. При указании места расположения программа вызывается бы стрее, чем в случае использования execlp. В последнем случае поиск требуемой програм мы может происходить в нескольких каталогах. При использовании точного указания места расположения программы достигается большая защищенность, чем в случае ис пользования execlp. Если в переменной PATH содержится неправильный список катало гов, то таким способом вы можете запустить не ту версию программы. execv(fullpath, arglist)
Функция execv выполняет то же, что и функция execvp. Но в функции execv не производится поиск файла, используя для этого переменную PATH. Первый аргумент функции должен точно указывать место расположения программы, которая будет запускаться на исполне ние. При использовании функций execv и execl с указанием точного пути к программе дос тигается больший уровень секретности, чем при использовании списка каталогов в пере менной PATH, который может быть легко изменен злоумышленниками.
Заключение Основные идеи •
•
• •
В Unix исполнение программы - это загрузка исполнимого кода в процесс и последующее исполнение этого кода. Процесс - это пространство памяти и другие системные ресурсы, которые необходимы программы для ее исполнения. Каждая программа при своем исполнении находится в составе своего собственного процесса. Процесс имеет: уникальный идентификатор процесса (PID), собственника процесса, размер, а также другие свойства. Системный вызов fork создает новый процесс путем построения почти точной копии вызывающего процесса. Новый процесс называют дочерним процессом. Программа может загрузить в текущий процесс посредством вызова требуемой функции из семейства функций ехес.
Заключение
• •
•
315
Программа может ожидать, когда закончится дочерний процесс. Это достигается с помощью выполнения вызова wait. Вызывающая программа может передавать список строковых аргументов для функции main в новой программе. Новая программа может возвратить вызывающей программе небольшое числовое значение, которое передается с помощью вызова exit. В Unix shell управление исполнением программ выполняется с помощью fork, ехес и wait.
Что дальше? Shell производит запуск программ на исполнение. Но shell - это также и язык програм мирования. Далее мы рассмотрим скрипты (процедуры) shell и то, каким образом следует модифицировать нашу версию shell, обеспечить обработку скриптов, управление поряд ком выполнения команд и как использовать переменные.
Исследования 8.1 Родительские и дочерние процессы. По коду возврата из fork процесс может опреде лить, является ли он процессом-отцом или дочерним процессом. Какие другие средст ва может использовать процесс? 8.2 Предскажите, каким будет вывод при работе такой программы: main()
{ int n; for(n = 0; n<10; n++)
{ printf("my pid = %d, n = %d\n", getpid(), n); sleep(1); if (fork() != 0) exit(0);
/* что будет, если эти две */ Г строки будут удалены? */
} } Каким будет вывод программы, если две строки, содержащие комментарий, будут уда лены? 8.3 В программе psh2.c используется массив фиксированной длины, где содержится список аргументов. Каким образом следует модифицировать программу, чтобы устранить ограничение на количество аргументов, которые пользователь может задавать при обращении к команде? Необходима ли такая модификация? Как воспримет Unix уста новление пределов на число и на длину аргументов для ехес? 8.4 fork и файловые дескрипторы. Изучите такой код: main() { int fd; int pid; char msg1[] = 'Testing 1 2 3..\n"; char msg2[] = "Hello, hello\n"; if ((fd = creatC'testfile", 0644» == ■ 1) return 0;
Процессы и программы. Изучение sh
316 if (write(fd, msgl, strlen(msg1)) == -1) return 0; if ((pid = fork()) return 0; if (write(fd, msg2, strlen(msg2)) == -1) return 0; close(fd); return 1;
} Проверьте, как будет работать эта программа. После вызова fork в обоих процесса фай ловый дескриптор будет установлен в одну и ту же позицию в выходном файле. Сколь ко сообщений попадет в этот файл? С помощью каких строк вы будете судить о файло вых дескрипторах и связях с файлами? 8.5 fork и стандартный ввод/вывод. Изучите такой код: «include <stdio.h> main()
{ FILE *fp; int pid; char msgl [] = 'Testing 1 2 3..\n"; char msg2[] = "Hello, hello\n"; if ((fp = fopen("testfile2", "w")) == NULL) return 0; fprintf(fp, "%s", msgl); if ((pid = fork()) ==-1) return 0; fprintf(fp, "%s”, msg2); fclose(fp); return 1;
} Проверьте, как работает эта программа. Сколько сообщений окажется в файле? Объяс ните полученный результат. Сравните результаты вывода этой программы с результа тами работы программы forkdemol .с. 8.6 Фоновая обработка. Откомпилируйте и запустите на исполнение программу: main()
{ inti; if (fork() != 0) exit(0); for (i=1; i<=10; i++){ printff'still here..\n"); sleep(i);
} return 0;
1
Заключение
317
Разберитесь, что делает эта программа и как она работает. В Unix пользователи могут запускать программы на исполнение в фоновом режиме. Насколько эта программа подходит для исполнения в фоновом режиме? 8.7 Ошибки при выполнении ехес. Если выполнение функции ехес заканчивается в дочернем процессе аварийно, то программа вызывает exit. Обращение к exit - это экстремальное проявление действий. Почему не используется возврат из функции с выдачей сообще ния о коде ошибки?
Программные упражнения 8.8 Ожидание окончания двух дочерних процессов. Расширьте код программы waitdemol.c с тем, чтобы процесс-отец создавал бы два дочерних процесса и далее ожидал, когда каждый из них выдаст exit. Модифицируйте ваше решение дальше так, чтобы программа могла бы воспринимать некоторое целое число, которое должно задаваться в качестве аргумента в командной строке. Далее программа должна создавать дочерние процессы, количество которых задается значением переданного числа. Для каждого процесса должна задаваться случайная длина временного интервала (в секундах), на котором процесс спит. Нако нец, процесс-отец после каждого выполнения exit в дочернем процессе должен быть оповещен о таком событии. 8.9 Использование сигнала SIGCHLD. Напишите программу, с помощью которой поможет изучить действие сигнала SIGCHLD. Модифицируйте программу waitdemo2.c, где нужно установить обработчик сигнала SIGCHLD, а затем построить цикл, где будет каждую секунду выводиться сообщение “waiting”. После того, как дочерний процесс выполнит exit, процесс-отец должен вывести сооб щение, в котором должно находиться статусное сообщение дочернего процесса. После чего процесс-отец должен завершить работу. 8.10 Множественность сигналов SIGCHLD. Напишите программу, которой передается при вызове в качестве аргумента целое число. В соответствии со значением этого чис ла программа создает указанное число дочерних процессов. Все дочерние процессы засыпают на пять секунд, а затем выполняют exit. Процесс-отец устанавливает обра ботчик для сигнала SIGCHLD, после чего входит в цикл, где выводит сообщение раз в секунду. Обработчик сигнала вызывает wait. Он выводит сообщение о PID дочернего процесса и увеличивает счетчик. Когда счетчик достигнет значения аргумента, который был передан в программу, то программа должна быть завершена. Проверьте работу этой программы, задавая различное число порождаемых дочерних процессов. Программа может пропустить факт окончания некоторых дочерних процессов, когда задается требование на порождение слишком большого числа дочерних процессов. Сможете ли вы объяснить, почему могут возникнуть потери сигналов? Каким может быть решение этой проблемы? 8.11 Модифицируйте программу psh2.c с тем, чтобы программа заканчивала исполнение, если пользователь наберет на терминале команду “exit” или когда программа обнару жит конец файла. 8.12.Сигналы и shells. Стандартные Unix shells не заканчивают свою работу, если пользо ватель посылает сигнал прерывания или сигнала выхода при работе дочернего процес са. Как стандартный Unix shell реагирует на эти сигналы, когда производится чтение командной строки? Модифицируйте программу psh2.c с тем, чтобы достигнуть такого же поведения, которое свойственно обычному shell.
Глава 9 Программируемый shell. Переменные и среда shell
Цели Идеи и средства • • • • •
В Unix shell является языком программирования. Что такое shell скрипт? Каким образом shell обрабатывает скрипт? Как в shell работают управляющие структуры? exit(O) = успех. Shell переменные: зачем и как? Что такое среда? Как она устроена?
Системные вызовы и функции • •
exit getenv
Команды •
env
9.1. Программирование в среде SHELL Командный интерпретатор shell запускает программы на исполнение и одновременно сам рассматривается как язык программирования. (Более точно - командный интерпретатор не является языком программирования. Но он поддерживает язьпс программирования с од ноименным названием. -Примеч. пер.) Неотъемлемой частью Unix являются программы, написаные на языке shell, которые называют $\\е\\-скриптами. Например, процедура начальной загрузки Unix, а также многие административные программы используют shell-скрипты. В этой главе мы начнем изучение программных возможностей shell, а затем мы добавим некоторые из этих возможностей к тому shell, который мы написали в послед ней главе. В частности, мы добавим управляющую структуру if.Jhen, локальные перемен ные и глобальные переменные.
9.2. SHELL-скрипты: что это такое и зачем?
319
9.2. SHELL-скрипты: что это такое И зачем? Unix shell - это интерпретатор языка программирования. Shell интерпретирует команды, которые поступают от пользователя при наборе на клавиатуре, а также интерпретирует последовательности команд, которые хранятся в составе shell-скриптов.
9.2.1. Shell скрипт - это пакет команд Shel I-скрипт представляет собой файл, в котором находится последовательность (пакет) команд. Запуск скрипта означает выполнение каждой команды из такого файла. Вы може те использовать скрипт, который будет выполнять несколько команд по мере выдачи одно го требования. Вот пример: # Это скрипт scriptO # Он запускает определенные команды
Is echo the current dateAime is date echo my name is whoami
Первые две строки - это комментарии. В shell строки, которые начинаются с символа #, игнорируются. Оставшаяся часть скрипта содержит команды. Shell выполняет команды из скрипта одну за другой, пока не будет достигнут конец файла или пока shell не обнаружит команду exit. Исполнение shell-скрипта. Вы можете запустить shell-скрипт на исполнение, передавая командному интерпретатору имя скрипта в качестве аргумента: $ sh scriptO scriptO scriptl script2 script3 the current date/time is Sun Jul 29 23:29:49 EDT 2001 my name is bruce
$ Или вы можете установить право на исполнение для файла и просто набрать имя скрипта (Здесь предполагается, что имя файла и имя скрипта одно и то же. - Примеч. пер.). $ chmod +х scriptO $ scriptO scriptO scriptl script2 script3 the current dateAime is Sun Jul 29 23:31:23 EDT 2001 my name is bruce
$
320
Программируемый shell. Переменные и среда shell
При этом вам понадобится выполнить команду chmod в отношении скрипта только один раз. Бит исполнения остается установленным до тех пор, пока вы его не измените. Второй метод, который основан на представлении файла как исполнимого и который предполагает обраще ние к скрипту по имени, является более простым методом. Использование скрипта, как ис полнимого файла дает возможность рассматривать скрипт как команду, аналогично тому, как мы используем системные команды или программы, которые мы написали. Какой shell мы используем? Мы изучали и писали скрипты, которые использовали син таксис sh. Это изначальный (оригинальный) shell в Unix. Его называют также и Bourne Shell - по имени автора, который его разработал. После появления sh были разработаны еще много различных shell. Каждый имеет определенные отличия по отношению к другим ' как по синтаксису, так и по свойствам. То небольшое синтаксическое подмножество, которое мы будем изучать, будет общим для нескольких shell, в том числе для sh, bash и ksh.
Программные свойства sh: переменные, ввод/вывод и оператор if..then Shell-скрипты представляют собой реальные программы. Отметим, какими свойствами обладает скрипт script2. #!/bin/sh # script2: реальная программа с переменными, вводом, # потоком управления BOOK=$HOME/phonebook.data echo find what name in Phonebook read NAME if grep $NAME $BOOK > Дтр/рЬЛтр then echo Entries for $NAME cat /tmp/pb.tmp else echo No entries for $NAME fi rm Дтр/рЬЛтр
Запуск и исполнение script2:
$ ./script2 find what name in Phonebook dave Entries for dave dave 432-6546
$ ./script2 find what name in Phonebook fran No entries for fran
$ cat $HOME/phonebook.data ann 222-3456 bob 323-2222 carla 123-4567 dave 432-6546 eloise 567-9876
9.3. smsh 1-Разбор текста командной строки
321
В скрипте помимо последовательности команд используются: Переменные Shell имеет переменные. В script2 мы установили значения переменных BOOK и NAME. Эти переменные были далее использованы в скрипте. Для извлечения значения, которое хранится в переменной, используется префикс $. Имена переменных не должны со держать прописных букв, хотя я только что сделал наоборот. (Автор слишком категоричен в отношении именования переменных. Большие буквы возможно использовать в именах переменных с учетом некоторых ограничений. - Примеч. пер.). Пользовательский ввод Команда read дает возможность для shell читать строки со стандартного ввода. Вы можете использовать команду read, чтобы сделать скрипты интерактивными, а также чтобы по лучать данные из файлов или из программных каналов. Управление В этом примере показано, каким образом в shell используется управляющая структура if .then..else..fi. В shell используются и другие управляющие структуры, в том числ while, case, for. Среда В данном скрипте используется переменная НОМЕ. Значением этой переменной является путь к вашему домашнему каталогу. Значение переменной НОМЕ устанавливается про граммой login. Это значение доступно для всех потомков вашего входного shell (log-in shell). НОМЕ-это одна из нескольких переменных окружения. В такие переменные поль зователи могут записывать собственные установки (значения), которые действуют на по ведение различных программ. Например, в переменную TZ записывается текущее время данного часового пояса (зоны). Если переменной TZ было присвоено значение EST5EDT, то с помощью этого значения сообщается всем програмам, которые используют ctime (такие программы, как date и ls-l), о необходимости отображать время для восточного вре менного пояса U.S. Мы рассмотрим роль и структуру среды далее в этой главе. Улучшение нашего shell В последней главе мы написали shell, в котором для создания и запуска процессов были использованы системные вызовы: fork, execvp и wait. В этой главе мы сделаем несколько улучшений в нашем shell. Во-первых, добавим средст во для разбора текста командной строки. После введения такого средства пользователь может написать команду и все аргументы для нее в одной строке. Далее добавим в shell управляющую структуру if .then. И наконец, мы добавим локальные и глобальные пере менные.
9.3. smshl-Разбор текста командной строки Первое улучшение нашего shell будет заключаться в добавлении возможности разбора текста командной строки. Эта версия будет называться smsh I.e. Пользователь может теперь набирать текст команды в одной строке: find /home -name core -mtime +3 -print
Программа будет разбивать эту командную строку на массив строк, который может быть передан через вызов execvp. Логика программы smshl.с представлена на рисунке 9.1. Улучшения программы psh2.c заключаются: в разбиении командной строки на аргументы;
322
Программируемый shell. Переменные и среда shell
в игнорировании сигналов SIGINT и SIGQUIT в shell и в восстановлении их диспозиции по умолчанию в дочернем процессе; в обеспечении пользователей возможностью выхода по средством нажатия на Ctrl-D (ключ конца файла).
Функция main для shell будет такой: int main()
{ char *cmdline, ’prompt, **arglist; int result; void setup(); prompt = DFL_PROMPT; setup!); while ((cmdline = next_cmd(prompt, stdin)) != NULL){ if ((arglist = splitline(cmdline)) != NULL){ result = execute(arglist); freelist(arglist);
} free(cmdline);
} return 0;
) Назначение трех функций: next_cmd next_cmd читает следующую команду из входного потока. В ней выполняется вызов malloc, чтобы иметь возможность принимать командные строки произвольной длины. Функция возвращает NULL при достижении конца файла. splitline splitline разбивает текст командной строки на массив слов и возвращает этот массив. Она ис пользует malloc для того, чтобы можно было строить командные строки с произвольным ко личеством аргументов. Массив заканчивается указателем NULL. execute execute использует для запуска команды fork, execvp и wait, execute возвращает код окончания команды.
9.3. smshl -Разбор текста командной строки
323
Программа smshl состоит из трех файлов: smshl.с, splitline.c и executex. Откомпилируем про граммы и проверим работу: $ сс smshl .с splitline.c execute.c -о smshl $ ./smshl >ps -f UID PID PPID С STIME TTY TIME CMD bruce 23203 23199 0 Jul29 pts/4 00:00:00 bash bruce 25383 23203 0 08:23 pts/4 00:00:00 ./smshl bruce 25385 25383 0 08:23 ts/4 00:00:00 ps-f > нажмите здесь Ctri-D *
Заметьте, что ps -f является дочерним процессом ./smshl, который в свою очередь является дочерним процессом bash. Далее представлен код smshl .с: Л* smshl .с small - shell версия 1 ** Первая реально полезная версия после shell, который использовал ** приглашения. Здесь производится разбор текста командной строки. ** Используется fork, ехес, wait; игнорируются сигналы
★* j
#include <stdio.h> #include <stdlib.h> #include #include <signal.h> «include "smsh.h" «define DFL.PROMPT ">" int main()
{ char *cmdline, ‘prompt, **arglist; int result; void setup(); prompt = DFL.PROMPT; setup!); while ((cmdline = next_cmd(prompt, stdin)) != NULL){ if ((arglist = splitline(cmdline)) != NUtl){ result = execute(arglist); freelist(arglist);
} free(cmdline);
} return 0;
} void setup()
Г * назначение: инициализация shell * возврат: отсутствует. При возникновении проблемы вызывается fatal()
*/
324
Программируемый shell. Переме signal(SIGINT, SIG IGN); signal(SIGQUIT, SIG IGN);
} void fatal(char *s1, char *s2, int n)
{ fprintf(stderr,"Error: %s,%s\n", s1, s2); exit(n);
} Далее представлен программный код execute.c: Г execute.c - код, который использует small shell для выполнения команд */ «include <stdio.h> «include <stdlib.h> «include «include <signal.h> «include <sys/wait.h> int execute( char *argv[])
Г * назначение: запуск программы разборки аргументов * возврат: статусная информация, передаваемая с помощью wait или -1 при * ошибке * ошибки: -1 - при ошибках в fork() или wait() */
{ int pid; int childjnfo = -1; if (argv[0] == NULL) /* нет */ return 0; if ((pid = fork()) ==-1) perror("fork"); else if (pid == 0){ signal(SIGINT, SIG DFL); signal(SIGQUIT, SIG.DFL); execvp(argv[0], argv); perror(''cannot execute command"); exit(1);
} else { if (wait( &ch ild_i nfo) == -1) perrorf'wait");
} return child_info;
} Далее представлен код splitline.c: Г splitline.c - функции чтения команды и синтаксического разбора для smsh
*
*
char *next_cmd(char ‘prompt, FILE *fp) - получить следующую команду
I smshl-Разбор текста командной строки char **splitline(char *str); - синтаксический разбор строки 7 «include <stdio.h> «include <stdlib.h> «include <string.h> «include "smsh.h" char * next_cmd(char *prompt, FILE *fp)
Г w назначение: чтение следующей командной строки из fp
* возврат: строка с динамическим распределением памяти, где содержится * командная строка * ошибки: NULL при EOF (в действительности это не ошибка) вызовы fatal из emallocQ ’ замечания: пространство выделяется участками длиной BUFSIZ. 7
{ char *buf; /* буфер */ int bufspace = 0; /* общий размер */ int pos = 0; /* текущая позиция */ int с; Г для ввода символа 7 printf("%s", prompt); /* приглашение для пользователя */ while((c = getc(fp)) != EOF) { /* необходима память? */ if(pos+1 >= bufspace){ /* 1 для \0 7 if (bufspace == 0) /* у: сначала 7 buf = emalloc(BUFSIZ); else Г или расширить 7 buf = erealloc(buf,bufspace+BUFSIZ); bufspace += BUFSIZ; /* модифицировать размер */
} /* конец команды? 7 if (с == '\n') break; Г нет, добавить в буфер */ buf[pos++] = с;
} if (с == EOF && pos == 0) Г EOF и нет ввода 7 return NULL; /* скажем так */ buf[pos] = '\0'; return buf;
} j★★ ★* j
splitline (преобразование командной строки в массив)
«define is_delim(x) ((х) == "|| (х) == '\t') char ** splitline(char *line)
Программируемый shell. Переменные и с/
Г * назначение: представление текста строки в качестве * массива из токенов (* подстрок), разделяемых пробелами * возврат: массив указателей на копии токенов (массив заканчивается NULL) * или NULL, если в строке нет токенов * действие: обозначить массив, локализовать подстроки, сделать копии * замечание: будет работать strtok()
*/ { char *newstr(); char **args; int spots = 0; int bufspace = 0; int argnum = 0; char *cp = line; char *start; int len; if (line — NULL) return NULL; args = emalloc(BUFSIZ); bufspace = BUFSIZ; spots = В U FSIZ/sizeof (char *); while(*cp != '\0')
Г индекс свободного элемента таблицы */ /* байты в таблице */ /* используемые слоты */ Г позиция в строке */
/* отработка специальной ситуации */ /* инициализация массива */
{ while (is_delim(*cp)) /* пропуск лидирующих пробелов*/ cp++; if (*cp == '\0') /* выход по признаку конца строки */ break; /* выделить место для массива (+1 для NULL) */ if(argnum+1 >=spots){ args = erealloc(args,bufspace+BUFSIZ); bufspace += BUFSIZ; spots += (В U FSIZ/sizeof (char *));
} /* отметить точку start, для последующего нахождения конца слова * start = ср; len = 1; while (*++ср != ’\0’ && !(is_delim(*cp))) len++; args[argnum++] = newstr(start, len);
} argsfargnum] = NULL; return args;
} Г * назначение: создание строк
9.3. smshl-Разбор текста командной строки * возврат: строка, никогда не NULL
*/ char *newstr(char *s, int I)
{ char *rv = emalloc(l+1); rv[l] = Л0-; strncpy(rv, s, I); return rv;
} void freelist(char **list)
Г * назначение: освобождение списка, который возвращается от splitline * возврат: ничего * действие: освобождение всех строк в списке и затем освобождение ci
*/ { char **ср = list; while(*cp) free(*cp++); free(list);
} void * emalloc(size t n)
{ void *rv; if ((iv = malloc(n)) == NULL) fatalfout of memory","", 1); return rv;
} void * erealloc(void *p, size t n)
{ void *rv; if ((rv = realloc(p.n)) == NULL) fatal("realloc() failed","", 1); return rv;
} Далее представлен код smsh.h: «define YES 1 «define NO 0 char *next_cmd(); char *’splitline(char *); void freelist(char **); void *emalloc(size_t); void *erealloc(void *, size_t); int execute(char **); void fatal(char *, char *, int);
328
Программируемый shell. Переменные и среда shell
9.3.1. Замечания относительно smsh 1 smshl много проще в использовании, чем psh2. Дополнительные удобства такие:
Несколько команд в одной командной строке Обычный shell предоставляет пользователю возможность в тексте командной строки отделять одну команду от другой знаком “точка с запятой”. В таком случае пользователь может писать несколько команд в одной строке: Is demodir; ps -f; date
Фоновая обработка Обычный shell предоставляет пользователю возможность запускать процесс в фоновом режиме. Это требование отмечается знаком амперсанда (&) в конце текста команды: find /"home -name core -print &
Работа процесса в фоновом режиме означает, что вы его запускаете, а затем опять получаете приглашение от shell. Фоновый процесс продолжает работать, а вы можете использовать shell для запуска других программ. Хотя это довольно мудрено звучит, но при этом реализуется чрезвычайно простой принцип. Изучите блок-схему, чтобы понять, как вам можно получить опять приглашение от shell без ожидания, когда закончится запущенная команда. Идея простая и элегантная. Но вам необходимо спланировать, как вы собираетесь управлять сигналами, и решить, как избежать появления зомби. Все это напоминает авантюрное кино. Команда exit Обычный shell предоставляет пользователю возможность набрать команду exit с тем, что бы выйти из shell. Команда exit допускает использование целочисленного аргумента, как, например, exit 3. В таком случае целочисленное значение аргумента будет передаваться как аргумент в функцию exit.
9.4. Поток управления в SHELL: почему и как? Второе усовершенствование в нашем shell - это добавление управляющей структуры if. then.
9.4.1.
Что Делает if?
В shell поддерживается управляющая структура if. Пусть вы планируете копировать (выпол нять back up) свой диск каждую пятницу (Friday). Для этого рассмотрим такой пример: if date | grep Fri then echo time for backup. Insert tape and press enter readx tar cvf /dev/tape /home fi
Управляющая структура if в shell работает аналогично тому, как работает оператор if в других языках: проверяется некое условие, и если результат такой проверки положительный, то вы полняется некий блок программного кода. В shell под условием понимается команда, а под позитивным результатом - успешное выполнение команды. В примере команда - это date | grep Fri. При ее выполнении ищется подстрока “Fri” в выводе команды date. Команда grep либо успешно закончит поиск указанной подстроки, либо под строка не будет найдена. А как программа может сказать о своем успешном окончании?
9.4. Поток управления в SHELL: почему и как?
329
Выполнение exit(O) при успехе. Программа grep вызывает exit(O) для того, чтобы оповес тить об успешной работе. Все Unix-программы следуют принятому соглашению: код воз врата, равный 0, при выходе из программы будет означать успешное выполнение про граммы. Пусть, например, команда diff сравнивает два текстовых файла. Тогда команда diff возвратит 0, если она не обнаружила отличий в файлах. Это будет означать успешное вы полнение. Программы для управления файлами и каталогами, такие, как mv, ср и rm, будут возвращать 0, если они успешно справились с переименованием, копированием и удале нием файлов соответственно. Алгоритм управляющей структуры if..then в скриптах shell основан на предположении, что нулевой код возврата команды будет означать успех. Использование else в структуре if. В управляющей структуре if может быть использова на альтернативная ветвь else: Is who if diff filel filel.bak then echo no differences found, removing backup rm filel.bak else echo backup differs, making it read-only chmod -w filel.bak fi date В блоке else, как и в блоке then, содержится некоторое число команд, в числе которых могут быть и управляющие структуры if..then.
Управляющая структура if характеризуется еще одним свойством. В этой структуре между ключевыми словами if и then располагается блок команд. Успешность выполнения условия будет определяться кодом возврата последней выполненной команды из этого блока.
9.4.2. Как работает if Управляющая структура if работает так: (a) Shell запускает команду, которая следует за словом if (b) Shell проверяет код возврата выполненной команды. (c) Код возврата, равный 0, означает успех, а ненулевое значение означает неудачу (d) Shell выполняет команды, которые следуют за словом then, если был зафиксирован успех. (e) Shell выполняет команды после слова else, если был зафиксирована неудача. (О С помощью ключевого слова fi отмечается конец блока if
330
Программируемыйshell.Переменныеисредаshell
9.4.3. Добавление if к smsh Мы знаем, что делает управляющая структура if Каким образом можно добавить структуру if к нашему shell? Знаем, как запускать команду. Для этого нужно вызвать execute. Знаем, как проверить код возврата после выполнения exit в программе. Он становится доступен нам по сле выполнения wait. Мы можем сохранять результат, полученный из команды после if, в некоторой переменной. Затем нам необходимо решить откуда читать команды: из блока then или из блока else. Нам также нужно быть уверенными в том, что мы обнаружим then на строке, которая следует за строкой блока if Добавление нового уровня: process Наша исходная модель слишком проста. Блок-схема г smshl содержит путь, идущий непо средственно от splitline к fork. Каждая команда проходит через ехес. В новой версии некоторые строки не будут проходить через ехес: это строки, которые начинаются с if, then или fi, а также блок команд then, когда обнаружено невыполнение условия. С добавлением синтаксиса if командная обработка делается более сложной. Поэтому мы напишем оберточную функцию process, которая и должна спрятать всю эту сложность. Модифицированная версия блок-схемы показана на рисунке 9.2.
Что делает process? Функция process управляет передачами управления в скрипте по мере проверки значений ключевых слов: if, then и fi. Она будет вызывать fork и ехес, только когда будет в этом необ ходимость. process должна записывать код возврата команд, которые работают в блоке ус ловие. По этому коду будет приниматься решение о передаче управления на блок then или на блок else.
9.4. Поток управления в SHELL: почему и как?
Как работает process? Область кода и область состояния развития Функция process рассматривает текст скрипта как текст, состоящий из нескольких облас тей. Одна область - это блок then, другая область - это блок else, а третья область - это часть, которая вся находится вне структуры if Shell будет трактовать команды в раз личных областях по-разному, как это изображено на рисунке 9.3. Рассмотрим область, которая находится вне структуры if Будем называть эту область нейтральной (neutral). Здесь производится чтение, синтаксический разбор и выполнение команд. Область
neutral
Вхождение в shell
)
who if diff filel filel.bak want_then then | rm filel.bak then__block I echo removing backup j else f f chmod -w filel.bak
else_block neutral
j ™date I
РИСУНОК 9.3 СкрИПТ, СОСТОЯЩИЙ ИЗ
различных областей
Следующая область - это область, которая находится между строкой if и then. В этой об ласти shell выполняет команду, от которой shell будет записывать ее код возврата. Другая область находится между then 1 и else или fi. Последняя область находится между else и fi. После fi опять продолжается нейтральная область. Shell должен отслеживать, в какой текущей области он находится. Он должен также отсле живать, каков результат выполнения команды в блоке условие, когда он переходит в об ласть wantthen. В различных областях необходимо вести различную обработку. В програм ме вводится понятие состояния, которое зависит от места проведения обработки. Функция process вызывает три функции, которые управляют переменными состояния. is_controLcommand is_control_command - логическая функция, с помощью которой process узнает, является ли ко
манда некоторым языковым элементом, или это команда, которую нужно выполнить. do^controLcommand do_control_command обрабатывает ключевые слова if then и fi. Каждое слово - это граница
между двумя состояниями. Эта функция модифицирует переменную состояния и выпол няет необходимые для нее действия. ok_to_execute ok_to_execute проверяет текущее состояние и результат условной команды. Возвращает
булево значение, которое идентифицирует успешность выполнения текущей команды.
9.4.4. smsh2.c: Модифицированный код smsh2.c построена на основе программы smshl.с. В функции было сделано только одно из
менение - вызов execute был заменен на вызов process: /** smsh2.c - small-shell версия 2 ** Этот shell поддерживает синтаксически разбор командной строки ** и логику if..then..else.fi (с помощью processO)
332
Программируемый shell. Переменные и среда shi
<stdio.h> «include <stdlib.h> «include «include <signal.h> «include <sys/wait.h> «include "smsh.h" «include «define DFL_PROMPT ">" int main()
{ char *cmdline, *prompt, **arglist; int result, process(char **); void setup)); prompt = DFL_PROMPT; setup(); while ((cmdline = next_cmd(prompt, stdin)) != NULL){ if ((arglist = splitline(cmdline)) != NULL){ result = process(arglist); freelist(arglist); free( cmdline); return 0;
} void setup()
Г * назначение: инициализация shell " возврат: ничего. Вызов fatal() в случае возникновения проблем
7 { signal(SIGINT, SIG.IGN); signal(SIGQUIT, SIGJGN);
} void fatal(char *s1, char *s2, int n)
{ fprintf(stderr,"Error: %s,%s\n", s1, s2); exit(n);
} Изменения в двух новых файлах process.c и controlflow.c: Г process.c * уровень командной обработки * Функция process(char **arglist) вызывается в основном цикле * Она установлена перед функцией execute)). Этот слой управляет двумя видами * обработки: * а) встроенными функциями (например, exit(), set, =, read,..) b) управляющими структурами (например, if, while, for)
1. Поток управления в SHELL: почему и как?
31
7 «include <stdio.h> «include "smsh.h" int is_control_command(char *); int do_control_command(char **); int ok 1o_execute(); int process(char **args)
Г * * * * *
назначение: обработка пользовательской команды возврат: результат выполненной команды особенности: если встроенная команда, то вызов соответствующей функции,если это не execute)) ошибки: возникают при выполнении подпрограмм, которые здесь используются
7 { int rv = 0; if (args[0] == NULL)
rv = 0; else if (is_control_command(args[0])) rv = do_control_command(args); else if (ok_to_execute()) rv = execute(args); return rv;
} /* controlflow.c
ж
* "if" обработка. Производится с двумя переменными состояния: * if_state и if result
*/" «include <stdio.h> «include "smsh.h" enum states {NEUTRAL, WANTJHEN, THEN.BLOCK}; enum results {SUCCESS, FAIL}; static int if_state = NEUTRAL; Static int if_result = SUCCESS; static int last_stat = 0; int syn_err(char *); int ok_to_execute()
Г * назначение: определение команды, которую должен выполнить shell * возврат: 1 для yes, 0 для по * особенности: если зафиксирован УСПЕХ в THEN.BLOCK и в if_result, тогда yes * если зафиксирована НЕУДАЧА в THEN.BLOCK и в ifjesult, тогда по * если зафиксирована НЕУДАЧА в WANT THEN, тогда это синтаксическая ошибка * (не тот sh)
7
4
Программируемый shell. Переменные и int rv = 1; /* по умолчанию */ if (if_state == WANT_THEN){ syn_err("then expected"); rv = 0;
} else if (instate == THEN.BLOCK && ifjesult == SUCCESS) rv= 1; else if (instate == THEN.BLOCK && ifjesult == FAIL)
rv = 0; return rv;
} int is_control_command(char *s)
Г * назначение: ответить на вопрос - является ли команда управляющей для * shell? * возврат: 0 или 1
7 { return (strcmp(s,"if’)==01| strcmp(srthen")==01| strcmp(s,"fi")==0);
} int do_control_command(char **args)
Г * назначение: обработка "if, "then", "fi" - изменение состояния или * обнаружение ошибки * возврат: 0, если ok, -1 - при обнаружении синтаксической ошибки
7 { char *cmd = args[0]; int rv = -1; if(strcmp(cmd,"if")==0){ if (if.state != NEUTRAL) rv = syn.errfif unexpected"); else{ last_stat = process(args+1); ifjesult = (last.stat == 0? SUCCESS: FAIL); instate = WANT .THEN; rv = 0;
} } else if (strcmp(cmd,"then”)==0){ if (if.state !=WANT.THEN) rv = syn.errfthen unexpected"); else { if.state = THEN.BLOCK; rv = 0;
9.4. Поток управления в SHELL: почему и как?
335
} else if (strcmp(cmd,"fi")==0){ if (if_state != THEN_BLOCK) tv = syn_err("fi unexpected”); else { if_state = NEUTRAL; rv = 0;
} } else fatalfinternal error processing:”, cmd, 2); return rv;
} int syn_err(char *msg) Г назначение: управление синтаксическими ошибками в управляющих структурах * особенности: переустановить состояние на НЕЙТРАЛЬНОЕ * возврат: -1 - в интерактивном режиме. В скриптах должен быть вызов fatal
V { instate = NEUTRAL; fprintf (stderr. "syntax error: %s\n", msg); return -1;
') Код в controlflow.c не выполняет обработку части else в составе управляющей структуры if. Выполнение такой обработки остается в качестве упражнения. Откомпилируем и протес тируем эту версию: $ сс -о smsh2 smsh2.c splitline.c execute.c process.c controlflow.c $ ./smsh2 > grep Ip /etc/passwd lp:x:4:7:lp:/var/spool/lpd:
> if grep Ip /etc/passwd lp:x:4:7:lp:/var/spool/lpd:
>then > echo ok ok
> fi > if grep pati /etc/passwd > then > echo ok > fi > echo ok ok
> then syntax error: then unexpected
336
Программируемый shell. Переменные и среда shell
Что мы еще должны сделать? Все выглядит хорошо. Насколько полученные результаты сравнимы с результатами рабо ты обычного shell? $ if grep Ip /etc/passwd > then > echo ok
> fi lp:x:4:7:lp:/var/spool/lpd: ok
$ Этот shell управляет обработкой структуры if не так, как это делается в нашем shell. Стан дартный shell задерживает выполнение всей структуры, пока не будет обнаружено ключе вое слово fi. Как это работает? Почему это так делается? В обычном shell также поддержи вается обработка вложенных структур if Можно ли изменить нашу программу так, чтобы она обрабатывала вложенные структуры //?
9.5. SHELL-переменные: локальные и глобальные В Unix shell, как в любом языке программирования, используются переменные. Вы имеете возможность устанавливать значения переменных, извлекать значения переменных и про сматривать списки переменных, как это представлено в следующем коде: $ аде=7 $ echo $аде 71 $ echo аде аде $ echo $аде+$аде 7+7 $ read name fido $ echo hello, $name, how are you hello, fido, how are you $ Is > $name.$age $ food = muffins food: not found
# присвоение значения # извлечение значения # требуется использовать символ $ # выполнение строковых операций # ввод значения из stdin # будет интерпретировано как: # используется как часть команды ft в операторе присваивания нет пробелов
$ В shell используют два типа переменных: локальные переменные и переменные среды (переменные окружения). (Более точно нужно говорить не о типе переменных, а о некой их разновидности. Тип переменных задается допустимыми значениями и допустимыми действиями. Если говорить о разновидностях переменных, то есть еще одна разновид ность - специальные переменные. Это переменные, для имен которых используют один определенный метасимвол или целое число. - Примеч. пер.) Мы уже упоминали ранее в этой главе, что такие переменные, как НОМЕ и TZ, позволяют пользователям передавать собственные установки для программ. Такие переменные среды ведут себя отчасти так, как глобальные переменные. Их значения доступны для всех дочерних процессов в shell. Далее в этой главе мы изучим особенности построения и использования среды. А теперь нам нужно лишь запомнить, что есть два сорта переменных.
337
9.5. SHELL-переменные: локальные и глобальные
9.5.1. Использование переменных shell Предшествующий пример проиллюстрировал нам большинство операций над перемен ными. Возможные операции над переменными: Операция
Синтаксис
Замечания
Присваивание
var=value
Пробелы отсутствуют
Ссылка
$var
Удаление
unset var
Получение значения из input
read var
Получение списка переменных
set
Сделать переменную глобальной
export var
Также: read varl var2..
Имена переменных формируются как комбинация из символов: A-Z, a-z, 0-9 и _. Первый символ не может быть цифрой. (Это не так. См. предшествующее замечание о специаль ной разновидности переменных. -Примеч. пер.). При наборе символов в именах перемен ных регистр (верхний или нижний) является значимым. Значениями переменных являются строки. Значения не являются целочисленными. Все дей ствия над переменными - строчные. (В Korn shell, POS1X shell и других shell допускается ис пользование и целочисленных типов переменных. Их можно объявить с помощью typeset и далее выполнять над ними целочисленную обработку. - Примеч. ред.) Получение листинга переменных. Список переменных, которые в текущий момент опре делены в shell, можно получить так (В этот список не выводятся значения специальных переменных, которые составляют третью разновидность. - Примеч. пер.): $ set BASH=/bin /bash BASH_VERSI0N=1.14.7(1) DISPLAY=:0.0 EUID=500 HOME=/home2/bruce HOSTTYPE=i386
1FS= LANG=en LANGUAGE=en LD_LIBRARY_PATH=/usr/lib:/usr/local/lib LOGNAME=bruce 0PTERR=1 0PTIND=1 OSTYPE=Linux PATH=/bin:/usr/bin:/usr/X11 R6/bin:/usr/local/bin:/home2/bruce/bin PPID=30928 PS4=+ PWD=/home2/bruce/projs/ubook/src/ch09 SHELL=/bin/bash
338
Программируемый shell. Переменные и среда shell SH LVL=2 TERM=xterm-color UID=500 USER=bruce _=/bin/vi age=7 name=fido
В этот список включены переменные окружения, которые были установлены на момент моего входа в систему, плюс две локальные переменные, которые были добавлены мною в последующей работе.
9.5.2. Система памяти для переменных Для добавления переменных к нашему shell нам необходимо место для хранения имен и значений этих переменных. Эта система памяти должна отличать локальные перемен ные от глобальных. Вот какой будет выглядеть абстрактная модель системы памяти:
Модель Переменная
Значение
Глобальная?
data
“phonebook.dat"
Нет
НОМЕ
“Дтоте2Дйо"
Да
TERM
“t1061"
Да
Интерфейс (неполный) VLstorefchar *var, char *val) добавление/модификация var-va! VLJookup(char *var) извлечение значения из va'r VUist выдать список в виде таблицы на stdout
Реализация Мы будем реализовывать эту таблицу, где shell хранит все свои переменные, в формате ли бо связанного списка, либо хеш-таблицы, либо дерева. В первом приближении мы ис пользуем массив структур. Каждая переменная - это: struct var { char *str; int global;
/* строка: name=val */ /* логическая переменная */
}; static struct vartab[MAXVARS];
Структура представлена на рисунке 9.4. vartab
1
------------ Н TERM=vtlOO 1
Рисунок 9.4 >[ cityaBoston J Система памяти для переменных shell
9.5. SHELL-переменные: локальные и глобальные
339
9.5.3. Команды для добавления переменных: встроенные команды Мы умеем резервировать память для хранения переменных. Теперь нам требуется доба вить для нашего shell команды: по присвоению значений переменным, по получению спи ска переменных, по извлечению значений переменных. Другими словами, пользователи нашего shell должны иметь возможность исполнять такие команды: > TERM=xterm > set > echo $TERM set-это команда нашего shell, а не программа, которую должен запустить shell. Это анало
гично той ситуации, когда if и then рассматривают как ключевые слова, которые shell обра батывает сам. Чтобы различать set от команд, которые shell запускает с помощью ехес, мы будем называть set встроенной командой. Команду, представленную в форме vamame=value, shell обрабатывает так: добавляет запись в свою таблицу переменных. Операторы присваивания также относятся к встроенным командам. Введение в наш shell встроенных команд требует дополнительного изменения блок-схемы. До того как будут вызваны fork и ехес, требуется определить, не является ли команда встроенной (см. рисунок 9.5).
Модифицируем функцию process, с тем чтобы проверять команды на принадлежность к встроенным командам до вызова fork/exec: if (args[0] == NULL)
rv = 0; else if (is_controljttmmand(args[0])) rv = do_controLcommand(args); else if (ok_to_execute()){ if(!builtin_command(args, &rv)) rv = execute(args);
340
Программируемый shell. Переменные и среда shell
Новая функция builtin_command объединяет в своем составе вызов операций по проверке на встроенность и исполнение встроенных команд. После выполнения buiffin_command возвра щает логическое значение и модифицирует статус по ссылке. Новый код программы builtin.C: /* builtin.C * содержит переключатель и функции для встроенных команд
7 «include <stdio.h> «include <string.h> «include «include "smsh.h" «include "varlib.h” int assign(char *); int. okname(char *); int builtin command(char **args, int *resultp)
Г * * * *
назначение: запуск на исполнение встроенной команды возврат: 1, если в args[0] встроенная команда, 0, если нет особенности: проверка значения args[0] на принадлежность ко всем встроенным командам. Вызов функции.
7 { int rv = 0; if (strcmp(args[0],nset") == 0){ /* Это команда 'set'? */ VUist(); *resultp = 0; rv= 1;
} else if (strchr(args[0], -') != NULL){ /* оператор присваивания 7 *resultp = assign(args[0]); if (*resultp != -1) Г x-y= 123 - так нельзя! */ (v = 1;
}
else if (strcmp(args[0], "export") == 0){ if (args[1] != NULL && okname(args[1])) *resultp = VLexport(args[ 1 ]); else *resultp = 1; rv= 1;
} return rv; int assign(char *str)
Г * назначение: выполнить name=val и гарантировать допустимость имени * возврат: -1 для недопустимого Ival, или результат VLstore
9.5. SHELL-переменные: локальные и глобальные * предостережение: строка модифицируется, но потом восстанавливается ее * исходное значение
{ char *ср; int rv; ср = strchr(str,-'); *ср = ЛО’; rv = (okname(str)? VLstore(str,cp+i): -1); ср = return rv;
} int okname(char *str)
Г * назначение: оценка допустимости имени переменной в строке * возврат: 0, если нет, 1, если да
7 { char *ср; for(cp = str; *ср; ср++){ if ((isdigit(*cp) &&cp==str) |j !(isainum(*cp) || *cp=='_')) return 0;
} return (cp != str); /* нет пустых строк */
}
9.5.4. Как все работает? Откомпилируем и запустим на исполнение нашу модернизированную программу: $ сс -о smsh3 smsh2.c splitline.c execute.c process2.c \ controlflow.c builtin.с varlib.c $ ./smsh3 > set > day=monday > temp=75 > TZ=CST6CDT > x.y=z
cannot execute command: No such file or directory
> set day=monday temp=75 TZ=CST6CDT
> date Tue Jul 31 11:56:59 EDT 2001
> echo $temp, $day $temp, $day
342
Программируемый shell. Переменные и среда shell
Работа проходит нормально. Теперь наш shell поддерживает переменные. Мы можем присваивать значения переменным, можем получать список переменных. Программа не принимает на обработку недопустимые имена переменных, рассматривая при этом выра жения с именами переменных как имена программ, которые необходимо выполнить. Значение переменной TZ не передается в команду date. В нашем примере запуска про граммы обнаруживаются два момента, которые предполагают последующую доработку. Во-первых, в переменной TZ установлен код центрального временного пояса U.S., а команда date выводит дату, которая соответствует восточному временному поясу U.S. Мы ранее уже установили, что переменная TZ является частью среды и эта переменная передается от процесса-отца к дочернему процессу. Как это работает? Как shell может по местить переменные в среду, с тем чтобы дочерние процессы могли бы получать значения этих переменных? Наша следующая тема для рассмотрения будет посвящена среде. Не была произведена выборка значений в операциях подстановки переменных Stemp, Sday. При запуске нашего теста также было обнаружено, что наш shell не выбирает значения пере менных. Это означает, что когда наш shell производит обработку команды echo $temp, $day, то он не делает подстановку - вместо имени переменной не подставляется ее значение. Данные переменные являются локальными в shell. Команда echo не знает значений этих переменных. До запуска внешних программ shell должен производить подстановку переменных. Этот во прос будет изучен в конце данной главы.
9.6. Среда: персонализированные установки Пользователи любят персонализировать свой компьютер. Некоторым нравится выводить на свои экраны живописные изображения, а другие предпочитают использовать огра ниченную палитру цветов. Некоторые пользователи предпочитают проводить редактиро вание с помощью emacs, а другие предпочитают редактор vi. В Unix пользователям предос тавлена возможность указывать на свои предпочтения с помощью набора переменных, ко торый называют средой (окружением). Каждый пользователь имеет собственный уни кальный домашний каталог, пользовательское имя, файл для размещения в нем входящей почты, тип терминала и наиболее предпочтительный редактор. С помощью переменных среды можно описать многие настраиваемые установки. Поведение многих программ будет зависеть от таких установок. Например, при запуске скрипта script3 будет видно, что команда date использует значение, которое хранится в переменной TZ: #!/bin/sh # script3 - показывает, как переменная среды передается команде # TZ - это временная зона, значение влияет на дату и на результат работы # команды Is -I
# echo 'The time in Boston isM TZ=EST5EDT export TZ date echo "The time in Chicago is” TZ=CST6CDT date echo "The time in LA is" TZ=PST8PDT
date
# добавить TZ к окружению # date использует значение переменной TZ
9.6. Среда: персонализированные установки
343
Среда - это не часть shell. Но в shell есть команды, с помощью которых можно читать и изме нять среду shelh Как обычно, сначала мы посмотрим, что делает среда. Затем рассмотрим, как она работает. И наконец, добавим в наш код возможность работать со средой.
9.6.1. Использование среды Получение листинга среды С помощью команды env можно получить список всех установок в вашей среде: $ env LOGNAME=bruce LD_LIBRARY_PATH^/usr/lib:/usr/local/lib TERM=xterm-color HOSTTYPE=i386 PATH=/bin:/usr/bin:/usr/X11 R6/bin:/usr/local/bin:/home2/bruce/bin HOME=/home2/bruce SHELL=/bin/bash USER=bruce LANGUAGE=en DlSPLAY=:0.0 LANG=en =/usr/bin/env SHLVL=2
env - это обычная программа, а не встроенная команда shell. В списке переменных, который был представлен, содержатся переменные, которые полезны для использования во многих программах. Например, переменная LANG используется программами, которые отображают информацию или выводят сообщения. В Web-броузере переменная LANG . может быть использована для кодировки вывода текста о назначении кнопок или для кодировки текстов пунктов меню. С помощью переменной DISPLAY вы сообщаете систе ме X Windows, где вы хотели бы открыть окна. С помощью значения переменной TERM передается информация для curses об использовании конкретного кода для управления экраном.
Модификация среды var=value
Изменение установок переменных в вашей среде выполняется посредством присвоения новой строки в качестве значения для переменной среды. Например, если ваш Web-броузер поддерживает выдачу сообщений на французском языке, а также выводит тексты ме ню на этом языке, то вы должны будете обратиться к нему с установленным значением LANG=fr. export var
При использовании встроенной команды export в shell производится добавление новой переменной в среду. Если переменная var существовала, как локальная переменная, то те перь эта переменная будет переведена в состав переменных среды. Если переменная var не существовала, то shell создает ее. В bash допускается объединение действий по присвое нию значения и экспортированию: export var=value
344
Программируемыйshell.Переменныеисредаshell
Чтение среды в программах С В стандартной библиотеке С есть функция getenv: «include <stdlib.h> main()
{ char *cp = getenv("LANG"); if (cp != NULL && strcmpfcp, "fr”) == 0) printffBonjouryV’); else printf(HHello\nM);
} 9.6.2. Что собой представляет среда? Как она работает? Среда - это просто массив строк, который доступен для каждой программы (см. рисунок 9.6). Каждая строка в массиве представлена в такой форме: var=value. Адрес массива хранится в глобальной переменной environ. Вот и все, что нужно для работы со средой. Среда-это про сто массив строк, на который “смотрит” указатель из переменной environ. Для прочтения среды необходимо прочитать этот массив строк. Для изменения среды следует изменить строки, либо изменить указатели в массиве, либо установить глобальную переменную так, чтобы она указывала на другой массив.
Примеры программ
showenv.c работает аналогично команде env: /* showenv.c - показывает, как читать и выводить содержимое среды
7
extern char **environ; /* указатель на массив строк */ main()
{ inti; for(i = 0; environ[i]; i++) printf("%s\n’', environ[i]);
} changeenv.c изменяет среду, а затем запускает на исполнение команду env: /* changeenv.c - показывает, как изменять среду * замечание: "env” вызывается для отображения новых установок
9.6. Среда: персонализированные установки
345
*/
«include <stdio.h> extern char ** environ; main()
{
char *table[3]; table[0] = "TERM=vt100"; table[1] = "НОМ E=/on Ahe/range"; table[2] = 0; environ = table; execlpf'env", "env", NULL);
/* установка значений массива */
/* указатель на массив */ /* выполнить программу */
} Далее приведена демонстрация работы: $ ,/changeenv TERM=vt100 НОМ Е=/on/the/rang е
$ Проанализируйте внимательно эту программу. Мы создали массив строк в одной програм ме changeenv, а затем вызвали execlp для запуска другой программы env. Эта новая программа может читать массив строк. Почему-то оказалось, что этот массив был скопирован из про странства данных первой программы в пространство данных второй программы. Но ехес уничтожает все данные! Когда мы обсуждали системный вызов ехес, то отметили, что он работает как трансплантат мозга. Происходит замещение кода и данных вызывающей программы на коды и данные но вой программы. Массив, на который указывает переменная environ, представляет собой един ственное исключение из этого правила. Когда ядро выполняет системный вызов execve, то оно копирует массив и строки в область данных новой программы (см. рисунок 9.7).
346
Программируемый shell. Переменные и среда shell
Давайте отследим, как происходит передача массива от родительского процесса в новую программу. Вызов fork копирует весь родительский процесс, кодовую часть и данные, включая и среду. При выполнении ехес удаляется кодовая часть и данные из процесса и вместо них в процесс помещаются код и данные новой программы. Из старой програм мы копируются лишь аргументы, передаваемые в execvp, и строки, которые хранятся в со ставе среды.
Дочерний процесс не может изменить среду родительского процесса Установки среды дочернего процесса - это копии строк родительского процесса. Дочерний процесс не может модифицировать среду родительского процесса. Передача значений в среду является простой и удобной, поскольку весь массив автоматически копируется, когда процесс вызывает fork и ехес.
9.6.3. Добавления средств по управлению средой в smsh Изменим наш shell теперь так, чтобы обеспечить доступ к среде. Во-первых, наш shell будет включать переменные среды в свой список переменных. Во-вторых, пользователи нашего shell будут в состоянии модифицировать значения переменных среды, а также добавлять в среду новые переменные.
Доступ к переменным среды Мы знаем структуру среды и можем использовать набор функций для добавления пере менных в состав списка переменных. Когда стартует наш shell, то будет произведено копирование значений среды в наш массив среды (см. рисунок 9.8). После проведения копирования мы можем использовать команду set и оператор присваивания для просмотра и модификации имеющихся установок в среде.
9.6. Среда: персонализированные установки
347
Изменения в среде При проверке работы smsh36b^o видно, что при изменении значения TZ sto никак не отра жается на команде date. Мы знаем, как изменять значения переменных среды. Простейший вариант изменения среды - создать совершенно новый массив, в котором будут находить ся установки из нашей предыдущей среды, и установить указатель на этот новый массив в переменной environ (см. рисунок 9.9). После того как вызывается ехес, ядро копирует эти установки для новой программы. Заметим, что в начальном массиве среды, на который те перь нет ссылки, все еще остаются исходные значения.
Изменения в smsh Добавим теперь два шага в программный поток, как это показано на рисунке 9.10. Эти ша ги будут реализованы с помощью добавления двух строк кода: Установка в smsh4.c void setup{)
Г * назначение: инициализация shell * возврат: ничего. Вызов fatal() при возникновении проблемы
7 .
{ extern char **environ; VLenviron2table(environ); signal(SIGINT, SIGJGN); signal(SIGQUIT, SIG IGN);
} Исполнить в execute2.c
Программируемый shell. Переменные и среда shell
348
if ((pid = fork()) ==-1) perrorffork"); else if (pid == 0){ environ = VLtable2environ(); /* new line */ sig'nal(SIGINT, SIG.DFL); signal(SIGQUIT, SIG_DFL); execvp(argv[0], argv); perror("cannot execute command”); exit(1);
Проверка работоспособности выполненных изменений $ make smsh4 сс -о smsh4 smsh4.c splitline.c execute2.c process2.c controlflow.c \ buiitin.c varlib.c
$ ./smsh4 > date Tue Jul 31 09:51:03 EDT 2001
> TZ=PST8PDT > export TZ > date Tue Jul 31 06:51:30 PDT2001
> Пользователь может модифицировать значения переменных среды и добавлять новые переменные среды к массиву переменных среды. Shell обеспечивает доступность этих но вых значений для любой программы, которую он запускает.
9.6. Среда: персонализированные установки
9.6.4. Код vartib.c Г varlib.c * система для хранения пары name=value * с возможностью маркировки элементов как относящиеся к среде * интерфейс: VLstore(name, value) возвращает 1 при успехе, 0 в неудаче * VUookup(name) возвращает строку или NULL, если ничего нет * VUist() выводит текущий массив среды
*
* функции для работы со средой * VLexport(name) добавляет имя в список переменных среды * VLtable2environ() копирование из массива в environ * VLenviron2table() копирование из environ в массив
*
* особенности: * массив представляет собой массив структур, где * содержится флаг глобальной переменной и строка * в форме name=value. Тем самым разрешается добавление EZ к среде. При этом гарантируется простой поиск, поскольку * будет происходить поиск "пате="
7 «include <stdio.h> «include <stdlib.h> «include "varlib.h" «include <string.h> «define MAXVARS 200 struct var { char *str; int global;
/* связанный список был бы более /* строка name=val */ /* boolean */
}; static struct var tab[MAXVARS]; /* таблица (или массив) */ static char *new_string(char *, char *); /* приватные методы 7 static struct var *fmdjtem(char *, int); int VLstore(char *name, char *val)
Г * проход no списку; если найден, то заменить; иначе добавить в конец, * так как ничего не удаляется: пробел означает свободную позицию * при возникновении проблем возвращается 1. При успешной работе - 0.
7 { struct var *itemp; char *s; int rv = 1; Г найти место для размещения и образовать новую строку */ if ((itemp=find_item(name,1 ))!=NULL&& (s=new string(name,val))!=NULL)
О
Программируемый shell. Переменные
{ if (itemp- >str) /* есть значение? */ free(itemp- >str); f* у: удалить его */ itemp->str = s; rv = 0; /* все хорошо! */
} return rv;
} char * new stringfchar *name, char *val)
Г * возвращается новая строка в форме name=value или NULL при ошибке
7 { char *retval; retval = malloc(strlen(name) + strlen(val) + 2); if (retval != NULL) sprintf(retval, "%s=%s”, name, val); return retval;
} char * VUookup(char *name)
Г * возврат значения переменной или пустая строка, если нет значения
7 { struct var *itemp; if ((itemp = find_item(name,0)) != NULL) return itemp->str + 1 + strlen(name); return
} int VLexport(char *name)
Г * Пометка переменной для экспортирования; добавление переменной, если ее нет * Возвращается 0 при нормальном выполнении, 1 - неуспех
7 { struct var *itemp; int rv = 1; if ((itemp = find_item(name,0)) != NULL){ itemp->global = 1; rv = 0;
} else if (VLstore(name, ”") == 1) rv = VLexport(name); return rv;
} static struct var * find_item(char ‘name, int first_blank)
>. Среда: персонализированные установки
351
Г * Поиск элемента в таблице * Возвращается указатель ptr на структуру или NULL, если элемент не найден * ИЛИ, если (first_blank), тогда возвращается указатель на первую пустую * позицию
7
.
{ int i; int len = strlen(name); char *s; for(i = 0; KMAXVARS && tab[i].str != NULL; i++)
{ s = tab[i].str; if (strncmp(s, name, len) == 0 && s[len] == '='){ return &tab[i];
} } if (i < MAXVARS && first_blank) return &tab[i]; return NULL;
} voidVUist()
Г * Выполняется команда set * Выводится список содержимого переменной table, в котором каждая * экспортируемая переменная будет помечена символом '*'
7 { int i; for(i = 0; i<MAXVARS && tab[i].str != NULL; i++) if (tab[i] .global) printff * %s\n", tab[i].str); else printff' %s\n", tab[i].str);
} } int VLenviron2table(char *env[])
Г * Инициализировать переменную table посредством загрузки массива строк * Возврат: 1, когда ok; 0, когда not ok (Здесь обычная функция С,, которая может возвращать то, что ей удобно. В данном случае возвращается ИСТИНА,с точки зрения С, а не UNIX, если успех, и ЛОЖЬ, если неуспех. - Примеч. ред.)
7 { int
i;
Программируемый shell. Переменные и ct char *newstring; for(i = 0; env[i] != NULL; i++) if (i == MAXVARS) return 0; newstring = malloc(1+strlen(env[i])); if (newstring == NULL) return 0; strcpy(newstring, env[i]); tab[i].str = newstring; tab[ij. global = 1;
} while(i < MAXVARS){ /* Я знаю, что нам не надо это делать */ tab[ij.str = NULL; /* статические глобальные переменные имеют */ tab[i++] .global = 0; f* нулевое значение по умолчанию */
} return 1;
} char ** VLtable2environ()
Г * Создание массива указателей, предназначенного для построения новой среды * Заметьте, что вам необходимо выполнить free(),чтобы избежать потери памяти
7 { int j, char
i, n = 0; **envtab;
I* индекс */ Г другой индекс 7 j* счетчик 7 Г массив указателей 7
Г * сначала нужно определить число глобальных переменных
7 for(i = 0; i<MAXVARS &&tab[i].str != NULL; i++) if (tab[i]. global ==1) n++; /* затем выделить память для этих переменных */ envtab = (char **) malloc((n+1) * sizeoffchar *)); if (envtab == NULL) return NULL; /* далее загрузить массив указателей 7 for(i = 0, j = 0; KMAXVARS &&tab[i].str != NULL; i++) if (tab[i] .global == 1) envtab[j++] = tab[i].str; envtabO] = NULL; return envtab;
9.7. Общие замечания о SHELL
353
9.7. Общие замечания о SHELL В этой главе мы изучили shell с позиций языка программирования. Мы добавили в shell три существенных свойства: разбор командной строки, средства для отработки логики if..then\\ переменные. Наш маленький shell быстро вырос по размеру. Его основные свойст ва сведены в таблицу.
Свойство
Поддержка
Необходимо дополнительно
Команды
Запуск программ
Переменные
=, set
read, подстановка $var
if
if.then
else
Среда
Вся
exit
exit
cd
cd Нет
Все перечисленное
Подстановка переменных. Для добавления возможности вести подстановку значений переменных потребуется дополнительное изучение. Где в блок-схеме shell должен делать подстановку переменной - вместо $Х подставлять значение переменной X? Проанали зируйте вот такую последовательность действий:
$ read х who am i
$ $x mori.xyz.com!nobody ttyl Dec 31 13:56
$ grep $x /etc/passwd grep: am: No such file or directory grep: i: No such file or directory
$ Какой вывод можно сделать по приведенным выше результатам относительно связи меж ду шагом разбора командной строки в shell и той частью в shell, где производится подста новка переменных? Каковы преимущества такого решения? Как это можно добавить в на шу программу? Перенаправление ввода/вывода. Shell предоставляет пользователям возможность перена правлять ввод и вывод процесса в файлы или другие процессы. Каким образом работает этот механизм? Можем ли мы ввести его в наш shell? Вопросы перенаправления ввода/вы вода будут изучены в следующей главе.
Заключение Основные идеи •
•
Unix shell запускает программы, которые называют скриптами shell. Скрипт shell может запускать программы, воспринимать ввод от пользователя, использовать переменные и исполняться по сложной логике. Логика управляющей структуры if..then в shell построена на соглашении, что программа при успешном окончании возвращает значение 0. Для получения кода возврата из программы shell использует wait.
354
•
•
Программируемый shell. Переменные и среда shell
В язык программирования shell включен механизм переменных. Значениями этих переменных являются строки, которые могут быть использованы произвольной командой. Переменные shell локальны в скрипте. Каждая программа наследует список строк из родительского процесса, из которого данная программа вызывается. Список строк называют средой. Среда используется для поддержания глобальных установок в сессии и для установки параметров для определенных программ. Shell предоставляет возможность просмотра и модификации среды.
Что дальше? Мы будем изучать перенаправление ввода/вывода.
Исследования 9.1 Опасность при работе со встроенными командами. Напишите С-программу (или shell-скрипт) и назовите ее set. Далее попытайтесь выполнить программу из shell. Что произошло? Напишите С-программу (или shell-скрипт) и назовите ее no=dice. Далее по пытайтесь выполнить программу. Что произошло? Попытайтесь выполнить програм му test. Как можно добиться запуска перечисленных программ? 9.2 Вложенные структуры if Проект, использующий process.c и controlflow.c, можно рас ширить, чтобы имелась возможность использовать вложенные if. Можно ли тогда бу дет задавать управляющие структуры в формате: if cmd 1
then if cmd2
then cmd3
else cmd4
fi else cmd5 fi
с произвольной степенью вложенности? Понадобится ли при этом большее число переменных состояния? Если вы думаете строить решение на основе стека, то оцените возможность решить ту же проблему на основе использования рекурсии. 9.3 vaiiib.C при модификации среды создает полностью новый массив среды. Почему нельзя использовать realloc для установки размера оригинала?
Программные упражнения 9.4 Множественность команд в командной строке. Модифицируйте smshl .с, чтобы иметь
возможность задавать несколько команд в одной строке. Самый простой вариант вы полнить это - нужно модифицировать next_cmd. Сократите частоту выдачи приглаше ний. 9.5 exit3. Модифицируйте smshl.с, чтобы была возможность выполнять команду exit, для ко торой задается аргумент. Рассмотрите возможность отказа выполнения этой команды, если в качестве аргумента будет задаваться нечисловое значение (например, exit left). Куда следует разместить обработку этой команды в блок-схеме обработки команд? Бу дет ли нам необходимо добавлять новый шаг в алгоритм обработки?
Заключение
355
9.6 else. Модифицируйте программу process.c для поддержки части else в управляющей структуре if. 9.7 Функция okto^execute использует две переменные для идентификации текущей области и текущего состояния. Вы можете заменить две переменные одной, которая может при нимать несколько значений. Рассмотрите такой набор состояний: NEUTRAL, IF_SUCCEEDED, IF_FAILED, SKIPPING_THEN, DOING_THEN, SKIPPING_ELSE, DOING_ELSE
Модифицируйте controlflow.c, чтобы можно было использовать такую систему. 9.8 Фоновые процессы. Модифицируйте smshl.с, чтобы при наличии в конце команды сим вола & эта команда запускалась на исполнение в фоновом режиме. Вам потребуется сделать некоторые изменения в next_cmd. 9.9 Обычный shell задерживает исполнение управляющей структуры, пока не будет прочи тано оконечное ключевое слово fi. (Заметим, что fi - это не сокращения слова final. Это слово if, написанное наоборот.) Полностью другое решение заключается в том, что чи таются все строки в структуре if в структуру данных, состоящую из трех частей. Первая часть содержит команду проверки условия. Следующая часть - список команд для области then. Последняя часть - список команд для области else. После прочтения строк из структуры вы можете далее выполнять команду проверки условия, а потом на основе полученного предыдущего результата исполнять список команд then или список команд else. Напишите версию smsh, которая обрабатывает структуру if, используя рассмотренный метод. В вашем решении должна быть преду смотрена возможность использования вложенных структур if. 9.10 Добавьте в нашем shell обработку цикла while. Для этого вам понадобится прочитать спи сок команд, составляющий тело цикла. Остерегайтесь потери памяти. 9.11 Процесс имеет много атрибутов. Один из атрибутов процесса - текущий каталог. Разработчики Unix написали программу chdir, которая относится к ряду программ ра боты с каталогами (такими, как pwd, Is, mv и т. д.). Эта программа была отвергнута, а воз можность смены каталога была включена непосредственно в shell. Что плохого могло происходить при использовании утилиты chdir? Добавьте команду cd в ваш shell. 9.12 В shell поддерживаются специальные переменные, для представления с их помощью системных установок. Например, в переменной $$ содержится PID для shell. В пере менной $? содержится код возврата последней команды. Добавьте эти переменные в вашу программу. (Автор не точен в трактовке специальных переменных. Как уже указывалось в замечании - это переменные с одно-символьными именами. Конструк ция вида $$ означает подстановку переменной с односимвольными именем $. Другими словами - $$ это не имя специальной переменной, а операция “взять значение от пере менной с именем $”. - Примеч. пер.) 9.13 В стандартных Unix shells допустимо использование “’экранирования” аргументов командной строки. Команда вида vi "MyBook Report" содержит два аргумента. Добавьте возможность экранирования в ваш shell. Где в составе алгоритма shell будут обрабаты ваться экранированные аргументы? Рассмотрите команду rm "file1.c;2". Если ваш shell распознает символ в качестве разделителя команд, то это выражение будет воспри нято при грамматическом разборе как одна команда с двумя аргументами. 9.14 Приглашение, составленное пользователем. В большинстве shell пользователи имеют воз можность устанавливать собственный текст в приглашении, что выполняется присвоением специальной переменной текстовой строки в качестве значения. Добавьте это свойство в ваш shell. Предварительно решите, какую переменную вы будете использовать для уста новки приглашения. В sh и bash используется переменная PS1, а в csh используется prompt.
Глава 10 Перенаправление ввода/вывода и программные каналы
Цели Идеи и средства • • • • • •
Перенаправление ввода/вывода: что это такое и зачем? Определение стандартного ввода, вывода и вывода ошибочных сообщений. Перенаправление стандартного ввода/вывода для файлов. Использование fork для. перенаправления ввода/вывода для других программ. Программные каналы (pipes). Использование fork при работе с программными каналами.
Системные вызовы и функции dup, dup2 pipe
10.1. SHELL-программирование Как работают команды: Is > my.files who | sort > userlist
Каким образом shell сообщает программе о том, что необходимо передавать ее выходные дан ные в файл, а не выводить на экран? Каким образом shell соединяет выходной поток одного процесса с входным потоком другого процесса? Что подразумевается под термином стан дартный ввод? В этой главе мы сосредоточим наше внимание на конкретной форме межпро цессного взаимодействия - на механизме перенаправления ввода/вывода (I/O) и на программ ных каналах (pipes). Начнем с рассмотрения того, как могут помочь механизмы перенаправ ления ввода/вывода и программные каналы при написании shell-скриптов. Далее мы рас смотрим основополагающие свойства операционной системы, которые позволяют реализо вать работу по перенаправлению ввода/вывода. Наконец, мы напишем программы, где будут изменены входные и выходные потоки для процессов.
10.2.Приложение SHELL наблюдение за пользователями
357
10.2. Приложение SHELL: наблюдение за пользователями Рассмотрим такую проблему. Вы располагаете списком друзей, которые работают на той же Unix-машине, на которой работаете и вы. Необходимо создать программу, которая бу дет сообщать вам о входе пользователей в систему и о выходе из нее. По таким результа там вы сможете вести наблюдение за своими друзьями. Вы можете написать С-программу, которая будет использовать файл utmp и интерваль ные таймеры. Программа будет открывать файл utmp, помещать туда список пользовате лей, а затем засыпать до тех пор, пока не появится необходимость опять обращаться к файлу utmp и вносить в него определенные изменения. Сколько времени пойдет на такую разработку и какой код необходим для выполнения таких действий? Более простым решением будет написать shell-скрипт. В Unix уже есть программы, которая формирует список текущих пользователей в системе. Это команда who. Кроме того, в Unix есть программа, которые позволяют переходить в состояние сна и в состояние обработки списков строк. Далее приведен скрипт, который ведет отчетность обо всех вхо дах в систему и обо всех выходах из нее: Алгоритм
get list of users (call it prev) while true sleep get list of users (call it curr) compare lists in prev, not in curr -> logout echo logged in:" in curr, not in prev - > login make prev = curr repeat
shell-код
who | sort > prev while true; do sleep 60 who | sort > curr echo logged out:" comm -23 prev curr comm • 13 prev curr mv curr prev done
В этом скрипте в составе тела цикла while использованы совместно семь средств Unix, а также показана полезность механизма перенаправления ввода/вывода при решении про блемы в данной программе. Давайте рассмотрим детали программ и связи между програм мами. В первой строке скрипта производится построение списка пользователей, отсортиро ванного по именам пользователей. Это список тех пользователей, которые уже вошли в сис тему на момент, когда начал работу скрипт. Команда who выводит список пользователей, а команда sort читает список со стандартного входа и выводит отсортированную версию это го списка.
Перенаправление ввода/вывода и программные каналы
358
Строка who | sort > prev требует от shell одновременного запуска команд who и sort. Кроме того, сообщается о необходимости послать вывод команды who непосредственно на вход команды sort (см. рисунок 10.1). Два процесса запланированы для параллельного исполне ния. Они будут разделять время центрального процессора с другими процессами в систе ме. Часть текста строки вида: sort > prev
воспринимается для shell как требование записать вывод команды sort в файл prev. При этом файл будет создан, если он до выполнения этого перенаправления не существовал. Если же файл существовал, то его старое содержимое будет заменено на новое. После того как процесс проснется через минуту, в скрипте будет создан новый список поль зователей в файле с именем curr. Каким образом можно сравнить два отсортированных спи ска, состоящих из записей о входе в систему? В Unix есть средство comm, действие которого представлено на рисунке 10.2. С помощью comm можно найти общие строки в составе двух отсортированных файлов. В рассматриваемых двух файлах есть три подмножества: множе ство из строк файла 1, множество из строк файла 2, множество строк, которые содержатся одновременно в одном и другом файлах. Команда comm сравнивает два отсортированных списка и выводит результат в три колонки, для каждого из этих подмножеств. С помощью опций команды можно подавить вывод любой из колонок. Например, с помощью двух ко манд: comm -23 prev curr # запрет вывода колонок 2 и 3 => показать строки только в prev
и comm -13 prev curr # запрет вывода колонк 1 и 3 => показать строки только в curr
будут получены те два множества, которые мы хотели. Записи о logouts: строки о тех вхо ждениях в систему, которые есть в предшествующем списке (prbv), но которых нет в теку щем списке (cur). Записи о новых logins: строки о вхождениях систему, которых нет в предшествующем списке, но которые есть в текущем списке.
Наконец, с помощью команды mv curr prev происходит замена списка prev на список curr.
Выводы Скрипт watch.sh продемонстрировал нам три важные идеи: (a) Мощность shell-скриптов - решение достигнуто более простым образом и быстрее, чем при использовании языка С. (b) Гибкость программных средств - каждое средство выполняет свою конкретную роль при решении общей задачи (c) Возможность использования средств перенаправления ввода/вывода и програм мных каналов.
10.3. Сущность стандартного ввода/вывода и перенаправления
359
В скрипте watch.sh показано, каким образом можно использовать оператор >, который по зволяет рассматривать файлы как переменные произвольного размера и произвольной структуры. Результат выполнения оператора присваивания вида ( на языке С): х = func_a(func_b(y)); /* сохранить вывод функции funcji от функции func_b в х */
будет аналогичен результату выполненя командной строки в shell: prog_b | prog_a > x #сохранить вывод от комбинации двух команд в х
Вопросы А как это все работает? Какова роль shell в установлении связей между процессами? Ка кую роль при этом играет ядро? Какова роль конкретных программ?
10.3. Сущность стандартного ввода/вывода и перенаправления Механизм перенаправления ввода/вывода в Unix основан на использовании принципа стандартных потоков данных. Рассмотрим команду sort. Эта команда читает данные из од ного потока, записывает отсортированные результаты в другой поток и выдает сообщения об ошибках при работе в третий поток. Проигнорируем сейчас рассмотрение вопроса о месте, где существуют эти стандартные потоки. Тогда утилиту sort можно представить так, как это показано на рисунке 10.3. На рисунке изображены три потока данных: standard input - стандартный ввод, т. е. поток данных, который входит в процесс. standard output ~ стандартный вывод, т. е. поток результирующих данных процесса. standard error - стандартный поток ошибок, т. е. поток сообщений об ошибках.
10.3.1. Фактор 1: Три стандартных файловых дескриптора Во всех средствах Unix используется трехпоточная модель, которая изображена на рисунке 10.3. Модель построена на основе простого соглашения. Каждый из трех пото ков представлен собственным файловым дескриптором. На рисунке 10.4 показаны дета ли такого соглашения.
360
Перенаправление ввода/вывода и программные каналы
Утверждение: Все средства Unix используют файловые дескрипторы 0,1,2. Файловый дескриптор 0 означает стандартный ввод, файловый дескриптор 1 - стандартный вывод, файловый дескриптор 2 - стандартный вывод сообщений об ошибках. Во всех средст вах Unix предполагается в начале их работы, что файловые дескрипторы 0, 1,2 уже являются для них открытыми на чтение, запись и запись, соответственно.
10.3.2'. Соединения по умолчанию: терминал Когда вы запускаете на исполнение некоторую программу с уровня командной строки shell, то обычно stdin, stdout и stderr присоединены к вашему терминалу. Поэтому програм ма читает с клавиатуры и записывает результаты и сообщения об ошибках на экран. Например, если вы наберете sort и нажмете на ключ Enter, то ваш терминал будет подсое динен к программе sort. Далее вы можете набирать на клавиатуре столько строк, сколько вам необходимо. Когда вы обозначите конец интерактивного файла нажатием на ключ Ctrl-D на отдельной строке, то программа sort отсортирует полученные входные данные и запишет результат на stdout. Большинство средств в Unix обрабатывают данные, которые читаются из файлов или со стандартного ввода. Если для некоторого средства задается в командной строке имя фай ла, то программа будет читать данные из файла. Если имя файла не указано, то программа читает данные из стандартного ввода.
10.3.3. Вывод происходит только на stdout С другой стороны, в большинстве программ не указываются имена выходных файлов. Предполагается, что они записывают результаты своей работы через дескриптор I и оши бочные сообщения - через дескриптор 2 . Если вам потребовалось послать выходные ре зультаты процесса в файл или на вход другого процесса, вы должны изменить путь, по ко торому можно пройти от файлового дескриптора при передаче данных.
10.3.4. Shell, отсутствие программы, перенаправление ввода/вывода Вы обращаетесь к shell с просьбой присоединить файловый дескриптор 1 к файлу с помо щью нотации по перенаправлению вывода: cmd > filename. Shell связывает этот файловый дескриптор с указанным файлом. 1. В командах sort и dd допускается подавление stdout. Для этого есть достаточные аргументы.
10.3. Сущность стандартного ввода/вывода и перенаправления
361
Программа продолжает писать через файловый дескриптор 1, не предполагая о смене места назначения данных. В листинге listargs.c показано, что программу никак не затрагивает, когда будут сделаны перенаправления на уровне командной строки. /* listargs.c вывод числа аргументов в командной строке, списка аргументов, а затем вывод сообщения на stderr
*
7 #include <stdio.h> main(int ac, char *av[])
{ int i; printffNumber of args: %d, Args are:\n", ac);' for(i=0;i
} Программа listargs выводит на стандартный вывод список аргументов в командной строке. Заметим, что программа listargs не выводит символ перенаправления и имя файла.
$ сс listargs.c -о listargs $ ./listargs testing one two args[0] ./listargs args[1] testing args[2] one args[3] two This message is sent to stderr.
$ ./listargs testing one two > xyz This message is sent to stderr.
$ cat xyz args[0] ./listargs args[1] testing args[2] one args[3] two
$ ./listargs testing >xyz one two 2> oops $ cat xyz args[0] ./listargs args[1] testing args[2] one args[3) two
$ cat oops This message is sent to stderr.
Эти примеры демонстрируют важность использования средства перенаправления в shell. Особо важным является то обстоятельство, что shell не передает в команду символ пере направления и имя файла. Вторым важным фактором является то, что требование по перенаправлению может ока заться в произвольном месте в команде и знак перенаправления при этом не требуется вы делять пробелами. Даже команда, подобная такой: > listing Is, будет допустимой. Таким образом, знак > не заканчивает команду и аргументы. Он рассматривается только как сред ство для установления требования на перенаправление.
362
Перенаправление ввода/вывода и программные каналы
И наконец, еще одно свойство. Во многих shell нотация перенаправления поддерживается и в отношении других файловых дескрипторов. Например, в нотации 2>filename указано на необходимость перенаправления файлового дескриптора 2, т. е. стандартного вывода сооб щений об ошибках, в поименованный файл.
/ 0.3.5. Соглашения по перенаправлению ввода/вывода Мы убедились на примере программы watch.sh, что перенаправление ввода/вывода являет ся составной частью программирования в Unix. В программе listargs.c было показано, что перенаправляет ввод и вывод shell, а не программа. Но как shell производит перенаправление ввода/вывода? Как можно написать программу, в которой происходит перенаправление ввода/вывода? Наша задача в этой главе - напи сать ряд программ, в которых выполнялись бы три базовые операции перенаправления: who > userlist присоединение stdout к файлу
sort < data who | sort
присоединение stdin к файлу присоединение stdout к stdin
10.3.6. Фактор 2: Принцип “Первый доступный, самый малый по значению дескриптор” А что же все-таки представляет собой файловый дескриптор? Файловый дескриптор - это реализация в высшей степени простой концепции. Это просто индекс в массиве. У каждо го процесса в любой момент может быть несколько открытых файлов. Для учета таких от крытых файлов составляется массив. Файловый дескриптор - это просто индекс элемента в этом массиве. На рисунке Ю.5 представлена иллюстрация принципа “Первый доступный, самый малый по значению дескриптор”.
Утверждение: Когда вы отрываете файл, то вы всегда получите первый доступный, самый меньший по значению индекс в массиве. Когда происходит установка нового соединения с файловыми дескрипторами, то возника ет ситуация, аналогичная соединению с многоканальным телефоном. Абоненты звонят по основному номеру, а внутренняя телефонная система назначает для каждого нового со единения внутреннюю линию. При реализации большинства таких систем каждому очередному звонку назначается доступная линия с наименьшим номером.
10.4. Каким образом можно подключить stdin к файлу
363
10.3.7. Синтез Итак, нами было установлено два базовых фактора. Во-первых, мы имеем соглашение, соглас но которому все процессы в Unix используют файловые дескрипторы 0,1,2 для стандартного ввода, стандартного вывода и стандартного вывода ошибочных сообщений. Во-вторых, нами установлен факт, что в ситуации, когда процессу требуется получить новый файловый дескриптор, ядро назначает файловый дескриптор, наименьший по значению среди доступ ных. При объединении этих факторов становится понятным, как работает механизм перена правления ввода/вывода. Мы теперь сможем писать программы, которые выполняют перена правление ввода/вывода.
10.4.
Каким образом можно подключить stdin к файлу
Мы детально обсудим вопрос, как программа перенаправляет стандартный ввод для того, чтобы данные поступали в программу из файла. Если быть точными, то процессы не чи тают непосредственно из файлов. Процессы читают из файловых дескрипторов. Если мы присоединили файловый дескриптор 0 к файлу, то этот файл становится источником дан ных для стандартного ввода. Мы поэкспериментируем с тремя методами присоединения стандартного ввода к файлу. Некоторые из этих методов касаются не файлов, а используются в отношении программ ных каналов.
10.4.1. Метод 1: Закрыть, а затем открыть В первом методе используется техника закрыть-затем-открыть. Эта техника воспроиз водит действия, когда при разговоре по телефону разрывают соединение и получают сво бодную телефонную линию. Затем на нее “навешивают” телефон. В результате вы исполь зуете эту линию для разговора по указанному номеру. В данном случае выполняются та кие шаги: В начале работы мы имеем типичную конфигурацию. К драйверу терминала присоедине ны три стандартных потока. Данные проходят через файловый дескриптор 0 и выходят через дескриптор 1 (см. рисунок 10.6).
Затем, после выполнения close(O) (это первый шаг), происходит отсоединение стандартно го ввода. Мы вызвали close(O), чтобы разорвать соединение стандартного ввода с драй вером терминала. На рисунке 10.7 показана ситуация, когда перестал использоваться первый элемент в массиве файловых дескрипторов.
364
Перенаправление ввода/вывода и программные каналы
Наконец, выполняется open(ftlename,0_RDONLY) (это последний шаг). В результате от крывается файл, который вы желаете подсоединить к stdin. Минимальный номер доступ ного файлового дескриптора равен 0. Поэтому файл, который вы открываете, будет под соединен к стандартному вводу (см. рисунок 10.8). Любая функция, которая будет читать из стандартного ввода в программе, будет читать из этого файла.
В программе, которая представлена ниже, используется меггодзакрыть-затем-открыть : /*stdinredir1.c * цель: показать, как перенаправляется стандартный ввод путем настройки * файлового дескриптора 0 на соединение с файлом. * действие: производится чтение трех строк со стандартного ввода, а затем * закрывается файловый дескриптор 0 и открывается файл на диске. Затем еще *
производится чтение трех строк из стандартного ввода.
«include «include main()
<stdio.h> .
*/ { int
fd;
10.4. Каким образом можно подключить stdin к файлу
365
/* чтение и вывод трех строк */ fgets(line, 100, stdin); printf(’'%s", line); fgets(line, 100, stdin); printf("%s", line); fgets(line, 100, stdin); printf("%s", line); Г перенаправление ввода */ close(O); fd = openf/etc/passwd", O.RDONLY); if (fd != 0){ fprintf(stderr,"Could not open data as fd 0\n"); exit(1);
} Г прочитать и вывести три строки */ fgets(line, 100, stdin); printf("%s", line); fgets(line, 100, stdin); printf("%s", line); fgets(line, 100, stdin); printf("%s", line);
} Программа stdinreaderl читает и выводит три строки из стандартного ввода, перенаправля ет стандартный ввод, а затем еще раз читает и выводит три строки из стандартного ввода. Программа stdinreaderl в результате читает три строки, которые набираются на клавиатуре, и выводит их, а затем читает следующие три строки из файла passwd и выводит их: $ ,/stdinredirl linel linel testing Iine2 testing Iine2 line 3 here line 3 here root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin: daemon:x:2:2:daemon:/sbin:
$ Ничего особого не произошло. По сути освобождается линия, и вы можете звонить по ней по новому номеру. Когда будет установлено соединение, вы получаете возможность слу шать нового абонента через стандартный ввод (тот же телефон).
10.4.2. Метод2: open..dose..dup..close Рассмотрим такую ситуацию. Зазвонил телефон. Вы отвечаете, но далее захотели перевести разговор на другой телефон. Вы сообщаете кому-то на другом телефоне, что на этот телефон будет переведен полученный звонок, тем самым предоставляя звонящему два соединения. Затем, после перевода линии на другой телефон, разрываете связь со своим телефоном. Действуюей остается только соединение с другим телефоном. Знакомая ситуация при пере ключении телефона? Идея этого метода заключается в дублировании соединения между теле фонами для того, чтобы вы могли разорвать связь с одним из них без потери соединения с абонентом. Системный вызов dup в Unix, действие которого изображено на рисунке Ю.9, позволяет установить второе соединение с существующим файловым дескриптором. Этот метод тре бует выполнения четьюех шагов:
Перенаправление ввода/вывода и программные каналы
366
open(file). Первый шаг заключается в открытии файла, к которому будет присоединен stdin. В результате выполнения этого вызова будет получен файловый дескриптор, значение которого не равно 0, поскольку дескриптор 0 в текущий момент открыт.
close(O). Следующий шаг заключается в закрытии файлового дескриптора 0. После этого файловый дескриптор считается свободным. dup(fd). Системный вызов dup(fd) выполняет дублирование файлового дескриптора fd. В качестве дубля дескриптора будет использован свободный файловый дескриптор с наименьшим номером. Именно поэтому дублирующим файловым дескриптором для дескриптора отрытого файла оказывается дескриптор в позиции 0 в массиве дескрип торов открытых файлов. В результате мы подсоединили дисковый файл к файловому дескриптору 0.
closeffd). Наконец, мы выполняем ciose(fd). После выполнения этого вызова начальное соединение с файлом закрывается и остается соединение только с файловым дескрип тором 0. Сравните этот метод с технологией переключения разговора по телефону с од ного телефона на другой.
В программе stdinredir2.c использован метод 2: /* stdinredir2.c * показывает использование второго метода перенаправления стандартного ввода * используется «define для установки того или другого способа 7 «include <stdio.h> «include Г «define CLOSE.DUP /* open, close, dup, close */ /* «define USE_DUP2 /* open, dup2, close */
10.4. Каким образом можно подключить stdin к файлу
367
{
int fd; int newfd; char line[ 100]; /* чтение и вывод трех строк */ fgets(line, 100, stdin); printf("%s", line); fgets(line, 100, stdin); printf("%s”, line); fgets(line, 100, stdin); printf("%s", line); /* перенаправление ввода */ fd = openf’data", 0_RD0NLY); /* открытие дискового файла */ #ifdefCLOSE_DUP close(O); newfd = dup(fd); /* присвоение файловому дескриптору fd значения 0 */ #else newfd = dup2(fd,0); /* закрыть 0, присвоить для fd значение 0 */ #endif if (newfd != 0){ fprintf(stderr,"Could not duplicate fd to 0\n"); exit(1);
N
)
close(fd); /* закрытие оригинального файлового дескриптора fd */ /* чтение и вывод трех строк */ fgets(line, 100, stdin); printf("%s", line); fgetsjline, 100, stdin); printf("%s", line); fgets(line, 100, stdin); printf("%s", line);
} Данный четырехшаговый метод мы рассмотрели исходя из познавательных целей: были показаны возможности системного вызова dup. Метод существенно важен при работе с программными каналами. Более простой в использовании метод объединяет шаги, где выполняются системные вызовы close(O) и dup(fd), в один шаг. В методе выполняется сис темный вызов dup2.
10.4.3. Обобщенная информация о системном вызове dup dup,dup2 НАЗНАЧЕНИЕ
Копирование файлового дескриптора
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ
newfd = dup(oldfd); newfd = dup2(oldfd, newfd); 4
АРГУМЕНТЫ
oldfd - копируемый файловый дескриптор newfd - копия файлового дескриптора oldfd
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки newfd - новый файловый дескриптор
Системный вызов dup создает копию файлового дескриптора oldfd. С помощью систем ного вызова dup2 файловый дескриптор newfd становится дублем файлового дескриптора oldfd. Два файловых дескриптора ссылаются на один и тот же открытый файл. Оба систем ных вызова возвращают в качестве результата новый файловый дескриптор или значение -I при ошибке.
Перенаправление ввода/вывода и программные каналы
10.4.4. Метод3: open..dup2..dose В коде программы stdinredir2.c есть оператор #ifdef, с помощью которого происходит заме щение системных вызовов close(O) и dup(fd) на системный вызов dup2(fd,0). Системный вы зов dup2(orig,new) заводит файловый дескриптор new в качестве дубликата старого файло вого дескриптора orig. Это действие будет выполнено, даже если придется закрыть существующее соединение с файловым дескриптором new. 10.4.5. Shell перенаправляет stdin не для себя, а для других программ Рассмотренные примеры показывают, как программа может присоединить свой стандарт ный ввод к файлу. Естественно, на практике, если в программе возникает необходимость читать из файла, то она может просто открыть этот файл, а не изменять стандартный ввод. Смысл этих примеров заключался в том, чтобы показать, как одна программа может изме нять стандартный ввод для другой программы.
10.5. Перенаправление ввода/вывода для других программ: who > userlist Когда пользователь набирает команду вида who > userlist, то shell в ответ запускает на ис полнение команду who, для которой ее стандартный вывод он перенаправит на файл user list. А как это все происходит? Секрет заключается во втором разрыве при использовании fork и ехес. После выполнения fork дочерний процесс работает все еще в соответствии с программным кодом shell. Но дочерний процесс намерен вызвать ехес. Системный вызов ехес производит замену программы в дочернем процессе, но остаются неизменными атрибуты и соединения процесса. Другими словами, после выполнения ехес у процесса остается то же значение пользовательского иден тификатора (UID), остается то же значение приоритета, что и было, а также остаются в его распоряжении те же файловые дескрипторы, которые были за ним закреплены до выполне ния ехес. Итак, еще раз - программа получает в свое распоряжение открытые файлы процесса, куда она загружается. На рисунке 10.10 иллюстрируется, каким образом происходит перена правление вывода для дочернего процесса.
10.5. Перенаправление ввода/вывода для других программ: who > userlist
369
Давайте в пошаговом режиме рассмотрим, как процесс использует этот принцип для перенаправления стандартного вывода. 1. Начало действий На рисунке 10.11 представлен процесс, который был запущен в пользовательском про странстве. Как показано на рисунке, файловый дескриптор 1 был присоединен к файлу/ Для облегчения восприятия ситуации на рисунке другие файлы не показаны.
2. После выполнения системного вызова fork
На рисунке 10.12 видно, что появился новый процесс. Этот процесс выполняет тот же код, что и оригинальный процесс. Но он знает, что является дочерним процессом. Дочерний процесс имеет тот же программный код, те же данные, тот же самый набор открытых фай лов, как у его процесса-отца. Поэтому, естественно, в элементе 1 в массиве дескрипторов открытых файлов у дочернего процесса будет также ссылка на файл f. Далее дочерний процесс вызывает close(l).
370
Перенаправление ввода/вывода и программные каналы
3. После того, как дочерний процесс выполнил close (1)
На рисунке 10.13 видно, что процесс-отец не выполнял вызов close(l). Поэтому в процессе-отце файловый дескриптор 1 остается соединенным с файлом f. В дочернем про цессе в текущий момент файловый дескриптор 1 - это свободный дескриптор с самым минимальным номером. Далее дочерний процесс открывает файл g. 4. После
выполнения в дочернем процессе системного вызова creat("g'\ m)
Как показано на рисунке 10.14, теперь файловый дескриптор 1 будет присоединен к файлу g. Стандартный вывод дочернего процесса оказался перенаправлен на файл g. Далее дочерний процесс вызывает ехес, чтобы запустить на исполнение команду who.
10.5. Перенаправление ввода/вывода для других программ: who > userlist 5.
371
После запуска в дочернем процессе новой программы с помощью вызова ехес
На рисунке 10.1-5 изображено, как дочерний процесс начинает выполнять команду who. Про граммный код и данные shell удаляются из дочернего процесса и замещаются на код и данные программы who. После выполнения ехес файловые дескрипторы остаются теми же. Открытые файлы (Более точно - дескрипторы открытых файлов. - Примеч. пер.) не являются частью кода или данных программы. Это атрибуты процесса. Команда who записывает список пользо вателей в дескриптор 1. Команда who направляет результаты свой работы в файл g, даже не до гадываясь об этом. В программе whotofile.c проиллюстрировано использование рассмотренного метода: /* whotofile.c * назначение: показать, как происходит перенаправление вывода для другой * программы * принцип: fork, затем перенаправление вывода в дочернем процессе, затем ехес 7 #include <stdio.h> main()
{ int pid; int fd; printffAbout to run who into a file\n"); /* создание нового процесс или quit 7 if((pid = fork()) ==-1){ perrorf'fork"); exit(1);
}-
Г работает дочерний процесс */ if (pid = 0){ close(1); fd = creatf’userlist", 0644); execlp("who", "who", NULL); perrorf'execlp"); exit(1);
/‘закрытие, * Г затем открытие */ /* и запуск 7
372
Перенаправлениеввода/выводаипрограммныеканалы
) Г процесс-отец ждет окончания дочернего процесса, затем выводит сообщение */ if (pid !=0){ wait(NULL); printf("Done running who. results in userlist\n");
} 10.5.1. Итоговые замечания no перенаправлению стандартных потоков в файлы Итак, в Unix поддерживаются три базовых принципа, которые позволяют легко присоеди нять стандартный ввод, стандартный вывод и стандартный вывод ошибок к файлам: (a) В качестве файловых дескрипторов для стандартного ввода, вывода и вывода сооб щений об ошибках используются, соответственно, файловые дескрипторы 0, 1 и 2. (b) Ядро всегда использует при назначении файловый дескриптор с наименьшим но мером, из числа пронумерованных и неиспользуемых дескрипторов. (c) Набор файловых дескрипторов не изменяется после выполнения вызова ехес. Shell использует наличие интервала между выполнением системного вызова fork и ехес для того, чтобы присоединить потоки данных к файлам. Shell также поддерживает такие нотации для перенаправления: who »userlog sort < data Разработка программы, где используются эти две операции, остается в качестве упражне ния.
10.6. Программирование программных каналов Мы рассмотрели, каким образом можно написать программу, которая присоединяет стан дартный вывод к файлу. Теперь разберемся с тем, как можно использовать программные каналы для соединения стандартного вывода одного процесса со стандартным вводом другого процесса. На рисунке 10.16 показано, как работает программный канал. Про граммный канал представляет собой однонаправленный канал в составе ядра.
10.6. Программирование программных каналов
373
В программном канале происходит чтение с одного конца канала, а запись производится в другой конец канала. Для реализации конструкции вида who \ sort, нам необходимо знать две вещи. Нам нужно знать, как можно создать программный канал и как можно присое динить стандартный ввод и вывод к программному каналу
10.6.1. Создание программного канала Структура программного канала (довольно часто этот термин не переводится и использу ется в тексте в первоначальном виде - pipe. - Примеч. пер.) показана на рисунке 10.17. Для создания программного канала используется системный вызов pipe:
pipe НАЗНАЧЕНИЕ
Создание программного канала
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ
result = pipe(int array[2])
АРГУМЕНТЫ
array - целочисленный массив из двух элементов
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки 0 - при успешном завершении
Системный вызов pipe создает программный канал и присоединяет к двум концам канала два файловых дескриптора. Файловый дескриптор аггау[0] используется на том конце кана ла, откуда производится чтение, а файловый дескриптор аггау[1] присоединяется к концу канала, куда производится запись данных. Внутреннее устройство канала, как и внутрен нее устройство структур у открытого файла, скрывается ядром. Процесс видит только два файловых дескриптора. На рисунке 10.18 изображены два шага создания программного канала процессом. На изо бражении слева показан стандартный набор файловых дескрипторов перед выдачей сис темного вызова pipe. На изображении, где представлена ситуация после вызова pipe, видно, что был создан новый программный канал в ядре и для процесса были построены два соединения с этим программным каналом. Заметим, что при выполнении системного вызова pipe, как и при выполнении open, при на значении дескрипторов используется метод поиска файловых дескрипторов с наимень шим номером среди доступных в текущий момент.
374
Перенаправление ввода/вывода и программные каналы
Программа pipedemo.c создает программный канал, а затем использует канал для посылки данных самой себе:
Г pipedemo.c * Цель: продемонстрировать, как создается и используется программный канал. Действие: создается программный канал, производится запись данных * через один из концов канала, а затем после определенной работы происходит * чтение данных с другого конца программного канала. На самом деле все это * происходит несколько иначе, но смысл идеи демонстрируется точно.
7 #include tinclude main()
<stdio.h>
{ int len, i, apipe[2]; /* два файловых дескриптора 7 char buf[BUFSIZ]; /* буфер для чтения 7 Г построить программный канал */ if (pipe (apipe) ==-1){ perror("could not make pipe”); exit(1);
) printf("Got a pipe! It is file descriptors: {%d %d }\n", apipe[0], apipe[1]);
Г чтение из stdin, запись в программный канал, чтение из программного ’ канала, печать 7 while (fgets(buf, BUFSIZ, stdin)){ len = strlen(buf); if (write(apipe[1], buf, len) != Ien){ /* запись данных */ perrorf'writing to pipe"); /* выход 7 break; /* pipe */
10.6. Программирование программных каналов
375
) for (i = 0; i
} if (write( 1, buf, len) != Ien){ /* запись */ perrorf'writing to stdout"); /* to */ break; /* stdout */
) }
} На рисунке 10.19 изображен поток данных, который проходит от клавиатуры к процессу, от процесса к программному каналу, и от процесса возвращается опять к терминалу. Теперь мы знаем, как создается программный канал, как можно писать данные в канал и читать данные из канала. На практике, конечно, вряд ли программа будет посылать дан ные сама себе. Но мы можем связать вместе два процесса, используя для этого вызовы pipe и fork.
10.6.2. Использование fork для разделения программного канала После создания процессом программного канала, процесс соединен с обоими концами ка нала. После того, как процесс создаст дочерний процесс, который является копий порож дающего процесса, порожденный процесс получит по наследству все эти соединения с ппогпаммным каналом. Это показано на рисунке 10.20. Процесс-отец и дочерний про-
376
Перенаправление ввода/вывода и программные каналы
цесс имеют возможность записывать данные в канал на конце, где можно записывать. Процесс-отец и дочерний процесс имеют возможность читать данные на конце, предна значенном для чтения, (см. рисунок 10.21). Оба процесса имеют возможность писать и чи тать. Но программный канал будет использоваться более эффективно тогда, когда один процесс записывает данные в канал, а другой процесс читает данные из канала.
В программе pipedemo2.c показывается, как можно совместно использовать вызовы pipe и fork для создания двух процессов, которые взаимодействуют между собой через про граммный канал:
Г pipedemo2.c * *
Демонстрируется, каким образом с помощью fork() можно разделять программный канал
10.6. Программирование программных каналов
377
Процесс-отец может писать в канал и читать из него, а дочерний процесс может только писать в канал
* *
7 #include <stdio.h> #define CHILD_MESS tdefine PAR_MESS #define oops(m,x) main()
"I want a cookie\n" "testing..\n" {perror(m); exit(x);}
{ int pipefd[2]; /* pipe 7 int len; Г для записи */ char buf[BUFSIZ]; /* для чтения 7 int readjen; if (pipe(pipefd) ==-1) oops("cannot get a pipe", 1); switch(fork(j){ case -1: oopsf'cannot fork", 2); Г дочерний процесс пишет в канал каждые 5 секунд */ caseO: len = strlen{ СНI LD_M ESS); while (1){ if (write(pipefd[1 ], CHILD.MESS, len) != len) oops("write", 3); sleep(5);
} /* процесс-отец читает из канала, а также пишет в канал */ default: len = strlen( PAR_M ESS); while (1){ if (write(pipefd[1], PAR_MESS, len)Nen) oops("write", 4); sleep(1); read_len = read(pipefd[0], buf, BUFSIZ); if (readjen <= 0) break; write(1, buf, readjen);
} } }
10.6.3. Финал: Использование pipe, fork и exec Мы познакомились со всеми идеями и последовательностью действий, которые нужны для написания программы, соединяющей вывод команды who со вводом команды sort. Мы знаем, как создавать программный канал, мы знаем, как можно разделять программный
378
Перенаправление ввода/вывода и программные каналы
канал между двумя процессами, мы знаем, как изменить стандартный вывод процесса, мы знаем, как изменить стандартный ввод процесса. А теперь объединим все, перечисленное выше, с тем, чтобы можно было бы написать про грамму общего назначения. Программа будет называться pipe. При ее вызове необходимо будет указывать два аргумента, которые являются именами двух программ. На примерах: pipe who sort pipe Is head
показаны варианты обращения к программе pipe. Алгоритм программы такой: pipe(p) fork() I child
parent
I
I
close(p[0]) dup2(p[1],1) close(p[1]) exec "who"
close(p[1]) dup2(p[0],0) close(p[0]) exec "sort"
Далее представлен код программы: Г pipe.c * * Демонстрируется, как можно создать конвейер из двух процессов. * * Берутся два аргумента, задаваемые при обращении к программе. Каждый аргумент - это имя команды. Производится соединение вывода команды, * заданной значением av[1 ], с вводом команды, заданной значением 3v[2] * * Использование: pipe command 1 command2 * Действие: command 11 command2 * * Ограничения: команды в конвейере не должны иметь аргументов * • * используется execlpO, т.к. известно число аргументов * * Замечание: поменяйте ролями дочерний процесс и отцовский и посмотрите, * что получится */ «include . <stdio.h> «include «define oops(m,x) {perror(m); exit(x);} main(int ac, char **av)
{ i
nt
thepipe[2], newfd, pid;
/* два файловых дескриптора, */ Г необходимые для программного канала */ /* идентификатор процесса pid */
if (ас != 3){ fprintf(stderr, "usage: pipe cmd 1 cmd2\n"); exit(1);
} if (pipe(thepipe) == -1) oops("Cannot get a pipe", 1);
/* get a pipe */
/*------------------------------------------------------------------------ */ /* теперь, когда мы имеем канал, давайте образуем два процесса */
10.6. Программирование программных каналов
379
if ((pid = fork()) == -1) /* образовать процесс */ oopsf'Cannot fork", 2);
Г Г Процесс-отец будет выполнять ехес av[2] */ if (pid > 0){ Г Процесс-отец не будет писать в канал */ close(thepipe[ 1 ]); if (dup2(thepipe[0], 0) == -1) oopsf'could not redirect stdin",3); /* stdin будет дублирован, закрываем pipe 7 close(thepipe[0]); execlp(av[2], av[2], NULL); oops(av[2], 4);
) Г дочерний процесс выполняет ехес av[1 ] и производит запись в программный канал
7 close(thepiре[0]); if (dup2(thepipe[1 ], 1) == -1) oopsf'could not redirect stdout", 4); close(thepipe[1 ]); execlp(av[1], av[1], NULL); oops(av[1], 5);
/* дочерний процесс не будет читать из канала 7
/* stdout будет дублирован, закрываем pipe */
• > Программа pipe.c использует те же идеи и средства, которые использует shell при создании конвейеров. Однако shell не запускает внешнюю программу, аналогично pipe.c. Shell созда ет канал, затем с помощью fork создает два процесса, затем производит перенаправление стандартного ввода и вывода на канал, и, наконец, запускаются по ехес две программы.
10.6.4. Технические детали: Программные каналы не являются файлами Программные каналы во многом выглядят так, будто это обычные файлы. Процесс ис пользует системный вызов write для помещения данных в канал и использует системный вызов read для извлечения данных из канала/ Программный канал, как и файл, выглядит как последовательность байтов, в которой не различаются какие-то блоки или записи. Но с другой стороны, программные каналы и файлы имеют отличия. Например, как для программного канала следует трактовать конец файла? Следующие технические детали разъясняют некоторые сходство и различия. Чтение из программных каналов 1. Системный вызов read может быть блокирован на канале При попытке процесса выполнять системный вызов read в отношении программного канала, такой вызов будет блокирован до тех пор, пока в программный канал не будет записано некоторое количество данных. А что гарантирует от попадания процесса в состояние бесконечного ожидания при блокирований? 2. Чтение признака конца файла EOF из программного канала Когда все процессы-писатели закроют канал на стороне, где происходит запись в ка нал, то попытка выполнить вызов read в отношении такого состояния канала приведет к тому, что системный вызов получит в качестве результата 0. Что расценивается, как п изнак конца файла.
380
Перенаправление ввода/вывода и программные каналы
3. Наличие многих процессов- ч ит am елей может вызвать проблемы Структурно программный канал представляет собой очередь. После того, как процесс прочитал из канала какое-то число байтов, эти данные больше не остаются в канале. Если два процесса пытаются читать из одного программного канала, то один процесс получит в результате один набор байтов, а другой процесс получит уже другой набор байтов. Если два процесса не используют некоторый метод, который мог бы коорди нировать их доступ к каналу, то данные, которые они прочитают, вероятно, будут не определенными (Здесь данные не определены в том смысле, что процессы не могут знать, в каком порядке они могут произвести чтение данных из канала. -Примеч. пер.)
Запись в программные каналы 4. Запись в программный канал по системному вызову write блокируется, пока в канале не появится свободное место. Каналы имеют конечный размер, который по значению значительно меньше типовых размеров дисковых файлов. Когда процесс пытается выполнить вызов write в отноше нии канала, то такой вызов будет блокирован до тех пор, пока в канале не появится дос таточно места для проведения такой записи. Если процессу необходимо записать, скажем, 1000 байтов, а свободное пространство в канале в этот момент имеет размер только 500 байтов, то процесс будет ждать, пока в канале появится свободное про странство не менее 1000 байтов. А что произойдет, если процесс пожелает записать в канал сообщение размером в миллион байтов? Не будет ли процесс бесконечно долго ждать выполнения такого вызова? 5. При выполнении write гарантируется минимальный размер участка памяти В соответствии со стандартом POSIX установлено, что ядро не разбивает участки памяти под данные на блоки, меньшие по размеру 512 байтов. В Linux гарантирован размер нерасщепляемого буфера для программного канала в 4096 байтов. Если два раз личных процесса будут пытаться писать в канал, и каждый процесс передает сообще ния не более 512 байтов, то для процессов будет гарантировано, что их сообщения в ка нале не будут расщеплены. 6. write заканчивается аварийно, если в текущий момент при работе с каналом обнару живается отсутствие процессов-читателей Если все процессы-читатели произведут закрытие канала на том конце, где произво дится чтение, то тогда попытка выполнить вызов write в отношении канала может при вести к проблеме.(Новые данные из канала не будут, конечно, читаться, когда будут убиты все процессы-читатели - Примеч. пер.). Если данные не были востребованы из канала, то куда девать новые данные? Для того чтобы избежать потери данных, ядро использует два метода, чтобы уведомить процесс о том, что попытка выполнить write будет бесполезной. Ядро посылает процессу сигнал SIGPIPE. Если при этом процесс убивается, то каких-то дальнейших действий и не требуется. В другом варианте после выполнения write в указанной ситуации в качестве кода возврата устанавливается -1, а значение переменной errno становится равным EPIPE.
Заключение Основные идеи • •
Перенаправление ввода/вывода дает возможность отдельным программам работать по своему назначению. Каждая программа выполняет свои функции. По соглашениям, принятым в Unix, программы производят чтение входных данных чепез Файловый лескпиптоп 0. запись данных — чепез лескоиптоо 1. и выдачу
Заключение
•
•
•
•
•
381
сообщений об ошибках - через дескриптор 2. Эти три файловых дескриптора называют стандартным вводом, стандартным выводом, стандартным выводом сообщений об ошибках. Когда вы входите в систему, то процедура входа устанавливает файловые дескрипторы 0,1 и 2. Эти соединения и все дескрипторы открытых файлов передаются от процесса-отца к дочернему процессу. Они остаются у дочернего процесса и после . выполнения системного вызова ехес. Системные вызовы, которые создают файловые дескрипторы, всегда используют при назначении нового дескриптора значение дескриптора с наименьшим номером среди доступных дескрипторов. Перенаправление стандартного ввода, вывода и сообщений об ошибках изменяет место, к которому будут прикреплены файловые дескрипторы 0,1,2. Существуют несколько методов перенаправления стандартного ввода/вывода. Программный канал -это очередь данных в ядре, в отношении которой к каждому концу присоединен файловый дескриптор. Программа создает программный канал с помощью системного вызова pipe. При выполнении процессом-отцом системного вызова fork дескрипторы на обоих концах канала копируются для дочернего процесса. Программные каналы могут связывать процессы, у которых общий процесс-отец.
Что дальше? В традиционных программных каналах Unix происходит передача данных между процес сами только в одном направлении. А что произойдет, если два процесса попытаются пере давать данные через канал в прямом и обратном направлениях? Что, если два процесса не будут являться родственниками или если два процесса развиваются на разных ком пьютерах? В последующих главах мы рассмотрим работу программных каналов более детально, а затем начнем изучение вопросов сетевого программирования. Идея, которая была реализована в программных каналах, была обобщена при реализации идеи сокетов.
Исследования 10.1 Значение символов >>. Нотация » говорит, что shell должен произвести присоедине ние (по append) вывода к файлу. Как будет поступать shell - будет ли он использовать метод auto-append (см. главу 5)? Или будет производить установку указателя записи на конец файла, и начинать потом запись с этого места? Проведите эксперимент с исполь зованием скриптов, чтобы ответить на эти вопросы. 10.2 В программе pipe.c процесс-отец запускал программу, которая принимала данные, а дочерний процесс запускал программу, которая вырабатывала данные. Какие будут от личия, если эти процессы поменять ролями в части обработки данных? Для изменения ро лей достаточно сделать такие изменения: if (pid > 0) нужно заменить на if (pid == 0). Что при этом произошло? Почему? 10.3 Какие необходимо сделать изменения в вашем shell, чтобы иметь возможность рабо тать с программными каналами? Во-первых, как вам следует модифицировать поток управления для того, чтобы идентифицировать и управлять командами, которые заканчиваются знаком для программного канала? Во-вторых, что будет, если несколько команд будут разделены знаком для программного канала?
382
Перенаправление ввода/вывода и программные каналы
10.4 В программе pipe.с читающий процесс sort закрывает свой файловый дескриптор на чтение из канала. Измените код таким образом, чтобы читающий процесс не закрывал бы дескриптор на запись в канал. После чего запустите программу на исполнение и пона блюдайте за ее поведением. 10.5 Добавление средств обработки символовв > и < к вашему shell. Мы ранее, в этой главе изучили смысл нотации для присоединения стандартного ввода или стандартного вывода к файлу. Мы увидели, что символ перенаправления и имя файла могут оказаться в произ вольном месте командной строки. Мы также отметили, что символ перенаправления и имя файла не являются частью списка аргументов, которые передаются в программу. В каком месте алгоритма нашего небольшого shell необходимо будет идентифициро вать требования на изменение ввода или вывода на дисковый файл? Где в алгоритме нашего небольшого shell должно производиться перенаправление? Что произойдет, если пользователь наберет: set > varlist? Допустит ли shell выполнить перенаправление вывода для встроенных команд? Каким образом можно добавить эту возможность в наш shell? 10.6 Защита от пользователей. Что произойдет, если пользователь наберет: sort data. . В чем заключается проблема с таким требованием на перенаправление? Что в данной ситуации будут делать стандартные shells в Unix? Как может ваш shell справиться с этой проблемой? 10.7 Мы изучили и проверили методы для присоединения стандартного ввода или стан дартного вывода процесса к файлу. Во всех наших примерах предполагалось использо вание обычных дисковых файлов. Может ли механизм перенаправления ввода/вывода работать с файлами устройств? Другими словами, что будет, если вы выполните close (0) и open (7dev/tty",0)? Что будет делать shell с командой who > /dev/tty? 10.8 В программе pipe.c мы вызывали fork и ехес. Но мы не вызывали wait. А почему? 10.9 В чем dup похож на link?
Программные упражнения 10.10 Модифицируйте скрипт watch.sh так, чтобы он .получил бы в результате ряд усовер шенствований. а) В данной версии должны выводятся записи о всех входах пользователей в систему и о всех выходах. Может быть полезным вариант, где можно будет передавать в качестве аргумента имя файла, хранящего список тех пользователей, за которыми следует вести наблюдение. (b) В данной версии производится некий вывод при каждой итерации цикла, даже если ничего не изменяется. Модифицируйте эту программу так, чтобы она выводила бы со общения о новых входах и новых выходах только в случае, если появляется нечто, что необходимо показать. (c) В команде who выводится список пользователей, с указанием для каждого пользо вателя времени входа в систему и названия терминала. Это может быть излишней ин формацией для вас. Если пользователь соединяется с системой, используя для этого второе окно, то это тоже может быть вам не интересно. Напишите версию программы, которая сообщала бы когда для пользователя изменяется состояние “вышедший из сис темы” на к4не вышедший из системы”, независимо от терминала. (d) В данной версии данные хранятся в файлах prev и curr в текущем каталоге. Эти файлы остаются в каталоге, когда программа заканчивает работу. Такое решение не
Заключение
383
удовлетворительно по нескольким причинам. Каковы эти причины? Проверьте свой скрипт и используйте в нем временные файлы. Удаляйте эти файлы при выходе из про граммы. Прочитайте документацию о команде trap, которую можно использовать в shell. Проверьте, как используется команда mktemp. 10.11 Модифицируйте программу whotofile.C так, чтобы она присоединяла бы вывод коман ды who к файлу. Обеспечьте гарантию, чтобы программа продолжала работать в ситуа ции, когда обнаруживается, что файла не существует. 10.12 Напишите программу sortfromfile.c, которая перенаправляет вывод команды sort таким образом, чтобы команда читала бы из файла. Имя файла должно задаваться в качестве аргумента при обращении к программе 10.13 Расширьте возможности программы pipe.c так, чтобы она могла бы управлять трехт ступенчатыми конвейерами. В этой новой версии программа должна принимать через список аргументов имена трех программ, которые она будет запускать на исполнение в составе конвейера. Команда pipe3 who sort head
должна в результате привести к построению и запуску конвейера вида: who | sort | head. 10.14 Расширьте возможности программы pipe3 в предшествующем пункте так, чтобы она
могла бы строить конвейеры с произвольным количеством ступеней. 10.15 Развилка для процесса. Утилита tee предоставляет вам возможность перенаправлять
данные в файл, а также передавать эти же данные команде, которая работает на сле дующей ступени конвейера. Например, при работе конвейера: who | tee userlist | sort > Iist2
будет получен файл без сортировки и отсортированный файл: userlist и Iist2. В качестве аргумента для команды tee используется имя файла. Прочитайте документацию о этой команды, чтобы ознакомиться с деталями. Напишите программу progtee, которая пере направляла бы данные некоторой программе, а также передавала бы данные на сле дующую ступень конвейера. Например, в конвейере: who | progtee mail smith | sort | progtee mail -s "hello" root > Iist2
будет производиться передача по почте Смиту списка пользователи (без сортировки), передача отсортированного списка будет произведена для root, и копия отсортирован ного списка будет передана в файл Iist2 10.16 Программа isatty. Программам, которые производят запись на стандартный вывод, обычно нет дела до того, что файловый дескриптор может быть присоединен к терми налу или к дисковому файлу. Из текста следует, что процесс не может узнать, куда сейчас прикреплен дескриптор. Это не так. Библиотечная функция isatty(fd) возвращает значение true, если файловый дескриптор fd присоединен к терминалу. В функции isatty используется системный вызов fstat. Прочитайте документацию об этом вызове и ис пользуйте эту информацию для написания функции isaregfile, которая должна возвра щать true, если ее аргумент оказывается на самом деле файловым дескриптором, при соединенным к обычному файлу.
Глава 11 Соединение между локальными и удаленными процессами. Серверы и сокеты
Цели Идеи и средства • • • • • •
Модель клиент/сервер. Использование программных каналов для двусторонних связей. Сопрограммы. Сходство между файлами и процессами. Сокеты: зачем, что это такое, как устроены? Сетевые службы (сервисы). Йспользование сокетов для клиент/серверных программ.
Системные вызовы и функции •
fdopen
•
рореп
•
socket
•
bind
•
listen accept
•
connect
/ /. 1. Продукты и сервисы
385
11.1. Продукты и сервисы Программисты в Unix используют программные каналы для создания своеобразных линий цифровой сборки, родственные лентам заводских конвейеров, которые передают собираемые узлы от одного работающего к другому. В ряде учреждений не достаточна модель конвейера. В ряде случаев нужны двунаправленные связи. Рассмотрим химчистки, юридические и ветеринарые службы. Вы сдаете одежду в приемник химчистки, ведете свое домашнее животное к ветеринару, пересылаете по почте документы юристу. Здесь, в отличие от рабочего на автомобильном заводе, который .передает автомобиль на конвейере следующему рабочему, вы предпола гаете после выполнения сдачи получить что-то назад. В этих примерах мы рассмотрели работу, выполняющуюся неким другим человеком, которая рассматривается как услуга (сервис), а сами мы в таких примерах выступаем в роли клиентов по отношению к этому сервису (службе). . Какое отношение все это имеет к Unix? Программные каналы в Unix передают данные от одного процесса к другому. Процессы и программные каналы могут не только воспроиз водить работу сборочного конвейера, на выходе которого получают некие продукты, но также и воспроизводят службу сервиса. В этой главе мы сфокусируем внимание на пото ках данных между процессами, что является базисом для программирования модели кли ент/сервер.
11.2. Вводная метафора: интерфейс автомата для получения напитка Программы поглощают информацию. Большинство людей поглащают напитки. Представьте себе автомат по продаже газированного напитка, как это показано на рисун ке 11.1. Вы бросаете монетку, нажимаете на кнопку и получаете чашку некого напитка. А что происходит при этом внутри автомата? Внутри может находиться емкость с газированной водой и отдельная емкость для питье вого концентрата. При нажатии на кнопку начнется процесс смешивания исходных ма териалов. Далее только что полученный напиток будет налит в чашку покупателя. Но возможен и другой вариант. Внутри автомата может находиться просто бутыль с пред варительно приготовленным напитком, к которой присоединен насос. При нажатии на кнопку напиток просто наливается в чашку.
386
Соединение между локальными и удаленными процессами. Серверы и сокеты
Unix, как и автомат для продажи содовой, представляет один интерфейс, даже когда дан ные приходят от источников разных типов (см. рисунок 11.2).
(Iу 2) Дисковые файлы и файлы устройств Используется open для соединения, используется read и write для передачи данных.
(3) Программные каналы Используется pipe для создания, используется fork для разделения канала,*используется read и write для передачи данных.
(4) Сокеты Используется socket, listen, connect для соединения, используется read и writq для пере дачи данных. В абстракции файла Unix инкапсулирует как источник, так и средства для “производства данных”. В главе 2 мы рассматривали особенности чтения данных из файла. В главе 5 идея файла была расширена и распространена на устройства. Там мы рассмотрели, что чтение из процессов происходит аналогично чтению данных из файлов. *
11.3. Ьс: калькулятор в UNIX В любой версии Unix есть калькулятор Ьс. В программе Ьс есть переменные, циклы и функции. Калькулятор Ьс может обрабатывать очень большие числа, как мы убедились в главе 1: $ Ьс 17Л123 22142024630120207359320573764236957523345603216987331732240497016947\ 29282299663749675090635587202539117092799463206393818799003722068558\ 0536286573569713
Символ обратного слеша в конце строки указывает на последующее продолжение строки. Но Ьс не является калькулятором. Программа-калькулятор производит грамматический разбор входных данных, выполняет требуемые действия, а затем выводит результат.
/1.3. be: калькулятор в UNIX \
В большинстве версий Ьс происходит грамматический разбор входных данных, но дейст вий по вычислению не производится1. Вместо этого программа Ьс запускает на исполне ние программу-калькулятор dc и соединяется с ней через программные каналы. Програм ма dc является программой, где используется работа со стеком. Это предполагает, что пользователь должен вводить оба вычисляемых значения перед знаком операции. Напри мер, пользователь должен вводить данные так:
22 + чтобы сложить 2 и 2.
На рисунке 11.3 показано, как программа Ьс производит обработку выражения 2+2. Поль зователь набирает текст 2+2, затем нажимает на клавишу Enter. Программа Ьс читает это выражение со стандартного входа, делает его разбор - выделяет значения и действие, затем посылает программе dc такую последовательность команд: «2» «2й “+" “ри
Программа dc помещает в стек два значения, применяет в отношении этих значений операцию сложения, затем выводит на свой стандартный вывод значение, которое будут получено в результате вычисления выражения на вершине стека. Программа Ьс читает результат из программного канала, к которому присоединен стан дартный вывод программы dc. После чего полученный результат передается пользовате лю. В составе Ьс нет переменных. Если пользователь^наберет при вводе: х = 2 + 2, тогда программа Ьс обратится к dc с тем, чтобы было выполнено арифметическое действие и ре зультат был помещен в регистр х в программе dc. При выполнении команды Ьс -с видно, что после разбора происходит посылка данных калькулятору. Даже при использовании версии GNU команды Ьс происходит преобразование пользовательского ввода в выраже ние, ориентированное на стековое выполнение. Идеи, реализованные в Ьс /. Модель клиент/сервер Пара программ bc/dc представляет собой пример программной реализации модели клиент/ сервер. В программе dc реализован сервис: вычисление выражения. У программы dc есть хорошо определенный язык, который использует обратную польскую запись. Два процес са взаимодействуют взаимосвязи через stdin и stdout. В программе Ьс реализован пользова тельский интерфейс. Эта программа пользуется тем сервисом, который реализован в про грамме dc. Программу Ьс называют клиентом программы dc. 2. Двунаправленные коммуникации В отличие от модели сборочной (поточной) линии при обработке данных, модель клиент/ сервер часто требует, чтобы один процесс имел связь как со стандартным вводом, так и со стандартным выводом другого процесса. Традиционно программные каналы в Unix 1. В версии GNU программа Ьс производит и счетные действия.
388
Соединение между локальными и удаленными процессами. Серверы и сокеты
используют для передачи данных только в одном направлении2. На рисунке 11.3 показаны два программных канала между Ьс и dc. По верхнему каналу передаются команды для калькулятора, которые подаются на стандартный ввод dc. По нижнему каналу передаются данные от dc к Ьс, со стандартного вывода dc. 3. Постоянный сервис При исполнении программы Ьс будет запущен только один процесс dc. В shell, который мы < написали, для исполнения каждой команды создавался новый процесс. Программа Ьс будет использовать один и тот же интерфейс при каждом новом получении задания (одной строки текста) от пользователя. Такое взаимодействие отличается от стандартного меха низма типа “вызов-возврат” (call-return), который мы используем при функциональных вызовах. Пару bc/dc называют сопрограммами (coroutines), чтобы отличить их от другой пары - под программы (subroutines). В данном случае оба процесса продолжают работать, но управ ление передается от одного процесса к другому по мере того, как каждый из них заканчи вает выполнение своей части задания. Для процесса Ьс заданием является разбор входного текста и вывод результата, а для процесса dc заданием является выполнение арифметиче ских действий.
/1.3.1. Кодирование be: pipe, fork, dup, ехес
На рисунке 11.4 показано, каким образом построена информационная связь в ядре, через которое производится соединение пользователя с процессом Ьс, а процесса Ьс с процессом dc. Возьмем этот рисунок в качестве основы для построения такого кода: (a) Создать два программных каналов. (b) Создать процесс для исполнения программы dc. (c) В новом процессе перенаправить stdin и stdout на программные каналы, а затем вы полнить ехес dc.
2. Некоторые программные каналы используют для передачи данных в прямом и обратном направлениях (см. упражнение 11.11)
11.3. be: калькулятор в UNIX
389
(d) В родительском процессе считать данные от пользователя и произвести их раз борку. Далее выдать через канал команды для dc. Потом прочитать из канала ответ от dc выдать результат пользователю. Далее следует код программы tinybc.c. В этой версии Ьс используется sscanf для проведения разбора входного текста пользователя. Взаимодействие с dc производится через два кана ла: Г* tinybc.c ** ** ** **
* Небольшой калькулятор, где используется dc для вычислений * Демонстрируется работа с двунаправленными каналами * Ввод выглядит так: число операция число * tinybc преобразует ввод в строку: число \п число \п операция \п р * и передает полученный результат на stdout
** +.............. + +............ ** stdin >0 >== pipetodc ====> ** | tinybc | | dc ** stdout <1 <== pipefromdc ==< ** +................ -..................... + +.................................
** ** ** ★* ** ** ** ★* ** •kit
**
* схема программы a. Построить два канала b. fork (породить другой процесс) c. В цепочке dc-to-be между процессами: Перенаправить стандартные ввод и вывод на каналы. Затем выполнить exed dc d. В процессе tinybc образовать: Работу с пользователем через ввод/вывод Посылку команд и результатов через каналы e. После закрытия каналов dc заканчивается * замечание: многострочные ответы не обрабатываются
7 «include <stdio.h> «define oops(m,x) {perror(m); exit(x);) main()
{ int pid, todc[2], fromdc[2]; /* оборудование */ /* образовать два канала */ if (pipe(todc) == -11 pipe(fromdc) == -1) oopsfpipe failed", 1); Г создать процесс для поддержки интерфейса пользователя */ if ((pid = fork{)) ==-1) oopsf'cannot fork", 2); if (pid == 0) Г дочерним процессом будет dc */ be_dc(todc, fromdc); else{ be_bc(todc, fromdc); /* родительским процессом будет ui */ wait(NULL); /* ожидание окончания дочернего процесса */
О
Соединение между локальными и удаленными процессами. Серверы и соке
} } be_dc(int in[2], int out[2])
Г *
установка stdin и stdout, затем: execl dc
7 { Г установка stdin из pipein */ if (dup2(in[0] ,0) — -1) f* дублирование дескриптора 0 на конце для чтения */ oops("dc: cannot redirect stdin",3); close(in[0]); /* пересылка через fd 0 */ close(in[1]); /* писать через этот конец нельзя */ /* установка stdout для pipeout */ if (dup2(out[1 ], 1) == -1) Г дублировать fd 1 для конца на запись */ oopsf'dc: cannot redirect stdout",4); close(out[1]); /* пересылка через fd 1 */ close(out[0]); /* нельзя читать через этот конец */ /* теперь выполнить execl dc с опцией - */ execlp("dc", " d c " , N U L L ) ; oops("Cannot run dc", 5);
} be_bc(inttodc[2], int fromde{2])
Г * Читать из stdin и преобразовать в обратную польскую запись, выдать в канал, затем читать из другого канала. Выдать результат пользователю * Используется fdopenQ для преобразования файлового дескриптора в поток
7 { int num1,num2; char operation[BUFSIZIJ, message[BUFSIZ], *fgets(); FILE *fpout, *fpin, *fdopen(); /* установка */ close(todc[0]); /* не производить чтение из канала к dc */ close(fromdc[ 1 ]); Г не писать в канал из dc */ fpout = fdopen(todc[1], "w”); /* преобразовать файловые дескрипторы */ fpin = fdopen(fromdc[0], "г"); /* в потоки */ if (fpout == NULL || fpin == NULL) fatal("Error converting pipes to streams"); Г основной цикл */ while (printff'tinybc: ”), fgets(message,BUFSIZ,stdin) != NULL){ /* ввод для разборки 7 if (sscanf( message,"%d%[-+7/']%d",&num1 .operation, &num2)!=3){ printf("syntax error\n"); continue;
/ 1. 3. be: калькулятор в UNIX
391
if (fprintf(fpout, "%d\n%d\n%c\np\n", num1, num2, ‘operation) == EOF) fatalf’Error writing"); fflush(fpout); if (fgets(message, BUFSIZ, fpin) == NULL) break; printf("%d %c %d = %s", num1, ‘operation, num2, message);
} fclose(fpout); /* закрыть канал */ fclose(fpin); /* dc увидит EOF */
} fatal(char mess[])
{ fprintf(stderr, "Error: %s\n", mess); exit(1);
) Далее представлены результаты выполнения tinybc: $ сс tinybc.c -о tinybc /tinybc tinybc: 2+2 2+2=4 tinybc: 55“5
55 Л 5 = 503284375 tinybc:
Внимательно изучите результаты работы и определите, что делает каждая часть програм мы. Программа tinybc выдает приглашение и вводит текст арифметического выражения. Результат вычисления - это строка, которая была выработана в dc. Программа tinybc только читает строку результатов из канала и включает ее в вывод.
11.3.2. Замечания, касающиеся сопрограмм Какие еще средства в Unix могут быть использованы в качестве сопрограмм? Можно ли использовать утилиту sort как сопрограмму для некоторой программы? Нет. Утилита sort читает сразу все данные до конца файла, а потом выдает результаты на вывод. Единствен ный вариант - послать признак конца файла через канал, чтобы закрыть записывающий конец. Но после закрытия записывающего конца вы не сможете послать следующую порцию данных на сортировку. dc обрабатывает данные и команды построчно. Взаимодействие с dc простое и предсказуе мое. Когда вы обращаетесь к dc, чтобы напечатать значение, то в ответ вы получаете одну текстовую строку. Когда же вы обращаетесь к dc и передаете значение, вы не получите ни какого ответа. Для программы, которая представлена как часть клиент/серверной сопрограммной систе мы, необходимо использование ясного способа для обозначения конца сообщения. Про грамма должна использовать простые, предсказуемые запросы и ответы.
392
Соединение между локальными и удаленными процессами. Серверы и сокеты
11.3.3. fdopen: файловые дескрипторы становятся похожими на файлы В программе tinybc.c мы ввели в использование библиотечную функцию fdopen. Эта функ ция работает аналогично функции fopen, возвращая FILE *. Но при работе с функцией передается в качестве аргумента файловый дескриптор, а не имя файла. При использовании fopen открывается нечто обозначенное через имя файла. Функция fopen открывает файлы устройств, а также обычные дисковые файлы. Использование fdopen пред полагается в случаях, когда вы располагаете не именем файла, а файловым дескриптором. Например, это необходимо в случае работы с программным каналом. Возможно вы захотите преобразовать соединение в тип FILE * с тем, чтобы можно было использовать далее стан дартные, буферируемые операции ввода/вывода. Посмотрите, как в программе tinybc.c ис пользуются операции fprintf и fgets для передачи данных через каналы к dc. При использовании fdopen удаленный процесс становится более похожим на файл. В сле дующей секции мы поработаем с рореп. Это функция, в которой инкапсулированы вызовы pipe, fork, dup и ехес. Эта функция полностью воссоздает иллюзию, что программы и файлы выполняют одно и то же.
11.4. рореп: делает процессы похожими на файлы Далее мы продолжим обсуждение вопроса, как программа может получить некие сервисы посредством присоединения к другому процессу. Кроме того обсудим возможности биб лиотечной функции рореп. Мы рассмотрим, что делает рореп и как она работает. Затем мы напишем нашу собственную версию.
11.4.1. Что делает функция рореп Функция fopen открывает буферируемое соединение с файлом: RLE *fp; fp = fopen("fi!e1", "г"); с = getc(fp); fgets(buf, len, fp); fscanf(fp,"%d%d%s",&x,&y,x); fclose(fp);
/* указатель на структуру */ /* аргументы: имя файла, тип соединения */ /* посимвольное чтение */ /* построчное чтение */ j* чтение токенов */ Г закрытие после окончания работы */
При обращении к fopen задаются два строковых аргумента: имя файла и тип соединения (т. е. “г”, “w”, “а”,...). Функция рореп во многом выглядит и работает аналогично, функ ции fopen. Функция рореп открывает буферируемое соединение с процессом: FILE *fp; fp = popenf'ls", "г"); fgets(buf, len, fp); pclose(fp);
/* используется такой же тип стурктуры */ /* аргументы: имя программы, тип соединения */ /* точно такое же назначение */ /* закрытие после окончания работы */
На рисунке 11.5 иллюстрируется сходство между функциями рореп и fopen. Обе функции используют один и тот же синтаксис, и возвращают значение одного и того же типа. Первый аргумент при обращении к рореп - это имя команды, которая должна быть “откры та”. Это может быть любая команда shell. Второй аргумент - это символ, которым может
11.4. рореп: делает процессы похожими на файлы
393
Примеры рореп В следующей программе, где рассматривается конвейер who|sort, используется функция рореп для получения отсортированного списка текущих пользователей:
Г popendemo.c * * * * * *
Демонстрируется, как открыть программу для стандартного ввода/вывода Важные моменты: 1. рореп() возвращает RLE *, аналогично функции fopen{) 2. Указатель FILE * можно использовать при записи/чтении со всеми стандартными функциями 3. Вам необходимо выполнить pcIoseQ при окончании
7 «include «include int main()
<stdio.h> <stdlib.h>
{ FILE *fp; char buf[100]; int i = 0; fp = popen("who|sort", "r"); while (fgets(buf, 100, fp) != NULL) printf("%3d %s", i++, buf); Г вывод данных */ pclose(fp); г ВАЖНО! */ return 0;
j* открытие команды */ /* читать из команды */
} Второй. пример использует рореп для соединения программы mail с пользователями, которым выдается сообщение о системной проблеме:
Г рореп_ех3.с * * * *
Показывается, как использовать рореп для записи в процесс данных, считано из stdin. Эта программа посылает email двум пользователям. Заметим, как легко можно использовать fprintf для форматирования данных для посылки сообщения.
7 «include main()
{ FILE *fp;
<stdio.h>
394
Соединениемеждулокальнымииудаленнымипроцессами.Серверыисокеты fp = popenfmail admin backup", "w"); fprintf(fp, "Error with backup!!\n"); pclose(fp);
} Обязательное выполнение pclose Когда вы закончите чтение или запись из соединения, созданного с помощью рореп, необхо димо выполнить pclose, а не fclose. Окончание запущенного процесса должно ожидаться в порождающем процессе. Иначе процесс становится зомби. При выполнении pclose выпол няется wait.
11.4.2. Разработка функции рореп: использование fdopen Как работает рореп и как мы ее будем разрабатывать? Функция рореп запускает программу на исполнение и строит свя^вь со стандартным вводом или выводом этой программы. Нам потребуется новый процесс, чтобы запустить в нем программу. Поэтому мы исполь зуем fork. Нам потребуется соединение с этим процессом. Поэтому мы используем pipe. Нам потребуется файловый дескриптор для буферируемого потока. Поэтому мы использу ем fdopen. Наконец, нам потребуется запускать любую команду shell в новом процессе. Поэтому мы используем ехес. Но что мы будем исполнять? Единственной программой, которая может запускать на исполнение любую команду shell, будет программа самого shell: /bin/sh. Удобно запускать sh с опцией - с. Тем самым мы обращаемся к shell, чтобы он запустил команду, а потом сделал бы exit. Например: sh -с "who|sort"
Мы обращаемся к sh, чтобы он запустил командную строку who|sort (см. также рисунок П.6). Скомбинируем вызовы pipe, fork, dup2 и ехес, как это показано на диаграмме.
pipe(p) fork()
I-
+............................ +............................ + close(p[1]); fp = fdopen(p[0],"r") return fp;
close(p[0]); dup(p[1],1); close(p[1]); execl("/bin/sh","sh","-c",cmd,NULL);
11.4. рореп: делает процессы похожими на файлы
395
В программе рореп.с представлена реализация этой диаграммы: /* рореп.с - версия библиотечной функции рореп() * FILE *popen(char ‘command, char *mode) command - обычная команда shell * mode - символ "г" или "w" * В качестве результата возвращается поток, присоединенный к * команде, или NULL * execls "sh" "-с" command * подумать: можно ли управлять дочерним процессом с помощью сигналов?
7 «include <stdio.h> «include <signal.h> «de'fine READ 0 «define WRITE 1 FILE *popen(const char ‘command, const char ‘mode)
{ int pfp[2], pid;
/* программный канал и процесс */
FILE *fdopen(), *fp; /* fdopen строит fd для потока канала */ int parent_end, child_end; if (‘mode == 'r'){ f* направление передачи */ parent_end = READ; child.end = WRITE; } else if (‘mode == 'w'){ parent_end = WRITE; child_end = READ; } else return NULL; if (pipe(pfp) == -1) Г построить канал */ return NULL; if ((pid = fork()) == -1){ /‘и процесс */ close(pfp[0]); /* или завершить дела с каналом */ close(pfp[1]); return NULL;
}
Г ............................. код процесса-отца..................................... */ /* необходимо закрыть один конец и выполнить fdopen на другом конце */ if (pid > 0){ if (close(pfp[child_end]) == -1) return NULL; return fdopen(pfp[parent end], mode); /‘ тот же режим */
} } /*—.......................... код дочернего процесса.............................*/ Г необходимо перенаправить stdin или stdout, затем выполнить ехес для команды */ if (cHose(pfр[parent_end]) == -1) /* закрыть другой конец */ exit( 1); Г сделать возврат с кодом 1 */ if (dup2(pfp[child_end], child.end) == -1)
396
Соединение между локальными и удаленными процессами. Серверы и сокеты exit(1); if (close(pfp[child_end]) == -1) /* закрыть на этом конце */ exit(1); /* все готово для запуска команды */ execl("/bin/sh", "sh", "-с", command, NULL); exitfl);
} В этой версии рореп не используется механизм сигналов. А в чем проблема?
11.4.3. Доступ к данным: файлы, программный интерфейс АР/ и сервера Счпомощью fopen можно получать данные из файла, а с помощью рореп данные можно получать от процесса. Давайте сосредоточимся на основном вопросе получения данных и сравним при этом три метода. В качестве примера мы сравним три метода получения списка пользователей, которые вошли в систему. Метод 1: получение данных из файлов. Данные можно получить, читая их из файла. В главе 2 мы написали версию программы who, которая читает список текущих пользова телей из файла utmp. . Сервис, основанный на работе с файлами, нельзя признать отличным. Клиентские про граммы зависят от конкретного формата файла и специфических имен членов в струк турах. Строки вида: Г Backwards compatibility hacks. */ #define ut_name ut_user
в заголовочном файле Linux для структуры utmp иллюстрируют, как это реально выглядит. Метод 2: получение данных от функций. Вы можете получить данные, обращаясь для этого к функции. Библиотечная функция скрывает форматы данных и расположение файлав за стандартным функциональным интерфейсом. В Unix есть функциональный интерфейс для файла utmp. Страничка электронного справочника getutent описывает функ ции, с помощью которых можно читать базу данных utmp. Структура памяти может ме няться, но программы, которые будут использовать данный интерфейс, будут продолжать работу. Однако сервисы, основанные на API (Application programming interface-программный ин терфейс), не всегда обеспечивают удовлетворительное решение. Есть два метода исполь зования функций из системных библиотек. Программа может использовать статическое связывание, когда код функции включается в текст. Такие функции допускают использо вание имен файлов или форматов файлов, которые в текущий момент не являются пра вильными. С другой стороны программа может обращаться к функциям, которые находят ся в разделяемых библиотеках. Но эти библиотеки не всегда инсталлированы в системе. Или версия, которая находится в системе, не может быть сопряжена с той версией, которая нужна программе. Метод 3: получение данных от процессов. Третий метод основан на получении данных при чтении из процесса. Примеры bc/dc и рореп показали, как можно создавать соединения с другими процессами. Программа, которая хотела получить список пользователей, может обратиться к рореп, чтобы связаться с программой who. Программа who не является вашей программой. Она проверяет правильность файловых имен и форматов файлов, а также ис пользует необходимые библиотеки. Методу получения данных посредством обращения к конкретным программам присущи еще ряд преимуществ. Серверные программы могут быть написаны на любом языке: это могут быть скрипты, написанные на shell, или отком
/1.5. Сокеты: соединения с удаленными процессами
397
пилированные С-коды. Это могут быть программы на Java или Perl. Наиболее выразитель ное преимущество при реализации сервисных систем в виде отдельных программ за ключается в том, что клиентская программа может работать на одной машине, а серверная программа может работать на другой машине. Все, что необходимо при этом: использо вать некоторый вариант соединения процессов на различных машинах.
11.5. Сокеты: соединения с удаленными процессами С помощью программных каналов процесс может передавать данные другим процессам так же легко, как он посылает данные в файлы. Но каналы имеют два существенных ограничения. Канал создается одним процессом и разделяется с другим после выполнения fork. Поэтому программные каналы могут соединять только родственные процессы. Кроме того, программ ные каналы соединяют процессы, которые развиваются на одной и той же машине. В Unix есть еще и другой метод для межпроцессных взаимодействий - сокеты:
Сокеты предоставляют процессам возможность создавать соединения, которые похожи на каналы. Но ими могут пользовать не обязательно родственные процессы, а даже процес сы, которые развиваются на разных машинах (см. рисунок 11.7). Далее мы изучим основ ные идеи построения и использования сокетов. Посмотрим, как можно использовать соке ты для связывания клиентов и серверов, которые работают на разных машинах. Идея взаимодействия проста и аналогична телефонному звонку, чтобы узнать текущее время.
11.5.1. Аналогия: “....времяравно...” Многие клиенты пользуются таким сервисом - определяют время по определенному теле фонному номеру. Вы набираете этот номер. Автомат, с котбрым вы свяжетесь, сообщает текщее время в конкретном городе. Как это все работает? Что, если вам придет в голову мысль установить собственную службу времени? Вы можете использовать простейшее решение, изображенное на рисунке 11.8. На этом рисунке вы изображены на правой стороне и находитесь в офисе. Вы выступаете в роли сервера, который отвечает на запрос о текущем времени. В офисе на стене находят ся часы. Последовательность действий, которые вы будете следовать при работе в качестве вре менного сервера, в точности соответствует тем шагам, которые необходимо будет сделать в составе службы, использующей сокеты. Поэтому начнем детально описывать эти шаги.
398
Соединение между локальными и удаленными процессами. Серверы и сокеты
Установка и работа сервиса Что нужно установить и как начать работу в вашей службе времени сразу после того, как вы купили и установили часы? Установка сервиса. Установка вашего сервиса состоит из трех шагов. 1. Следует получить телефонную линию Прежде всего, вам необходимо заполучить телефонную линию, к которой вы можете присоединить телефон с помощью розетки на стене. Телефонный провод и розетка по зволят вам подсоединиться к телефонной сети так, чтобы звонки приходили бы на те лефон на вашем столе. Выражаясь более формально, можно сказать, что розетка будет являться оконечной точкой соединения. Когда у вас возникнет необходимость устано вить дома телефон, то обратитесь к телефонной компании или к монтеру с просьбой установить оконечную точку соединения (Это не очередной шаг в установке сервиса. Так автор шутит. - Примеч. пер.). 2. Следует получить телефонный номер для телефонного аппарата на установленной линии Клиентам необходим.номер, чтобы с его помощью подсоединиться к вашей оконечной точке соединения. В телефонной сети идентифицируется каждая настенная телефон ная розетка с телефонным номером. Чтобы продолжить наши аналогии, представим себе, что вы заняты большим бизнесом и в дополнение к службе времени намерены открыть еще ряд служб. Поэтому ваша телефонная розетка будет идентифицироваться телефонным номером и дополнительным номером. Например, вашим номером может быть 617-999-1234. А расширением номера может быть 8080. Телефонный номер будет идентифицировать здание, где находится ваш офис, а расширение номера (8080) будет идентифицировать ваш конкретный телефон в этом здании. Итак, в чем смысл этого дополнения? Один номер предназначен для здания, а второй номер - для вашей службы. Это важно.
11.5. Сокеты: соединения с удаленными процессами
399
3. Распределение входящих запросов Вы можете использовать платные телефоны, на которых будет отметка по incoming calls. Но вашей службе не подходит такой тип телефона. Вы договариваетесь с теле фонной компанией/чтобы по вашей линии было бы можно принимать входящие запро сы. Вам также необходим механизм формирования очереди из входящих запросов. Вам необходим механизм, которое будет сообщать каждому позвонившему, как важен его запрос для вас. А затем включить музыку. Идея использовать очереди имеет прямое от ношение к сокетам, а включение музыки - не имеет никакого отношения. Работа службы. Работа службы времени заключается в выполнении в цикле сле дующих трех шагов: 4. Ожидание звонка (запроса) Следует просто сидеть и ничего не делать до тех пор, пока не поступит вызов (зазвонит телефон). Используя техническую терминологию, вы должны быть заблокированы на звонке. Когда зазвонит телефон, то вы разблокируетесь и принимаете запрос. 5. Обслуживание В нашем случае вы смотрите на часы, а затем сообщаете по телефону позвонившему сколько сейчас времени. 6. Отключение Ваша работа по данному запросу окончена, поэтому вы отключаетесь. Эти шесть шагов, три из которых являются установочными, а три связаны с обслужи ванием запроса, представляют в деталях, как должна работать служба времени через телефонную сеть. Использование сервиса В каком порядке клиент может воспользоваться вашим сервисом? Клиент должен вы полнить следующие четыре шага: 1. Следует получить телефонную линию Клиенту также необходима оконечная точка соединения. Клиент заказывает и получа ет телефонную линию в телефонной сети. 2. Соединение с вашим номером Далее клиент использует линию, чтобы связаться через телефонную сеть с вашей ли нией. Клиент соединяется по бизнес-номеру и расширению, которые идентифи цируют вашу службу. Комбинация бизнес-номера и расширения называется сете вым адресом вашей службы. Используя техническую терминологию, телефонный номер вашего учреждения - это адрес хоста, а номер вашего расширения - это номер порта или просто порт. Относительно рассматриваемого ранее примера адре сом хоста будет 617-999-1234, а портом будет 8080. 3. Использование сервиса Теперь связь между двумя оконечными точками соединения (точка клиента и точка сервера) установлена. Поэтому можно через установленное соединение с любой стороны передавать данные для другой стороны. В случае со службой времени данные через установленное соединение посылает сервер, а клиент принимает эту информа цию. В более развитых сервисах потребуются более сложные взаимодействия между клиентом и сервером. Более сложные сервисы мы рассмотрим позже. 4. Отсоединение Взаимодействие закончено. Клиент отсоединяется.
400
Соединение между локальными и удаленными процессами. Серверы и сокеты
Важные концепции Пример со службой времени проиллюстрировал четыре концепции, которые мы будем ис пользовать при программировании сокетов. Клиент и сервер Эти идеи мы обсуждали уже несколько раз. Сервер - это программа, которая при своей ра боте обеспечивает некоторый сервис. В терминах Unix под сервером подразумевают про грамму, а не компьютер. Когда говорят о компьютере, то обычно употребляют термины: компьютер, хост, система, машина и бокс. Серверный процесс работает в цикле: ожидает запрос, обрабатывает его, затем опять возвращается к шагу приема нового запроса. Кли ентский процесс не является циклическим. Клиент устанавливает соединение, обменива ется данными с сервером и далее продолжает свою работу. Имя хоста и порт С позиций Internet сервер рассматривается как процесс, развивающийся на некотором компьютере. Компьютер называют хостам. Для машин назначают имена, как, например: sales.xyzcorp.com. Такое имя называют hostname (именем хоста). На этом хосте сервер имеет номер порта. Комбинация из имени хоста и порта идентифицирует сервер. Семейство адресов Ваша служба времени имеет телефонный номер. Но эта служба также имеет и адрес ули цы и почтовый индекс (zip code). Ваша служба еще определяется и по определенным ко ординатам: по долготе и по широте. Можно придумать еще и другие числовые способы идентификации. Каждый из таких наборов номеров представляет собой адрес вашей службы. Хотя вы вряд ли будете работать с долготой и широтой, которые будут выступать в роли номера телефона и расширения номера. Каждый из таких адресов принадлежит к семейству адресов. Телефонный номер и рас ширение номера имеют смысл в семействе адресов телефонной сети, что мы будем обо значать символически как AF_PHONE. Аналогично долгота и широта будут иметь смысл для семейства адресов в системе глобальных координат, что мы будем обозначать симво лически как AFJ3LOBAL. Протокол Протокол - это правила взаимодействия между клиентом и сервером. Для службы време ни протокол простой: клиент звонит, сервер отвечает, сервер сообщает точное время, сервер отсоединяется. Что получится, если мы начнем работать со службой DAS (directory-assistance service - спра вочная служба)? В таком случае протокол будет-уже более сложным. Вы, выполняя функции сервера, должны будете отвечать определенным образом. Вначале вы поприветствуете кли ента (“Телефонная справочная служба. Какой город вас интересует?'). Клиент в ответ дол жен назвать имя города. Сервер задает клиенту следующий вопрос, касающийся имени С4Что необходимо найти?”). Клиен;г в ответ называет имя персоны или название учрежде ния. После этого сервер сообщает клиенту искомый телефонный номер или сообщает о том, что искомого объекта в этом городе нет. В некоторых справочных службах могут соединить вас с найденым по запросу телефонным номером (за отдельную плату). Такой обмен сооб щениями и составляет протокол справочной службы DAP (directory-assistance protocol), речь о котором пойдет далее . В каждой клиент/серверной системе должен быть определен свой протокол.
11,5. Сокеты: соединения с удаленными процессами
401
11.5.2. Время Internet, DAP и метеорологические серверы Телефонная служба времени и телефонная справочная служба - это примеры серверов. Но эти примеры носят учебный характер. В Internet дело обстоит несколько не так. Давай те выполним: $ telnet mit.edu 13 Trying 18.7.21.69... Connected to mit.edu. Escape character is 'TМОП Aug 13 22:36:44 2001 Connection closed by foreign host.
$ Где-то на машине MIT располагается сервер времени, который ожидает поступления за просов через порт 13. Когда мы обращаемся к этому серверу с помощью программы telnet, то сервер воспринимает запрос, определяет показание часов в системе, посылает ответ о текущем времени в линию, затем отсоединяется. Все в точности соответствует действи ям, которые мы наблюдали при работе со службой времени. Даже используется тот же протокол. Теперь попытаемся соединиться через порт 13 с другими хостами. В результате вы можете определить, каково текущее время на машинах по всему миру. Программа telnet аналогична телефону. Она устанавливает соединение с портом на удален ном хосте, а затем пересылает данные с вашей клавиатуры через установленное соедине ние. Далее выводит на ваш экран результирующие данные через это же соединение. А что можно сказать относительно телефонной справочной службы? Сервер DAS обычно прослушивает запросы через порт 79. Например, мы можем выполнить: $ telnet princeton.edu 79 • Trying 128.112.128.81... Connected to princeton.edu. Escape character is ,A]\ smith alias:000012345 name:Waldo Smith department: Special Student email:[email protected] emailbox:[email protected] netid :waldos alias: name: department: email: emailbox: netid:
000333333 Ignatz E Smith Undergraduate Class of 1997 [email protected] [email protected] ismith
Сервер принимает запрос, когда он поступит от клиента. В протоколе определено, что клиент должен набрать искомое имя и затем нажать на клавишу return. Сервер посылает клиенту все записи, которые удалось обнаружить, а затем разрывает соединение.
402
Соединение между локальными и удаленными процессами. Серверы и сокеты
А как можно узнать сводку погоды? Попытаемся выполнить: telnet rainmaker.wunderground.com 3000
Протокол для этого метеосервера будет более сложным, но и более дружественным.
11.5.3. Списки сервисов: широко известные порты Каким образом я узнаю, что для доступа к серверу времени необходимо использовать порт 13 , а порт 79 используется для доступа к справочному серверу? Просто нужно знать широко известные порты. Это аналогично ситуации, когда в Соединенных Штатах знают, что в аварийных ситуациях нужно звонить по номеру 911, а для получения справки звонят по номеру 411. В файле /etc/services находится список широко известных сервисов и номера их портов.
$ more /etc/services #
$NetBSD: services,v
1.18 1996/03/26 00:07:58 mrg Exp $
# # Network services, Internet style
# # Note that it is presently the policy of IANA to assign a single well-known # port number for both TCP and UDP; hence, most entries here have two entries # even if the protocol doesn't support UOP operations. # Updated from RFC 1340, "Assigned Numbers" (July 19Э2). Not all ports # are included, only the more common ones.
# #
from: @(#)services
5.8 (Berkeley) 5/9/91
# tepmux echo echo discard discard systat daytime daytime --More--(13%)
1 Дер 7/tcp 7/udp 9/tcp 9/udp 11 Дер 13Дср 13/udp
# TCP port service multiplexer
sink null sink null users
По этому листингу можно найти и убедиться, что сервис daytime имеет порт 13. Изучите содержимое этого файла с целью ознакомления со стандартными службами на машинах Internet. Обратите внимание на записи ftp, telnet, finger и http. Все эти службы, которые работают на хостах Internet, базируются на идеях и технологии, которые были нами рассмотрены на примере службы времени. Теперь преобразуем эти идеи в вызовы Unix с тем, чтобы можно было написать нашу собственную версию сервера времени и клиента времени.
11.5., Сокеты: соединений с удаленными процессами
403
11.5.4. Разработка timeserv.c: сервер времени Работа нашей телефонной службы описывалась последовательностью действий из шести шагов. Каждый шаг соответствует системному вызову. В таблице, которая следует ниже, показано такое соответствие:
системный вызов
действие 1. Получение телефонной линии
socket
2. Связывание линии с номером
bind
3. Готовность к приему запросов
listen
4. Ожидание запроса
accept
5. Передача данных
read/write
6. Отсоединение
close
Вот таким будет код: Г timeserv.c - сервер времени дня, использующий механизм сокетов
7 tinclude <stdio.h> «include «include <sys/types.h> «include <sys/socket.h> «include «include «include «include <strings.h> «define PORTNUM 13000 «define HOSTLEN 256 «define oops(msg) {perror(msg); exit(1);} int main(int ac, char *av[])
Г телефонный номер нашего сервера времени */
{ struct sockaddr_in saddr; struct hostent *hp; char hostname [HOSTLEN]; int sock_id,sock_fd; FILE *sock_fp; char *ctime();
Г здесь строится наш адрес 7 Г это часть нашего 7 /* адреса */ Г идентификатор линии, дескриптор файла 7 Г использовать сокет как поток */ Г преобразование значения времени в секундах в строковое представление 7 time t thetime; /* результирующее время */
Г~ * Шаг 1: обратиться к ядру для построения сокета
7 sockjd = socket(PF_INET, SOCK_STREAM, 0); /* создание сокета */ if (sockjd == -1) oops("socket");
Г * Шаг 2: связывание адреса с сокетом. Адрес - это: хост, порт
7
Соединение между локальными и удаленными процессами. Серверы и сокеты
404
bzero((void *)&saddr, sizeof(saddr)); {* очистить нашу структуру */ gethostname(hostname, HOSTLEN); Г Гдея нахожусь? */ hp = gethostbyname(hostname); Г получить информацию о хосте */ Г заполнить поле хоста */ bcopy((void *)hp->h_addr, (void *)&saddr.sin_addr, hp->h_length); saddr.sin_port = htons(PORTNUM); /* заполнить поле порта сокета */ saddr.sin_family = AF.INET; /* заполнить поле семейства адресов */ if (bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) != 0) oops("bind");
Г * Шаг 3: готовность принимать запросы на сокете в очереди с Qsize=1
7 if (listen(sock_id, 1) != 0) oopsf'listen");
Л **основной цикл: accept(), writeO, close()
7 while (1){ sock_fd = accept(sock_id, NULL, NULL); [* ожидание запроса */ printffWow! got a call!\n''); if (sock_fd == -1) oopsf'accept"); f* ошибка при получении запроса 7 ; sock_fp = fdopen(sock_fd,”w"); f* сюда мы будем писать */ if (sockjp == NULL) /* Сокет как поток 7 oops(”fdopen”); /* если нельзя 7 thetime = time(NULL); /* получить время */ Г и преобразовать в строчное представление */ fprintf(sock_fp, "The time here is.."); fprintff sockfp, "%s", ctime(&thetime)); fclose(sock fp); f* release connection 7
} } Далее представлено пояснение, как работает эта программа.
Шаг 1: обращение к ядру для создания сокета Сокет - это оконечная точка соединения. Сокет, аналогично телефонной розетке на стене, представляет собой место, из которого можно сделать запрос, и место, к которому могут быть направлены запросы. Системный вызов socket служит для создания сокета.
socket НАЗНАЧЕНИЕ
Создание сокета
INCLUDE
#include <sys/types.h> #include < sys/soc ket. h >
ИСПОЛЬЗОВАНИЕ
sockid = socket(int domain, int type, int protocol)
11.5. Сокеты: соединения с удаленными процессами
405
socket АРГУМЕНТЫ
domain- коммуникационный домен PFJNET - для Internet-сокетов type - тип сокета SOCK_STREAM - выглядит как канал protocol - используемый протокол для сокета О - по умолчанию
КОДЫ ВОЗВРАТА . -1 - при обнаружении ошибки sockid - идентификатор сокета, если успех
Системный вызов socket создает оконечную точку для коммуникации. После выполнения возвращает идентификатор сокета. Существуют различные виды коммуникационных сис тем, каждая из которых называется коммуникационным доменом. Internet - это один до мен. Позже мы увидим, что ядро Unix представляет собой другой домен. В Linux под держиваются коммуникации с несколькими другими доменами. Тип сокета определяет тип потока данных, который программа планирует использовать. Тип SOCKJSTREAM ра ботает аналогично двунаправленному программному каналу. Данные, которые записы ваются с одного конца, можно читать с другого конца. Данные при этом рассматриваются как непрерывная последовательность байтов. В следующей главе мы рассмотрим тип SOCK_DGRAM. Последний аргумент, protocol, обозначает протокол, который используется в сетевом коде ядра. Но это не протокол между клиентом и сервером. Значение 0 указывает на использо вание стандартного протокола. Шаг 2: связывание адреса с сокетом. Адрес: хост, порт Следующий шаг заключается в установлении сетевого адреса для нашего сокета. В Inter net домене адрес состоит из имени хоста и номера порта. "Мы не можем использовать порт 33. Он зарезервирован для сервера реального времени. Будем использовать вместо этого порт 13000. Вы вправе выбрать любой номер порта для вашего сервера. Только он не дол жен быть слишком малым по значению и не должен уже использоваться. Порты с мини мальными номерами могут быть использованы только для системных сервисов, а не обычными пользователями. Проверьте в вашей системе, каков диапазон номеров. Для представления номера порта отводится поле в 16 разрядов. Поэтому можно сказать, что портов много. Свойства системного вызова bind::
bind НАЗНАЧЕНИЕ
Связывание адреса с сокетом
INCLUDE
#include <sys/types.h> #include <sys/socket.h>
ИСПОЛЬЗОВАНИЕ
result = bindfint sockid, struct sockaddr *addrp, socklenj addrlen)
АРГУМЕНТЫ
sockid - идентификатор сокета addrp - указатель на структуру, содержащую адрес addrlen - длина структуры
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки 0 - .если успех
Системный вызов bind назначает адрес для сокета. Адрес назначается для такой же цели. Он подобен телефонному номеру, по которому распознается телефонная розетка на стене вашего офиса. Процессы будут использовать адрес сокета, когда они пожелают связаться с вашим сервером. В каждом семействе адресов есть свой формат. Семейство Internet-адресов
406
Соединение между локальными и удаленными процессами. Серверы и сокеты
(AFJNET) использует в качестве адреса имя хоста и номер порта. Адрес - это структура, в которой в качестве членов указаны имя хоста и номер порта. Наша программа сначала обну ляет структуру, затем заполняет поле хоста и поле порта. Наконец, заполняется поле семейст ва адресов. Обратитесь к справочнику, чтобы получить информацию о функциях, которые были использованы для формирования каждого из членов структуры. После того, как были заполнены все требуемые части адреса, полученный адрес присоединяется к сокету. В других типах сокетов используются адреса с другими членами.
Шаг 3: готовность принимать запросы на сокете в очереди размером size = 1 Сервер принимает входящие запросы. Поэтому наша программа должна выполнить сис темный вызов listen: listen НАЗНАЧЕНИЕ
Установить готовность соединения на сокете принимать запросы
INCLUDE
tinclude <sys/socket.h>
ИСПОЛЬЗОВАНИЕ
result = listen(int sockid, int qsize)
АРГУМЕНТЫ
sockid - идентификатор сокета, который будет принимать запросы qsize - максимальное число запросов на установление связи
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки 0 - если успех
Системный вызов listen сообщает ядру о готовности определенного сокета принимать вхо дящие запросы. Не все типы сокетов могут принимать входящие запросы. Тип SOCK STREAM может. Второй аргумент в системном вызове определяет размер очереди входящих запросов. В нашем коде мы затребовали очередь, которая вмещает только один запрос. Максимальный размер очереди зависит от реализации сокета.
Шаг 4: ожидание поступления и прием запроса После того как сокет был создан, ему приписан адрес, подготовлен для приема входящих запросов, программа готова к работе. Далее сервер ждет, пока поступит запрос. Для этого используется системный вызов accept: accept НАЗНАЧЕНИЕ
Получить соединение на сокете
INCLUDE
#include <sys/types.h> #include <sys/socket.h>
ИСПОЛЬЗОВАНИЕ
fd = accept(int sockid, struct sockaddr *callerid, socklen_t *addrlenp)
АРГУМЕНТЫ
sockid - принять запрос н§ этом сокете callerid - указатель на структуру с адресом вызывателя addrlenp - указатель на ячейку, где находится длина адреса вызывателя
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки fd - файловый дескриптор, открытый на чтение и запись
Системный вызов accept задерживает текущий процесс до тех пор, пока не появится входя щее соединение на указанном сокете, accept возвращает файловый дескриптор, который от крыт на чтение и запись. Этот файловый дескриптор является соединением с файловым де скриптором вызывающего процесса. Системный вызов accept поддерживает формат иденти
11.5. Сокеты: соединения с удаленными процессами
407
фикатор вызывателя (caller ID). Сокет в вызывателе имеет адрес. Для Internet-соединений в качестве адреса используется имя хоста и номер порта. Если callerid и addrlenp ненулевые, то ядро помещает адрес вызывателя в структуру, на которую указывает callerid, а длину этой структуры помещает в ячейку, на которую указывает addrlenp. Подобно человеку, который по звонку абонента может принять некое решение о дальнейшем разговоре, сетевая программа может использовать адрес запрашивающего процесса для принятия какого-либо решения при управлении входящим запросом. Шаг 5: передача данных Файловый дескриптор, который возвращается после выполнения accept, является обычным файловым дескриптором. К нему применимо все то, что мы узнали при рас смотрении open в главе 2. В программе timeserv.c мы используем fdopen, чтобы преобразо вать этот файловый дескриптор в поток. Поэтому мы можем использовать fprintf. Мы мог ли бы также использовать и вызов write. Шаг 6: закрытие соединения Файловый дескриптор, который был получен после выполнения accept, может быть закрыт с помощью стандартного системного вызова close. Когда один процесс закроет свой конец на сокете, то процесс на другом конце увидит признак конца файла, если он попытается прочитать данные. Программные каналы работают аналогично.
11.5.5. Проверка работы программы timeserv. с Откомпилируем и запустим программу нашего сервера времени:
$ сс timeserv.c -о timeserv $ timeserv & 29362
$ Мы стартовали наш сервер, указав после команды знак амперсанта. Поэтому shell запус тит сервер, но не выполнит вызова wait. Сервер будет заблокирован на системном вызове accept. Мы можем соединиться с ним с помощью telnet:
$ telnet 'hostname' 13000 Trying 123.123.123.123 Connected to somesite.net Escape character is ,A]\ Wow! got a call! The time here is..Tue Aug 1411:36:30 2001 Connection closed by foreign host.
$ $ telnet'hostname' 13000 Trying 123.123.123.123 Connected to somesite.net Escape character is,A]’. Wow! got a call! The time here is'.Tue Aug 1411:36:53 2001 Connection closed by foreign host,
t
Соединение между локальными и удаленными процессами. Серверы и сокеты
408
Мы установили два соединения, и сервер в ответ выдал каждый раз по запросу точное вре мя. Сервер будет работать до тех пор, пока мы не убьем его:
$ kill 29362 telnet работает в данном случае как клиент для этого сервера. Но это не всегда приемлемый вариант для связи с сервером. Далее мы напишем специального клиента для этого сервера.
11.5.6 Разработка программы timednt.c: клиент времени В нашем клиенте для телефонной службы времени были представлены четыре Шага его деятельности. Причем каждому соответствует свой системный вызов:
Системный вызов
Действие 1. Получить телефонную линию 2. Вызвать сервер
socket 'connect
3. Передать данные
read/write
4. Отсоединиться
close
Вот каким будет код: /* timednt.c - клиент для timeserv.c * обращение: timeclnt имя хоста номер порта
7 «include <stdio.h> «include <sys/types.h> «include <sys/socket.h> «include «include «define oops(msg) main(int ac, char *av|])
{perror(msg); exit(1);}
{ struct struct int s char int
sockaddrjn servadd; /* номер запроса 7 hostent *hp; /* используется для получения номера 7 ockjd, sockfd; /* сокет и файловый дескриптор fd 7 message[BUFSIZ]; /* место для приема сообщения */ messlen; /* длина сообщения */
•Г
' Шаг 1: получить сокет 7 sock_id = socket(AF_INET, SOCK_STREAM, 0); /* получить линию 7 if (sockjd ==-1) oopsf'socket"); Г или неудача 7
Г *
Шаг 2: связь с сервером
4 * потребуется сначала создать адрес сервера (хост, порт)
7 bzero(&servadd, sizeof(servadd)); /* обнулить адрес 7 hp = gethostbyname(av[1 ]); /* поиск ip # хоста 7 if (hp == NULL) .
11.5. Сокеты: соединения с удаленными процессами
409
oops(av[1]); /* или закончить */ bcopy(hp->h_addr, (struct sockaddr *)&servadd.sin_addr, hp->h_length); servadd.sin_port = htons(atoi(av[2])); /* занести номер порта */ servadd.sinjamily = AF_INET; /* занести тип сокета */ /* теперь вызов */ if {con neCt(sock_id, (struct sockaddr *)&servadd, sizeof(servadd)) !=0) oops("connect”);
Г * Шаг 3: передача данных от сервера, затем отсоединиться
*/ messlen = read(sock_id, message, BUFSIZ); /* чтение */ if (messlen == -1) oops("read"); if (write(1, message, messlen) != messlen) /* и запись */ oopsfwrite"); /* на stdout */ close(sock id);
}' Далее следует поясненйе, как работает программа.
Шаг 1: обращение к ядру для создания сокета Для соединения с сетью клиенту необходим сокет. Точно так же, как клиенту нашей теле фонной службе времени была нужна телефонная линия для соединения с телефонной се тью. Сокет дЛя клиента должен быть в качестве Intemet-соквта (AFJNET) и быть потоко вым сокетом (SOCK_STREAM).
Шаг 2: соединение с сервером Клиент соединяется с сервером времени. Есть системный вызов connect, который является сетевым эквивалентом телефонного вызова.
connect НАЗНАЧЕНИЕ
Соединение с сокетом
INCLUDE'
#include <sys/types.h> #include <sys/socket.h>'
ИСПОЛЬЗОВАНИЕ
result = connect(int sockid, struct sockaddr *serv_addrp, socklenj addrlen);
АРГУМЕНТЫ
sockid - сокет, используемый для соединения serv_addrp - указатель на структуру, где находится адрес сервера addrlen - длина этой структуры
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки 0, если успех
При выполнении connect предпринимается попытка связать сокет; заданный идентифика тором sockid, с сокетом по адресу, на который указывает serv addrp. Если попытка была успешной, то вызов connect возвращает 0. В этом случае sockid будет рассматриваться как действующий файловый дескриптор, который открыт на чтение и запись. При записи в этот файловый дескриптор данные передаются сокету на противоположный конец соединения. Данные, которые будут записываться на одном конце, могут быть считаны с помощью файлового дескриптора на другом конце.
410
Соединение между локальными и удаленными процессами. Серверы и сокеты
Шаги 3 и 4: передача данных, а затем отсоединение После успешного выполнения connect процесс может читать и писать данные через файло вый дескриптор, как в случае, если бы он был соединен с обычным файлом или программ ным каналом. В клиент/серверной службе времени программа timeclnt просто читает одну строку, поступившую от сервера. Клиент, после прочтения значения времени, выполняет close в отношении файлового дескриптора и заканчивается. Если клиент неожиданно закончится, то тогда ядро закроет открытый им файловый дескриптор.
11.5.7. Проверка работы программы timednt.c Далее мы не увидим картинок на нескольких страницах. Достаточно посмотреть на простой рисунок 11.9 и вспомнить о чем идет речь. Серверный процесс развивается на одном ком пьютере. Клиентский процесс на другом компьютере связывается через сеть с сервером. Сервер посылает данные клиенту с помощью вызова write. Клиент принимает это сообщение с помощью вызова read.
Для полной проверки созданной нами программной системы необходимо запустить две программы на разных машинах. Я не уверен, что это будет наглядно выглядеть в книге, но все же проверка может быть проведена так: $ hostname computed .mysite.net $ сс timeserv.c -о timeserv $ ./timeserv & [1] 10739
# определить имя текущей машины # первая машина # создание сервера # и запуск его
$ $ scp timeclnt.c bruce@computer2: # передать куда-либо код кед клиента bruce@computers2’s password: timeclnt.c 11 KB 11.8 kB/s | ETA: 00:00:001100% $ ssh bruce@computer2 bruce@computer2's password: No mail. computed: bruce$ cc timeclnt.c -o timeclnt
11.5. Сокеты: соединения с удаленными процессами
411
computer2:bruce$ ./timeclnt computerl 13000Wow! got a call! The time here is ..Tue Aug 14 02:44:31 2001 computer2:bruce$ Сервер был откомпилирован и запущен на computerl. Затем я скопировал программный код клиента на компьютер computer2 и открыл сессию на компьютере computer2. На компьютере computed я откомпилировал программу клиента и обратился к программе с запро.сом на соединение с сервером, который работает на computerl, для подсоединения к порту 13000. Сообщение, которое я увидел, было послано через сеть от сервера на computerl к клиенту на computed. А клиент уже посылает сообщение на стандартней вывод. Действительно ли я получил вывод от computer2? Я подсоединен к computed,чработая на computerl. Поэтому терминал, на котором появилось сообщение, реально подсоединен к computerl. Обратитесь к упражнению, где вам предлагается подумать о том, что реально происходит? Программы timeserv/timeclnt дают нам возможность узнать значение времени, которое опрашивается на другом компьютере. Проверка значения времени на другом компьютере также позволяет проводить синхронизацию часов компьютера. Одна из машин в сети мо жет отвечать за измерение времени. Другие машины могут использовать этот вид клиент/ серверной системы для периодической переустановки их часов.
11.5.8. Другие серверы: удаленный Is Нашим следующим проектом будет разработка программы, которая выводила бы списки файлов на удаленном компьютере. Вы должны быть зарегистрированы на двух системах. Что нужно сделать, если вам понадобится получить список файлов на другой машине? Вы должны открыть сессию на другой машине и запустить на исполнение команду Is. Более быстрым, более удобным методом будет использование программы ”удаленная Is”, которая будет названа rls. При обращении к ней вы должны будете задать имя хоста и имя каталога:
$ rls computer2.site.net/home/me/code Естественно, для работы rls нужен серверный процесс, который должен развиваться на другой машине и принимать запросы. После отработки запроса сервер возвращает ответ на запрос. В данном случае система будет выглядеть так, как показано на рисунке Н Л О . На одном компьютере работает сервер. Клиент на другом компьютере связывается с сервером и посылает ему имя каталога. В ответ сервер передает клиенту список файлов в этом каталоге. Клиент отображает этот список, выдавая его на стандартный вывод. Такая система, состоящая из двух процессов, обеспечивает доступ к каталогам на другом ком пьютере.
412
Соединение между локальными и удаленными процессами. Серверы и сокеты
Планирование работы системы remote Is (удаленная Is) Для реализации системы rls нам потребуется три составляющих: (a) протокол (b) клиентская программа (c) серверная программа
Протокол Протокол состоит из запроса и ответа. Сначала клиент посылает одну строку, в которой указано имя каталога. Сервер читает эту строку. Далее сервер открывает и читает указан ный каталог. После чего посылает клиенту список файлов. Клиент построчно читает спи сок файлов до тех пор, пока сервер не закроет соединение, что вызовет выработку призна ка конца файла.
Клиент: rls /* rls.c - клиент службы “remote Is" * использование: rls имя хоста каталог .
7 «include <stdio <sys/types.h> «include <sys/socket.h> «include «include «include «define oops(msg) PORTNUM «define main(int ac, char *av[])
{perror(msg); exit(1);} 15000
{ struct sockaddr_in servadd; Г номер вызова 7 Г структура для получения номера 7 struct hostent *hp; Г сокет и файловый дескриптор fd 7 int sock id, sock fd; char buffer[BUFSIZ]; Г буфер для приема сообщения */ Г длина сообщения */ int n_read; if (ac !=3)exit(1); Г Шаг 1: получить сокет **/ sock_id = socket(AF_INET, SOCK.STREAM, 0); /* получить линию 7 if {sock_id == -1) oops("socket"); I* или неудача 7 Г Шаг 2: соединение с сервером **/ bzero(&servadd, sizeof (servadd)); Г обнулить адрес 7 hp = gethostbyname(av[1]); /* поиск ip # хоста 7 if (hp == NULL) oops(av[1]); Г или окончание */ bcopy(hp->h_addr, (struct sockaddr *)&servadd.sin_addr, hp->h_length); servadd.sin_port = htons(PORTNUM); /* занести в данное поле номер порта 7 servadd.sin_family =• AF_INET; /* занести в данное поле тип сокета 7 if (connect(sock_id,(struct sockaddr *)&servadd, sizeof(servadd)) !=0) oopsfconnecf); /** lilar 3: поглять имя каталога, затем ппочитать полученный пезульта **/
11.5. Сокеты: соединения с удаленными процессами
413
if (write(sockjd, av[2], strlen(av[2])) == -1) oops("write"); if (write(sockjd, 1) == -1) oops("write"); while((njead = read(sock_id, buffer, BUFSIZ)) > 0) if (write(1, buffer, njead) == -1) oops("write"); close(sock_id);
} Обратите внимание на разницу между этим клиентом и клиентом службы времени. Клиент rls сначала записывает имя каталога в сокет. В соответствии с протоколом, клиент должен посылать строку. Поэтому клиентом использован при посылке символ newline. Далее клиент входит в цикл, производя копирование данных от сокета на стандартный вы вод, пока не будет обнаружен признак конца файла. Программа rls.c использует низкоуров невые вызовы write и read для передачи данных между клиентом и сервером. В цикле используется стандартный буфер, который должен иметь подходящий размер. Далее мы напишем сервер.
Сервер: rlsd Сервер получает сокет, выполняет bind, listen, а затем accept, чтобы воспринять запрос. После приема запроса сервер читает имя каталога из сокета. Потом он формирует список файлов из указанного каталога. Как сервер получает список файлов из каталога? Мы мог ли бы скопировать нашу версию команды Is из главы 3. Но мы можем также использовать более простой Метод - просто используем рореп, чтобы прочитать вывод, который будет получен при работе обычной команды Is (см. рисунок 11.11).
Использование popen ("Is") для получения списка файлов из удаленных каталогов В последующем коде используется рореп по отношению к этому концу: Г rlsd.c - сервер “remote Is”- без паранойи
7 tinclude <stdio.h> «include tinclude <sys/types.h>
4
Соединение между локальными и удаленными процессами. Серверы и сокеты «include <sys/socket.h> «include «include «include «include <strings.h> «define PORTNUM 15000 «define HOSTLEN 256 «define oops(msg) {perror(msg); exit(1);} int main(int ac, char *av[])
Л порт нашего сервера “remote Is"*/
{ struct sockaddrJn saddr; /* сюда будет помещен адрес */ struct hostent *hp; /* это часть нашего */ char hostname[HOSTLEN]; /*адреса*/ int sock_id,sock_fd; /* идентификатор линии, файловый дескриптор */ FILE *sock_fpi, *sock_fpo; /* потоки для ввода и вывода */ FILE *pipe_fp; /* использовать рореп для запуска Is */ char dirname[BUFSIZ]; /* из клиента */ char command[BUFSIZ]; /* для рореп() */ int dirlen, с; /** Шаг 1: обратиться к ядру для образования сокета **/ sock_id = socket(PF_INET, SOCK_STREAM, 0); /* получить сокет */ if {sock_id == -1) oopsC’socket"}; /** Шаг 2: связать адрес с сокетом. Адрес: имя_хоста, порт **/ bzero((void *)&saddr, sizeof(saddr)); /* очистить структуру */ gethostname(hostname, HOSTLEN); /* где я нахожусь? */ hp = gethostbyname(hostname); /* получить информацию о хосте */ bcopy((void *)hp->h_addr, (void *)&saddr.sin_addr, hp->h_length); saddr.sin_port = htons(PORTNUM); /* установить номер порта сокета */ saddr.sin_family = AFJNET; /* установить семейство адресов */ if (bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) 1= 0) oops("bind"); /** Шаг 3: готовность принимать входящие запросы на сокете в очереди с размером Qsize=1 **/ if (listen(sock_id, 1) != 0) oops(’Tistenn);
Г * основной цикл: accept(), write(), close()
*/ while (1){ sockfd = accept(sock_id, NULL, NULL); /* ожидать запрос */ if(sock_fd==-1) oops("accept''); /* открыть конец на чтение, как буферируемый поток */ if((sock_fpi = fdopen (sock_fd, "г”)) == NULL) ~ oops(”fdopen reading"); if (fgets(dirname, BUFSIZ-5, sock.fpi) == NULL)
/1.5. Сокеты: соединения с удаленными процессами
415
oopsfreading dirname"); sanitize(dirname); Г открыть записывающий конец, как буферируемый поток */ if ((sock_fpo = fdopen{sock_fd,"w")) == NULL) oopsf’fdopen writing"); sprintf(command,"ls %s", dirname); if ((pipe_fp = popen(command, "r")) == NULL) oopsfpopen"); /* передача данных от Is к сокету */ while((c = getc(pipe_fp)) != EOF) putc(c, sock_fpo); pdose(pipe_fp); fclose(sock_fpo); fclose{sock fpi);
} > sanitize(char *str)
Г * Будет очень плохо, если кто-либо передаст имя каталога в формате: *rm *", а мы наивно сочтем этот текст как команду "Is; rm *" * Поэтому... это приведет к удалению всех файлов ic
* Более надежное решение следует реализовать в качестве упражнения
7 { char *src, *dest; for (src = dest = str; *src; src++) if (*src == 7 || isalnum(*src)) *dest++ = *src; *dest = '\0';
} Следует заметить, что наш сервер использует стандартные буферируемые потоки для-чтения и для записи. Сервер использует fgets для чтения имени каталога от клиента. После вызова рореп сервер передает данные, используя для этого getc и putc, что аналогично рабо^ те по копированию файла. На самом же деле сервер копирует данные от одного процесса к другому, который находится на другом компьютере. Обратите внимание на функцию sanitize. Любой сервер, который запускает команды с использованием аргументов и данных, которые он принимает через Internet, должен быть написан очень аккуратно. Наш сервер ожидает приема от клиента имени каталога .Далее сервер присоединяет этот принятый текст к команде Is. Например, если клиент по сылает строку "/bin", то наш сервер создаст и выполнит строку "Is /bin”, которая является правильной. Однако если кто-либо пошлет серверу строку в и д а r m то наш сервер соз даст и выполнит строку "Is; rm Для уменьшения риска появления ошибок наша программа преполагает, что строка, которую она принимает, не выйдет за пределы буфера; не будет переполнения буфера для команды; не будут использованы специальные символы в имени каталога. Конечно, по следнее ограничение, которое требует использования в имени только алфавитно-цибро-
416
Соединение между локальными и удаленными процессами. Серверы и сокеты
вых символов, слишком сильное. Использование функции рореп при построении сетевого сервиса слишком рискованно, поскольку она передает строку для shell. Идею передачи строк для shell при создании сетевых служб следует считать устаревшей идеей. Я включил этот пример по двум соображениям. Во-первых, чтобы показать еще один вариант исполь зования рореп. И во-вторых, чтобы предупредить вас об опасности такого использования. Это представляется важным.
11.6. Программные демоны Серверные программы, как большая часть программ, имеют короткие, выразительные име на. Многие серверные программы имеют в конце имени символ d. Например, httpd, inetd, syslogd, atd. С помощью символа ^/указывается, что данная программа является демоном. Таким образом, syslogd идентифицирует программу которая является демоном системного журнала [system log daemon). Термин “демон” обозначает спиритического слугу, который в любой момент выступает для вас в роли некого паранормального помощника. Он постоянно витает, ожидая возможности прийти вам на помощь. В вашей системе нужно набрать для исполне ния команду ps -el или ps -ах, чтобы посмотреть процессы, в которых исполняются програм мы с именами, которые заканчиваются символом d. В справочнике помещена информация об этих командах. Кроме того, вы можете узнать дополнительную информацию о вариантах использования в Unix базовых операций при программировании клиент/серверных прило жений. Большая часть демонов стартует, когда начинает работать система. Скрипты shell в ката логе с именем вида /etc/rc.d3 стартуют в фоновом режиме. Они работают с отсоединенными терминалами, готовые производить обработку данных или поддерживать какой-то сервис.
Заключение Основные идеи •
•
•
•
• •
Некоторые программы при исполнении представляют отдельные процессы, которые выдают и принимают данные. В системе клиент/сервер серверный процесс произво дит обработку данных или поставляет некоторые данные для клиентских процессов. В системе клиент/сервер имеются средства связи и протокол. Клиенты и серверы могут взаимодействовать через программные каналы или сокеты. Протокол представляет собой набор правил для ррганизации общения между клиентом и сервером. Библиотечная функция рореп может выполнить произвольную команду shell в серверной программе. В результате доступ к серверу представляется таким, будто это буферируемый доступ к файлу. Программный канал доступен для использования через пару связанных файловых дескрипторов. Клиентский процесс создает коммуникационную линию посредством соединения своего сокета с сокетом сервера. Соединения между сокетами могут быть установлены, когда они находятся на разных машинах. Каждый сокет идентифицируется номером машины и номером порта.* При установлении соединений с помощью программных каналов и сокетов исполь зуются файловые дескрипторы. Файловое дескрипторы дают программе простой интерфейс для связи с файлами, устройствами и другими процессами.
3. Конкретное имя каталога зависит от версии Unix.
417
Что дальше? В этой главе мы рассмотрели программный проект, где была реализована модель клиент/ сервер. Мы рассмотрели два метода для связывания процессов: программные каналы и со кеты. В следующей главе мы сфокусируем внимание на принципах проектирования, которые используются в клиент/серверном программировании. Мы напишем при этом более сложные приложения. В частности, мы объединим программирование сокетов с на шим знанием файловых систем и средств управления процессами и напишем Web-cepeep
Исследования 11.1 Протокол доставки пиццы. Что произойдет, если вы решили вместо службы времени или справочной службы заняться бизнесом по доставке пиццы? В данной службе про токол будет более сложным. Опишите последовательность сообщений, которые пере даются между клиентом, и сервером в службе по доставке пиццы. Заметьте, что этот протокол содержит цикл, который дает возможность клиенту добавить несколько пунктов к порядку доставки. 11.2 Сигналы и функция рореп. Версия рореп, которая была представлена в тексте, не ори ентирована на сигналы. Правильно ли это? Дочерний процесс наследует диспозиции по управлению сигналами от процесса-отца: убить процесс, проигнорировать сигнал, вызвать функцию по обработке сигнала. 11.3 Поток данных при проверке работы программы timeserv. Пример запуска сервера и клиента службы времени показал, что я использовал ssh для получения доступа к computed с компьютера computerl. С помощью этого shell я откомпилировал и запустил на исполнение клиента. Мой терминал на самом деле присоединен к компьютеру computerl. Перерисуйте рису нок 11.11 так, чтобы на нем был показан мой shell на компьютере computerl, мой shell на компьютере computed, мой терминал и корректный поток данных от клиента timeclnt к моему терминалу. Получится достаточно сложный поток , не так ли? 11.4 Сокеты не являются файлами. Мы уже предварительно убедились, что дисковые файлы и файлы устройств поддерживают один и тот же стандартный файловый ин терфейс. Но соединения с дисковыми файлами имеют один набдр свойств, а соедине ния с файлами устройств имеют другой набор. А какие специальные свойства будут характерны для сокетов? Обратитесь за деталями к справочнику на странице setsockopt. 11.5 Серверы и stderr. Сервер “remote Is” при работе запускает команду Is. Что произойдет, если при выполнении команды Is будет обнаружена ошибка? Рассмотрите две особен ности управления сообщениями об ошибках. Во-первых, следует решить вопрос - как вы будете посылать клиенту сообщения об ошибках? Во-вторых, как вы будете запи сывать ошибочные сообщения в журнал и сообщать пользователю о возникшей про блеме?
Программные упражнения 11.6 Добавьте опцию -с в программу tinybc. После добавления этой опции должен работать вот такой конвейер: printf "2 + 2\п4 * 4\п" | tinybc -с | dc 11.7 Добавьте опцию -с для вашего shell. Какие для этого потребуются изменения?
Соединение между локальными и удаленными процессами. Серверы и сокеты
418
11.8 Напишите функцию pclose. При обращении к данной функции задается аргумент RLE*,
что является результатом работы рореп. Функция выделяет память для буфера и для учетных деталей. Функция fclose освобождает эту память и закрывает соответст вующий файловый дескриптор. Что должна делать по аналогии функция pclose? Что случится, если дочерний процесс погибнет между вызовом рореп и вызовом pclose? 11.9 Идентификатор вызывателя. Наш сервер службы времени не использовал возмож ность работы с идентификатором вызывателя, которая поддерживается в системном вызове accept. Модифицируйте программу timeserv.c так, чтобы она по мере поступле ния запроса выдавала бы сообщение в виде: Got a call from 123.123.123.123 (computer2.mysite.net).
Обратитесь к справочнику и соответствующим заголовочным файлам для изучения тех функций и структур, которые вам понадобятся при выполнении этого проекта. 11.10 Напишите программу, которая использует sort в качестве подпрограммы. Ваша про грамма должна читать строки данных в массив строк. Затем программа должна создать два программных канала и процесс для запуска sort. Пошлите последовательность строк на вход sort через один канал. Затем закройте этот канал. Прочитайте результаты, полученные в результате работы sort, через другой канал. Перешлите результаты в мас сив. Выведите содержимое массива на экран. 11.11 Двунаправленные программные каналы. В версиях Unix, которые поддерживают кон цепции System V, используются двунаправленные программные каналы. Вы можете проверить, поддерживаются ли в вашей версии Unix такие каналы, с помощью запуска такой программы: /* * testbpd.c - проверка наличия двунаправленных каналов
*/ main()
{ int р[2];
if (pipe(p) == -1) exit(1); if (write(p[0], "hello", 5) == -1) perrorf'write into pipe[0]failed"); else printf("write into pipe[0] worked\n");
} I
Внутренняя структура такого канала такова, что поддерживаются две очереди. Одна строится от pipe[0] к pipe[l], а другая - в противоположном направлении. Запись дан ных через один конец канала добавляет данные к очереди, которая направлена к друго му концу канала. А чтение данных с одного конца канала приводит к извлечению дан ных из очереди, которая направлена к другому концу канала. Если ваша система не поддерживает двунаправленные каналы, то вы можете создать такую пару: «include <sys/types.h> #include <sys/socket.h> int apipe[2]; /* канал */ socketpair(AF_UNIX, S0CK_STREAM, PFUNSPEC, apipe);
1
Перепишите программу tinylx.c так, чтобы она использовала один двунаправленный ппогпаммный канал, а не два однонаправленных канала.
Заключение
419
11.12 Блокировка IP. Модифицируйте программу timeserv.c так, чтобы она отвечала на кли ентские запросы только по определенному IP хоста. Сервер должен принимать запрос и проверять адрес клиента. Если клиент указывает недопустимый адрес, то сервер разрывает соединение. В противном случае сервер посылает клиенту сообщение о те кущем времени. Расширьте это блокирующее свойство так, чтобы сервер мог читать список доступных номеров IP из файла. Опишите, какие вы знаете практические приложения, которые используют такую технику. 11.13 Увеличение безопасности. Использование рореп в сервере является весьма рискован ным. Есть два способа уменьшить степень риска. Во-первых, можно написать более гибкую, более безопасную версию функции sanitize. Например, не возникает проблем с именами каталогов, в составе которых содержатся символы точки, тире, пробела, многие другие символы. Но в именах каталогов могут содержаться символы звездочки и точки с запятой. А для этих символов установлен особый смысл в shell. Напишите более приемлемую функцию, которая увеличивает безопасность. Другой метод - не использовать рореп, а вместо нее использовать fork, ехес, dup и т. д. Перепишите программу rlsd.c на основе данного предложения. Понадобится ли вам при этом использовать wait? Почему да или почему нет? 11.14 Finger-cepeep. Напишите версию сервера для телефонной справочной службы, к ко торой мы обращались через порт 79. Сервер должен принимать в одной строке пользо вательское имя, а посылать в ответ клиенту список записей, которые были найдены по его запросу. 11.15 Прокси сервера службы времени. Прокси - это программа, которая принимает ваше требование, передает его на другой сервер, а затем пересылает вам ответ от этого сервера. Все происходит аналогично как работает приемный пункт химчистки. Здесь одежду только принимают, но не чистят. Одежда пересылается на предприятие для чи стки, а потом забирается оттуда и выдается клиенту. Напишите прокси для сервера службы времени. Ваша программа должна принимать запросы на соединения через стандартный порт. Для выполнения соединения ваша программа должна открыть соединение с сервером “реального” времени, должна по лучить значение времени от сервера и послать полученное значение обратно вашему клиенту. 11.16 Прокси и кеши. Прочтите об использовании прокси-серверов в предшествующем пункте. Время изменяется с дискретностью один раз в секунду. Поэтому, если ваш прокси-сервер получает много запросов, поступающих через миллисекундные отрезки времени, то нет резона на каждый запрос от клиента обращаться к серверу. Напишите кеш-прокси-сервер для службы времени, сохраняющий значение времени, которое он считал от сервера. Он обращается к серверу, только если предварительно считанная строка от этого сервера хранится у него более одной секунды (см. gettimeofday). 11.17 Еще о кешировани и прокси. Кеширование для сервера службы времени, которое было рассмотрено в предшествующем упражнении, довольно простая идея. Объясни те, почему можно использовать кеширования для finger-сервера. Напишите fingerсервер, который поддерживает кеш с пользовательской информацией. Сервер службы времени имеет вполне очевидный срок хранения отдельного элемента информации в кеше. А сколько времени следует хранить пользовательскую информа цию в кеше для finger-сервера?
420
Соединение между локальными и удаленными процессами. Серверы и сокеты
HAS Сервер для “выпечки” номеров. В некоторых булочных-пекарнях есть автомат, ко торый выдает номера покупателей. Обозначение на счетчике, означающее "сейчас об служивается”, будет отображать номер следующего покупателя, который будет обслу жен. Разработайте клиент/серверную систему для “выпечки ” номеров. Сервер выраба тывает последовательные номера. Пользователь запускает клиентскую программу для получения номера от сервера. 11.19 Использование рореп Каждый С-программист знает, что в argv [0] обычно содержится имя программы, которая запущена. Есть и другой, достаточно экзотический способ, с помощью которого процесс может получить имя своей исполняемой программы. Про грамма может использовать рореп и в выводе ps отыскать программу с собственным иден тификатором процесса. Напишите программу, которая использует этот метод.
Глава 12 Соединения и протоколы. Разработка Web-сервера
Цели Идеи и средства • • • • •
Сокеты на сервере: цель и устройство. Сокеты на клиенте: цель и устройство. Протокол системы клиент/сервер. Проект сервера: использование fork для множества запросов. Проблема зомби. HTTP.
12.1. В центре внимания - сервер Использовать World Wide Web (глобальную паутину) достаточно просто. Следует набрать Web-адрес в броузере или кликнуть на ссылке. И с удаленного компьютера будет доставлена Web-страница. А как работает Web? Передача страницы от Web-сайта производится таким же образом, как проходила передача текущего времени от сервера службы времени? Большая часть клиент/серверных систем, использующих механизм сокетов, достаточно похожи. Электронная почта, файловый сервер, средство удаленного доступа, распреде ленные базы данных и многие другие Internet-сервисы выглядят по-разному при представ лении на экране, но работают они одинаково. После освоения какой-либо клиент/серверной системы, использующей механизм сокетов потока, нам будет достаточно просто понять, как работает большинство других таких сис тем. В этой главе мы рассмотрим базовые операции и принципы проектирования, которые являются общими для сетевых программ. Далее эти абстракции будут применены при на писании Web-cepeepa.
422
Соединения и протоколы. Разработка Web-cepeepa
12.2. Три основные операции В главе 11 мы убедились в том, что клиент/серверные системы, использующие сокеты потока, имеют структуры, которые аналогичны структуре на рисунке 12.1. Клиенты и серверы являются процессами. На сервере устанавливается некий сервис, затем сервер входит в цикл получения и обслуживания запросов.
Клиент соединяется с сервером, посылает запросы серверу, принимает данные от сервера, рассчитывается с сервером и заканчивается. В таком взаимодействии задействованы три основные операции: 1. Сервер устанавливает сервис. 2. Клиент соединяется с сервером. 3. Сервер и клиент выполняют необходимые финансовые действия. Рассмотрим сначала каждую из этих операций.
12.3. Операции 1 и 2: установление соединения Для работы поточных систем необходима установка соединения. Рассмотрим последова тельность шагов по установке соединения и затем сопоставим этим шагам необходимые библиотечные функции.
12.3.1. Операция 1: установка сокета на сервере Сначала, как это показано на рисунке 12.2, сервер устанавливает сервис. Заметим, что каждый поточный сервер при установке сервиса выполняет три таких шага: (a) Создать сокет. sock = socket(PF_INET, SOCK_STREAM, 0) (b) Связать сокет с адресом. bind(sock, &addr, sizeof(addr))
12.3.Операции1и2:установлениесоединения
423
(с) Перейти в состояние приема (режим прослушивания) входящих запросов. listen(sock, queue_size)
Чтобы каждый раз не раскрывать эти шаги для каждого сервера, которые мы будем разра батывать, объединим эту процедуру из трех шагов в простую функцию: make_server_socket. Код этой функции будет находиться в файле socklib.c, текст которого будет приведен далее в этой главе. Когда мы будем писать различные сервера, то мы будем обращаться к указан ной функции. При этом будем учитывать такие особенности: sock = make_server_socket(int portnum) возвращается -1 - при ошибке при успешном выполнении будет создан сокет на сервере, который прослушивает порт "portnum"
12.3.2. Операция 2: соединение с сервером Далее клиент соединяется с сервером (см. рисунок 12.3). Поточные сетевые клиенты соединяются с серверами по мере выполнения таких двух шагов: (a) Создать сокет. sock = socket(PFJNET, SOCK.STREAM, 0) (b) Использовать сокет для установления соединения с сервером. connect(sock, &serv_addr, sizeof(serv_addr))
Сведем эти два шага в одну функцию connect_to_server, код которой будет приведен позже в составе программы socklib.c. Когда мы будем писать коды для различных клиентов, то мы будем обращаться к этой функции. При этом будем учитывать такие особенности: fd = connectJo_server(hostname, portnum) возвращается -1 - при ошибке
При успешном выполнении будет открыт дескриптор на чтение и запись. Он будет соеди нен с сокетом "portnum" на хосте "hostname".
424
Соединения и протоколы. Разработка Wei
12.3.3. socklib.c Далее представлен программный код программы socklib.c: /* socklib.c
* *
* * * *
В этом файле содержатся функции, которые часто используются при написании клиент/серверных программ для Интернет. Здесь представлены основные функции:
*
int make_server_socket(portnum) Возвращает сокет на сервере при успехе или -1 - при ошибке int make_server_socket_q(portnum, backlog) --
* * *
int connect_to_server(char *hostname, int portnum) Возвращает присоединенный сокет при успехе или-1 - при ошибке
7 tinclude <stdio.h> tinclude tinclude <sys/types.h> tinclude <sys/socket.h> tinclude tinclude tinclude tinclude <strings.h> tdefine HOSTLEN 256 tdefine BACKLOG 1 int make server socket(int portnum)
{ return make server_socket q(portnum, BACKLOG);
} int make server socket q(int portnum, int backlog) '(
struct sockaddr jn saddr; /* здесь будет наш адрес */ struct hostent *hp; /* это часть нашего 7 char hostname[HOSTLEN]; /* адреса 7 int sockjd; /* сокет */ sockjd = socket(PF_INET, SOCK_STREAM, 0); /* получить сокет */ if (sockjd ==-1) return -1; /** построить адрес и связать его с сокетом **/ bzero((void *)&saddr, sizeof(saddr)); /* очистить нашу структуру */ gethostname(hostname, HOSTLEN); /* где я нахожусь? */ hp = gethostbyname(hostname); Г получить информацию о хосте 7 /* заполнить поле хоста 7 bcopy((void *)hp->h_addr, (void *)&saddr.sin_addr, hp->h_length); saddr.sin_port = htons(portnum); /* установить номер порта для сокета 7 saddr.sin family = AF INET; /* установить семейство адресов */ if (bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) != 0) return -1;
12.4. Операция 3: взаимодействие между клиентом и сервером
425
Г* переход в режим приема входящих запросов **/ if (listen(sockjd, backlog) != 0) return -1; return sock id;
}
int connectjo server(char *host, int portnum)
{ int sock; struct sockaddrjn servadd; /* номер запроса */ struct hostent *hp; Г используется для получения номера */ /** Шаг 1: получить сокет **/ sock = socket(AF_INET, SOCK_STREAM, 0);/* получить линию */ if (sock ==-1) return -1; /** Шаг 2: соединение с сервером **/ bzero(&servadd, sizeof(servadd)); /* обнулить адрес */ hp = gethostbyname(host); f* получить ip # хоста */ if (hp == NULL) return -1; bcopy(hp->h_addr, (struct sockaddr *)&servadd.sin_addr, hp-> hjength); servadd.sin_port = htons(portnum); /* установить номер порта */ servadd.sinjamily = AFJNET; f* установить тип сокета */ if (connect(sock, (struct sockaddr *)&servadd, sizeof(servadd)) != 0)
return -1; return sock;
}
12.4. Операция 3: взаимодействие между клиентом и сервером У нас есть функция для создания сокета на сервере и функция для соединения с сокетом на сервере. Как можно будет использовать на практике эти новые функции? Как отнесутся клиент и сервер к каждой из них? В этой секции мы рассмотрим обобщенное представле ние клиентской программы, обобщенную форму серверной программы, а также неко торые проектные решения по созданию сервера. Типичный клиент Сетевой клиент вызывает сервер и получает в ответ некий сервис. Типичный клиент будет выглядеть так: main()
{
int fd; fd = connect to server(host, port); /* вызов сервера 7 if (fd == -1) ~ “ exit( 1); /* или окончание работы 7 talk_with_server(fd); /* диалог с сервером 7 close(fd); Г отсоединение, когда все сделано 7
} Функция talk_with_server управляет диалогом с сервером. Детали функции зависят от при ложения. Например, клиент электронной почты будет “вести разговор” с почтовым сервером относительно почты, а клиента службы прогноза погоды будет интересовать погода.
Соединения и протоколы. Разработка Web-cepeepa
426
Типичный сервер Типичный сервер будет выглядеть так: main()
{ int sock, fd; /* сокет и соединение */ sock = make_server_socket( port); if (sock =.= -1) exit(1); while(1){ fd = accept(sock, NULL, NULL); /* получить очередной запрос */ if (fd==-1) break; /* или закончиться */ process_request(fd); /* диалог с клиентом */ close(fd); /* отсоединение, когда все сделано */
} } Функция processjequest управляет диалогом с клиентом. Детали этой функции зависят от приложения. Например, почтовый сервер сообщает клиенту о письмах, а сервер службы прогноза погоды сообщает клиенту, какой будет погода.
12.4.1. timeserv/timednt, использующие socklib.c Как можно использовать эти два типовых прототипа для построения клиент/серверных систем? Например, что нужно сделать с нашей клиент/серверной службой времени, чтобы она удовлетворяла бы этой модели? На рисунке 12.4 проиллюстрировано, как достигнуть данного соответствия. Для того, чтобы написать клиентские и серверные программы для службы времени с использованием socklib.c, мы напишем функции, которые поддерживают диалог: функцию talk_with_server для клиента и функцию processjequest для сервера службы времени:
talk with_server(fd)
{
process request(fd)
{ char buf [LEN]; int n; n=read(fd,buf,LEN);
timej now; char *cp; time(&now);
12.4. Операция 3: взаимодействие между клиентом и сервером write( 1 ,buf,n); }
427
ср = ctime(&now); writeffd, ср, strlen(cp));
} Сервер с помощью вызова time получает от ядра текущее время. Далее он выполняет ctime, чтобы преобразовать полученное числовое значение времени в печатный формат. Сервер записывает эту строку в сокет, что приводит к пересылке этой строки на сокет клиента. Клиент читает строку из сокета и записывает ее на стандартный вывод. В этой новой версии поддерживается та же логика, как и в предшествующей версии. Но мы получили более модульный проект, а код стал понятнее.
12.4.2. Вторая версия сервера: использование fork Рассмотрим теперь вторую версию сервера службы времени. Здесь вместо получения текущего времени будет использована команда Unix date. На рисунке 12.5 показаны вве денные элементы. Код будет такой: process_request(fd)
Г * послать дату клиенту через fd
*/ {
int pid = fork(); switch(pid){ case -1: return; /* сервис не предоставлен*/ case 0: dup2(fd, 1); /* дочерний процесс запускает date */ close(fd); /* перенаправление stdout */ execl(/bin/date”, "date", N ULL); oopsf'execlp"); /* или заканчивается */ default: wait(NULL); /* родительский процесс ждет дочерний * процесс */
}
}
Как показано на рисунке, сервер с помощью fork создает новый процесс. Дочерний про цесс перенаправляет стандартный вывод на сокет, а затем выполняет команду date. Коман да date вычисляет дату и записывает полученный результат на стандартный вывод, что в результате приводит к посылке этого результата клиенту. В этой программе мы исполь зовали вызов wait. Обычно shell вызывает wait после вызова fork. А какой смысл вызова wait в данном случае? Далее мы рассмотрим этот вопрос .
428
Соединения и протоколы. Разработка Web-cepeepa
12.4.3. Вопрос по коду проектирования: делать самому и делегировать работу другому? Служба времени иллюстрирует нам возможность использования двух типов серверов: Делаю все сам - сервер выбирает запрос и выполняет сам работы по данному запросу. Делегирую - сервер выбирает запрос, а затем с помощью fork порождает процесс, который выполняет работы по полученному запросу. Какие преимущества и недостатки у этих методов? Делшо сам, когда выполняются быстрые, простые задачи. Для определения текущей даты и времени требуется всего один системный вызов time и библиотечная функция ctime. При использовании fork и ехес для запуска команды date потребуется не менее трех систем* ных вызовов и в системе появится новый процесс. Для некоторых серверов наиболее эффективным методом будет выполнение работы самим сервером и управление очередью запросов через listen. При обращении к функции make_server_socket_q в программе socklib.c раз мер очереди задается через аргумент функции. Делегирую, когда выполняются более медленные, более сложные задачи. Серверы, которые выполняют продолжительные по времени задачи или задачи, которые предпола гают ожидание распределения ресурсов, делегируют свою работу. Происходит то, что делает работник в приемной офиса, который принимает входящие звонки. Он может пере ключать абонента на службу продаж в вашем офисе или на конкретную персону. После чего он опять переходит к приему новых входящих звонков. Работая по такой схеме, сервер может управлять одновременно многими запросами. Дая одновременного обслу живания нескольких клиентов сервер не должен выдавать wait чтобы ждать окончания дочернего процесса, который управляет запросом. Но если родительский процесс не вы дает wait, то его дочерний процесс при окончании становится зомби. Каким образом сервер может предотвратить появление зомби? Использование сигнала SIGCHLD для предотвращения зомби. Вместо того чтобы ждать, когда умрет дочерний процесс, родительский процесс может настроиться на прием сигнала, который возникает при окончании дочернего процесса. В главе 8 было рас смотрено, что ядро посылает процессу-отцу сигнал SIGCHLD, когда дочерний процесс выполняет вызов exit или когда дочерний процесс будет кем-то убит. Но в отличие от дру гих сигналов, сигнал SIGCHLD игнорируется по умолчанию. Процесс-отец может устано вить обработчик сигнала SIGCHLD. В этом обработчике можно вызвать wait. Самое простое решение будет выглядеть так: /* Самое простое использование обработчика сигнала SIGCHLD, в котором вызывается * wait() 7 main()
{
int sock, fd; void child_waiter(int), process jequest(int); signal(SIGCHLD, child_waiter); if ((sock = make_server_socket(PORTNUM)) == >1) oops("make_server_socketM); while) 1) { fd = accept(sock, NULL, NULL); if (fd ==-1) break; processjequest(fd); close(fd);
12.4. Операция 3: взаимодействие между клиентом и сервером
429
)
void child_waiter(int signum)
{
wait(NULL);
}’
void process request(int fd)
{
if (fork() == 0){ Г дочерний процесс 7 dup2(fd, 1); Г перенаправить сокет на fd 1 7 close(fd); /* закрыть сокет */ execlp("date,,>,,date,\NULL); Г выполнить по ехес команду date 7 oops("execlp date”);
} } Рассмотрим поток управления в этой программе. Поступает запрос. Процесс-отец вызы вает fork. Затем процесс-отец возвращается на прием следующего запроса. Дочерний про цесс при этом должен выполнить работу по запросу. Когда дочерний процесс закончится, процесс-отец получит сигнал SIGCHLD. Управление передается на функцию обработки сиг нала, и выполняется вызов wait. Дочерний процесс удаляется из таблицы процессов, а в процессе-отце управление передается от обработчика в функцию main. Звучат фан фары! Но остались две проблемы - одна простая, а другая более тонкая. Простая проблема заключается в том, что при передаче управления на обработчик сигнала прерывается сис темный вызов accept. Будучи прерванным по сигналу, системный вызов accept возвращает1, а в переменной ermo-значение EINTR. В нашем коде анализируется значение -1, возвра щаемое accept. В данной ситуации будет зафиксирована ошибка и произойдет окончание работы основного цикла. Нам понадобится модифицировать функцию main, чтобы раз личать ошибочную ситуацию и ситуацию, которая возникает при прерывании системного вызова. Такую модификацию следует сделать в качестве упражнения. Решение более тонкой проблемы будет зависеть от того, как в данной версии Unix произ водится управление множеством сигналов. Что произойдет, если несколько дочерних про цессов выполнят одновременно вызов exit? Пусть процессу-отцу в некоторой ситуации сразу было послано три сигнала SIGCHLD. Первый сигнал был воспринят процессом-отцом, и управление было передано обработчику сигналов. Процесс-отец затем вызывает wait, чтобы удалить дочерний процесс из таблицы процессов. Пока все нормально? Когда процесс-отец занят выполнением обработчика сигнала, ему поступил второй сигнал. В Unix поддерживается механизм блокировки сигналов, но не поддерживается на копление поступающих сигналов в очереди. Поэтому второй сигнал будет блокирован, а третий сигнал будет потерян. Если на интервале времени, когда работает обработчик сиг нала, поступят еще сигналы от дочерних процессов (после выполнения в них exit), то они также будут потеряны. Обработчик сигнала вызывает wait только один раз. Поэтому каждый потерянный сигнал означает уменьшение обращений к wait на единицу. А это в свою очередь означает уве личение на единицу числа зомби. Для решения проблемы обработчик сигналов должен вызывать необходимое число раз wait, чтобы удалить все зомби. Проблему можно решить с помощью системного вызова waitpid: void child_waiter(int signum)
{
while(waitpid(-1, NULL, WNOHANG) > 0)
430
Соединения и протоколы. Разработка Web-cepeepa
Вызов waitpid воспроизводит развернутое множество функций wait. При обращении к вызо ву с помощью первого аргумента задается идентификатор процесса, окончания которого необходимо дождаться. Если в качестве первого элемента задается -1, то это означает тре бование ожидать окончания всех дочерних процессов. Второй аргумент - это указатель на целое число, которое будет определять статус при возврате. В данном случае сервер не интересуется, что случается с его дочерними процессами при их окончании. В более развитых вариантах сервер может использовать статусную информацию для определения причин окончания процессов. Через последний аргумент задаются опции для waitpid. Опция WNOHANG указывает, что нет необходимости ждать, если зомби отсутствует. Этот цикл повторяется до тех пор, пока не будут закончены все ожидаемые процессы. Даже если закончатся сразу все дочерние процессы и при этом каждый выработает сигнал SIGCHLD, то все эти дочерние процессы будут обнаружены.
12.5. Написание Web-cepeepa Мы теперь располагаем вполне достаточной информацией для того, чтобы написать Webсервер. Web-cepeep - это, по сути, расширенный вариант сервера просмотра каталогов, который мы уже написали. Дополнительными частями будут сервер cat и сервер ехес.
12.5.1. Что делает Web-cepeep Web-cepeep реализует простую концепцию. Web-cepeep - это программа, в которой вы полняются три наиболее значимых для пользователя действия: (a) получение списка каталогов (b) выполнение команды cat в отношении файлов (c) запуск программ
Логика Web-cepeepa и клиента Web-cepeep предоставляет пользователям возможность выполнять эти три действия на удаленных машинах, используя при этом соединения с помощью сокетов потока (см. рисунок 12.6). Пользователи запускают клиентские программы на своих машинах и устанавливают соединение с нашим сервером, чтобы послать запрос на сервер. A Webcepeep посылает клиентам в ответ затребованную информацию. Последующий перечень действий иллюстрирует, каким образом будет протекать работа:
12.5. Написание Web-сервера
/ 431
клиент пользователь выбирает ссылку выполнить connect в отношении сервера —> выдать запрос с помощью write —>
читать ответ с помощью read отсоединиться Отобразить ответ: html: визуализировать картинка: нарисовать звук: воспроизвести повторить
<—
сервер вызов accept прочитать запрос с помощью read Удовлетворение запроса: каталог: список файлов в каталоге обычный файл: выдать cat для файла файл.сдк запустить файл на исполнение не существует: сообщение об ошибке выдать ответ с помощью write
12.5.2. Планирование работы нашего Web-cepeepa Какие действия нам необходимо запрограммировать? (a) Установка сервера Мы можем использовать make_server_socket из программы socklib.c. (b) Получить запрос Использовать accept для получения файлового дескриптора клиента. Мы можем использовать fdopen для того, чтобы преобразовать этот файловый дескриптор в буферируемый поток. (c) Прочитать запрос Как должен выглядеть запрос? Каким образом клиент формулирует запрос на что-либо? Нам следует разобраться с этими вопросами. (d) Отработка запроса Мы знаем, как получить список каталогов, получить содержимое файла с помощью cat и как запустить программу на исполнение. Мы можем использовать opendir и readdir, open и read, dup2 и ехес. (e) Послать ответ Как должен выглядеть ответ? Что, предполагается, должен увидеть клиент? С этими вопросами нам также следует разобраться. Это все выглядит обнадеживающе. Мы знаем уже почти все идеи и подходы, которые не обходимы для написания Web-cepeepa. Единственное, что нам осталось сделать, - ознако миться с протоколом Web-cepeepa.
12.5.3. Протокол Web-cepeepa Взаимодействие между Web-клиентом (обычно это броузер) и Web-сервером строится на за просе от клиента и ответе от сервера. Формат запроса и формат ответа определены протоко лом HTTP - (hypertext transfer protocol - протокол передачи гипертекста/ Протокол HTTP похож на протокол для временных серверов и finger серверов, которые были рассмотрены в последней главе. Для протокола используют простой текст. Как и в случаях работы с временными серверами и finger серверами, мы можем использовать telnet для взаимодействия с Webсерверами. Web-cepeepa прослушивают порт 80. Вот реальная копия взаимодействия:
432
Соединения и протоколы. Разработка Web-сервера
$ telnet www.prenhall.com 80 Trying 165.193.123.253... Connected to www.prenhall.com. Escape character is ,л]\ GET /index.html HTTP/1.0 HTTP/1.1 200 OK Server: Netscape-Enterprise/3.6 SP3 Date: Tue, 22 Jan 2002 16:11:14 GMT Content-type: text/html Last-modified: Fri, 08 Sep 2000 20:20:06 GMT Content-length: 327 Accept-ranges: bytes Connection: close <META HTTP-EQUIV-’Refresh" CONTENT="0; URL=http://Vig.prenhall.com/">
Caught you peeking!
-->
Connection closed by foreign host.
$. Тепрь здесь я посылаю одну строку запроса и принимаю многострочный ответ. Несколько слов о деталях.
Запрос HTTP: GET Я использовал telnet для установления связи с Web-сервером на конкретном хосте. telnet создал сокет, и после этого был вызов connect. Сервер воспринимает запрос на соеди нение и создает канал данных, который соединяет мою клавиатуру с процессом сервера, проходя при этом через сокеты. Затем я набрал такой запрос: GET /index.html НТТР/1.0
Запрос HTTP представляет собой одну строку, состоящую из трех полей. В первом поле размещается текст команды, во втором поле - аргумент, в третьем поле указывается версия протокола, который поддерживает взаимодействие клиента. В случае, который ил люстрируется здесь, я выбрал команду GET с аргументом /index.html. При этом я указал, что будет выбран протокол HTTP version 1.0. HTTP использует и другие команды. В большинстве случаев для Web-запросов исполь зуют GET, поскольку в большинстве случаев пользователи “кликают” на ссылки для по лучения страниц. Команда GET может сопровождаться несколькими строками дополни тельных аргументов. Мы рассматриваем здесь простой запрос и будем обозначать конец списка аргументов с помощцю пустой строки. Мы будем использовать те же соглашения относительно выдачи приглашений, которые были использованы нами при написании shell. Фактически Web-cepeep выступает в роли shell, у которого есть встроенные команды cat и Is.
12.5. Написание Web-cepeepa
f 433
Ответ HTTP: OK Сервер читает запрос, проверяет его и посылает обратно ответ. Текст ответа состоит из двух частей: заголовка и содержимого. Заголовок начинается со статусной строки: НТТР/1.1 200 ОК Статусная строка состоит из двух или более подстрок. В первой подстроке указывается версия используемого протокола взаимодействия для сервера. Во второй подстроке нахо дится номер кода ответа. В данном примере код ответа равен 200. Текстовое сопровожде ние для этого кода — “ОК”. Мы затребовали файл /info.html (Здесь, вероятно, следует счи тать, что в командной строке рассматривается команда GET /info.html /HTTP 1.0 и исполь зован аргумент /info.html. - Примеч. пер.), и сервер сообщает, что такой файл есть. Если бы файла с указанным именем не было , то код ответа был бы равен 404 и мы получили бы сообщение типа: “Not found”. Остаток заголовка состоит из нескольких строк, в которых содержится дополнительная информация, составляющая ответ. В этом примере в состав дополнительной информации входит: имя сервера; время выдачи ответа; тип данных, посылаемых сервером; тип соеди нения, используемый для ответа. Заголовок ответа может содержать также и дополнитель ные строки информации, кроме указанных. Сервер обозначает конец заголовка выдачей пустой строки. Эта пустая строка в нашем случае следует сразу за строкой текста: Connection: close.
Оставшаяся часть текста ответа составляет, собственно, содержание ответа. В нашем случае сервер посылает назад содержимое файла /index.html. Итоговые замечания по HTTP Базовая структура взаимодействия между Web-клиентом и Web-сервером такова: Клиент посылает запрос GET filename HTTP/version
Опдаальные аргументы Пустая строка Сервер посылает ответ HTTP/version status-code status* message
Дополнительная информация Пустая строка Содержимое Полное описание протокола находится в документе, который называется RFC1945 для версии 1.0 и RFC2068 для версии 1.1. В нашем Web-cepeepe воспринимаются запросы HTTP от клиентов и затем передаются обратно HTTP ответы. Простой текстовый формат таких запросов и ответов легко читать и обрабатывать с помощью стандартных функций ввода/вывода и текстовых функций языка С-
12.5.4. Написание Web-cepeepa Наш Web-cepeep будет поддерживать только команду GET. Сервер будет читать строку за проса и пропускать дополнительные аргументы. Затем он будет обрабатывать запрос и по сылать обратно ответ. Главный цикл нашего Web-cepeepa будет выглядеть так:
434
Соединения и протоколы. Разработка Web-cepeepa while( 1){ fd = accept(sock, NULL, NULL); fpin = fdopen(fd, "r"); fgets(fpin, request, LEN); read_u ntil_cr n I (fpin); process_rq(request, fd); fclose(fpin);
/* получить запрос */ /* сделать преобразование: FILE * */ /* прочитать клиентское требование */ /* пропустить аргументы */ Г ответить клиенту */ Г разорвать соединение */
} Для простоты здесь опущены действия по проверке ошибок для каждого системного вы зова и каждой функции. Обработка запроса. Обработка запроса заключается в идентификации команды и затем выполнении действий над аргументами. process_rq(char *rq, int fd)
{ pharcmd[11], arg[513]; if (fork() != 0) return; sscanf(rq, "%10s%512s'\ cmd, arg); if (strcmpfcmd, "GET) != 0) cannot_do(fd); else if (not_exist(arg)) do_404(arg, fd); else if (isadir(arg)) do_ls(arg, fd); else if (ends_in_cgi(arg)) do_exec(arg, fd); else Г в противном случае */ do cat(arg, fd);
/* если дочерний процесс, то работать */ Г если родительский процесс, то return */
Г проверка команды */ Г проверка наличия аргумента */ Г п: обратиться к пользователю */ /* каталог? */ Г у: список содержимого */ /* имя X.cgi? */ /* у: выполнить файл */
Г отобразить содержимое */
} В сервере создается новый дочерний процесс для управления каждым запросом пользова теля. Дочерний процесс разделяет текст запроса на две части: команда и аргументы. Если получили команду, отличную от GET, то сервер посылает назад HTTP код о нереализован ной команде. Если же принята команда GET, то сервер делает предположение, что он дол жен выбрать из запроса: или имя каталога, или имя исполняемой программы с расшире нием. cgi или имя обычного файла. Если нет такого каталога или файла с указанным име нем, то сервер извещает об ошибке. Если имя каталога или файла найдено, то сервер решает, какую из трех операций следует выполнить: Is, ехес или cat.
Функция получения листинга каталога. Функция do_ls обеспечивает отработку запро сов на получение списка каталогов: dojsfchar *dir, int fd)
{ FILE *fp; fp = fdopen(fd,"w“);
/* преобразование сокета в FILE * */
12.5. Написание Web-cepeepa headerffp, "text/plain"); fprintf(fp,’V\n"); fflush(fp); dup2(fd,1) dup2(fd,2); close(fd); execl("/bin/ls","ls","-l",dir,NULL); perror(dir); exit(1);
435
Г послать ответный заголовок HTTP */ /* и пометить конец заголовка */ Г воздействовать на сокет */ Г сделать сокет stdout */ /* сделать сокет stderr */
Г закрыть сокет */ /* выполнение Is -I */
[* или ошибка*/ Г окончание дочернего процесса */
} Мы в этой функции не использовали рореп, как это было сделано при обработке каталога в предшествующей главе. С помощью непосредственного исполнения команды Is мы сня ли все проблемы, которые возникают при передаче от пользователя к рореп произвольной строки при запуске через shell. Другие функции. Оставшаяся часть кода будет представлена далее в этой главе. Програм ма уже работает, но она не полна и не удовлетворяет требованиям безопасности. Необхо димо учесть еще ряд моментов: (a) устранить зомби (b) обеспечить защиту от переполнения буфера (c) программы CGI должны иметь доступ к некоторым переменным окружения (d) в заголовке HTTP должно содержаться больше информации. Но, тем не менее, программа представляет собой завершенный Web-cepeep. Программа содержит 230 строк программного кода на С, включая комментарии и пустые строки.
12.5.5. Запуск Web-cepeepa Откомпилируем программу и затем запустим ее с указанием определенного порта:
$ сс webserv.c socklib.c -о webserv $ ./webserv 12345 Теперь вы можете посетить наш Web-cepeep по адресу: http://yourhostname: 12345/. Можно по местить html-файл в каталог и открыть его с помощью: http://yourhostname: 12345/filename, html.
Создайте такой скрипт: #!/bin/sh # hello.cgi - a cheery cgi page printf “Content-type: text/plain\n\nhello\n";
Назовите скрипт hello.cgi и измените для скрипта права доступа с помощью chmod на 755. Затем используйте ваш броузер для обращения к скрипту: http://yourhostname: 12345/hello.cgi.
436
Соединения и протоколы. Разработка Web
12.5.6. Исходный код webserv Далее приведен программный код данного простого Web-cepeepa: /* webserv.c - минимальный по возможности web-сервер (версия 0.2) Использование: ws номер_порта Свойства: поддерживает только одну команду GET исполняется в текущем каталоге * создает новый дочерний процесс для выполнения каждого запроса * есть дыры в обеспечении безопасности работы * используется только для демонстрационных целей есть еще ряд недоделок * является хорошей начальной точкой для дальнейших проектов * Трансляция: сс webserv.c socklib.c -о webserv
7 #indude <stdio.h> #include <sys/types.h> «include <sys/stat.h> «include <string.h> main(int ac, char *av[])
{ int sock, fd; FILE *fpin; char request[BUFSIZ]; if (ac == 1){ fprintf(stderr,"usage: ws portnum\n"); exit(1);
} sock = make_server_socket(atoi(av[ 1 ])); if(sock==-1)exit(2); /* основной ЦИКЛ 7 while(t){ /* получение и буферирование запроса 7 fd = accept(sock, NULL, NULL); fpin = fdopen(fd, "r"); j* чтение запроса*/ fgets(request.BUFSIZ.fpin); printf("got a call: request = %s", request); read_til_crn I (f pin); /* выполнить то, что хочет клиент */ process_rq(request, fd); fclose(fpin);
} }
jit ..............................................................................................................................................................................................._........................... ..........................................*
read_ti l_cr nl (FI LE *) пропустить всю информационную часть в запросе до признака CRNL ...................................................................................................- 7
read_tii_crnl(FILE *fp)
i
,5. Написание Web-cepeepa char buf[BUFSIZ]; while(fgets(buf,BUFSIZ,fp) != NULL&&strcmp(buf,"\r\n") != 0)
Г process_rq(char *rq, int fd) Выполнение того, что затребовано в запросе, и запись ответа через fd Обработка запроса в новом процессе rq - это команда HTTP: GET Доо/bar.html HTTP/1.0 рrocess_rq(char *rq, int fd)
{ char cmd[BUFSIZ], arg[BUFSIZ]; /* создание нового процесса и возврат при неудаче */ if (fork() != 0) return; strcpy(arg,"./"); if (sscanf(rq, "%s%s", cmd, arg+2) != 2) return; if (strcmp(cmd,"GET) != 0) cannot_do(fd); else if (not_exist(arg)) do_404(arg, fd); else if (isadir(arg)) do_ls(arg, fd); else if (endsjn cgi(arg)) do_exec(arg, fd); else do cat(arg, fd);
} Г.............................................................................................................. Заголовок ответа: нужен только один Если contentjype равен NULL, тогда не посылать тип содержимого header(FILE *fp, char *content_type)
{ fprintf(fp, "HTTP/1.0 200 OK\r\n"); if (content_type) fprintfffp, "Content-type: %s\r\n", content type);
} Г Первые простые функции: cannot_do(fd) не реализована команда HTTP do_404(item,fd) нет такого объекта
Соединения и протоколы. Разработка Wet cannot_do(int fd)
{ FILE *fp = fdopen(fd,"w"); fprintf(fp, "HTTP/1.0 501 Not lmplemented\r\n"); fprintf(fp, "Content-type: text/plain\r\n"); fprintf(fp, "\r\n"); fprintf(fp, 'That command is not yet implemented\r\n"); fclose(fp);
} do_404(char *item, int fd)
{ FILE *fp = fdopen(fd,"w"); fprintf(fp, "HTTP/1.0 404 Not Found\r\n"); fprintf(fp, "Content-type: text/plain\r\n"); fprintf(fp, "\r\n"); fprintf(fp, 'The item you requested: %s\r\nis not found\r\n", item); fclose(fp);
} /*......................................................................................................................................* Секция для получения листинга каталога isadir() использует stat, not_exist() использует stat do_ls запускает Is.
7 isadir(char *f)
{ struct stat info; return (stat(f, &info) != -1 &&S_ISDIR(info.st mode));
} not_exist(char *f)
{ struct stat info; return(stat(f, &info) == -1);
} do ls(char *dir, int fd)
{ FILE *fp; fp = fdopen(fd,"w"); header(fp, "text/plain"); f pr i ntf {f p, "\r\n"); fflush(fp); dup2(fd,1); dup2(fd,2); close(fd); execlp("ls"1"ls","-l")dir,NULL); perror(dir);
.5. Написание Web-cepeepa exit(1);
} j~k...................................................................................................................................★ Обработка cgi. Функция для проверки расширения и для запуска программы
......................................................................................................................................*/ char * file_type(char *f) Г получить расширение имени файла */
{ char *ср; if((cp = strrchr(f, ’.’))!= NU11) return ср+1; return
} ends in cgi(char *f)
{ return (strcmp(file type(f), "cgi") == 0);
) do exec(char *prog, intfd)
{ FILE *fp; fp = fdopen(fd,"w"); header(fo, NULL); fflush(fp); dup2(fd, 1); dup2(fd, 2); close(fd); execl(prog, prog, NULL); perror(prog);
} Г.......................................................................................................................................' do_cat(filename,fd) Послать обратно содержимое ответа, следующего за заголовком
......................................................................................................................................7 do cat(char *f, int fd)
{ char ‘extension = file_type(f); char ‘content = "text/plain"; FILE *fpsock, *fpfile; int c; if (strcmp(extension,"html") == 0) content = "text/html"; else if (strcmp(extension, "gif') == 0) content = "image/gif'; else if (strcmp(extension, "jpg") == 0) content = "image/jpeg";
Соединения и протоколы. Разработка Web-cepeepa
440
else if (strcmp(extension, "jpeg") == 0) content = ’’image/jpeg"; fpsock = fdopen(fd, V'); fpfile = fopen(f, V); if (fpsock != NULL && fpfile != NULL)
{ header(fpsock, content); fprintf(fpsock, "\r\n"); while((c = getc(fpfile)) != EOF) putc(c, fpsock); fclose(fpfile); fclose(fpsock);
} exit(0);
}
12.5.7Сравнение Web-серверов Web-cepeep - это программа, которая дает пользователям возможность получать на дру гих компьютерах листинг каталогов, читать содержимого файлов, запускать программы. Все Web-серверы выполняют одни и те же базовые операции и все должны удовлетворять правилам ядра HTTP. В чем заключается различие между серверами? Некоторые серверы легче конфигуриро вать, ими легче управлять, чем другими. У некоторых серверов более развиты средства по поддержанию безопасной работы. На некоторых серверах процессы развиваются быстрее, чем на других, или могут использовать для своих нужд меньше памяти. Наиболее важное свойство для Web-сайтов - эффективность сервера, что предполагает ответы на такие вопросы. Сколько запросов сразу может отрабатывать сервер? Сколько системных ресурсов необходимо серверу при обслуживании каждого запроса? Web-cepeep в этой главе создавал новый запрос для обслуживания каждого запроса. Будет ли такой подход наиболее эффективным? Требования на чтение файлов и получение лис тингов каталогов могут выполняться долго. Поэтому сервер не должен ждать завершения таких действий. Но нужен ли нам для этого новый процесс? Существует третий метод для одновременного запуска нескольких операций. Программа может запустить на исполнение в одном процессе сразу несколько задач на основе механиз ма, который называют механизм нитей. Этот механизм мы изучим в одной из последующих глав.
Заключение Основные идеи •
•
Клиент/серверные программы, использующие механизм сокетов, удовлетворяют требова ниям общей структуры (framework). Сервер воспринимает и обрабатывает запросы (требования). Запросы вырабатывают клиенты. На серверах устанавливаются сокеты. Серверные сокеты имеют конкретные адреса и настраиваются на прием запросов.
Заключение
441
•
Клиенты создают и используют клиентские сокеты. Клиент не осведомлен об адресе своего сокета. (Здесь, вероятно, подразумевается, что клиент не выполняет bind - свя зывание сокета. - Примем. ред.)
•
Сервер может обрабатывать запросы по одному из двух вариантов. Он может обраба тывать запросы сам или может породить процесс с помощью fork, который и будет управлять запросом.
•
Web-cepeep - это популярное приложение, использующее механизм сокетов. Webcepeep обрабатывает три основных типа запросов: послать клиенту содержимое фай ла, послать клиенту листинг каталога, запустить программу. Протокол по передаче запросов (требований) и ответов называется протоколом HTTP.
Что дальше? Телефонная модель реализуется не только в сетевых системах клиентов/серверов. Неко торые люди делают покупки по почтовым каталогам, посылая заказы на товары, а в ответ получают выбранные товары. Используя коммуникационную запросную систему, каждый покупатель может связываться с несколькими магазинами. А каждый магазин может одно временно обслуживать много покупателей. В следующей главе мы рассмотрим сетевое программирование, где используется модель почтовых карточек: сокеты дейтаграмм.
Исследования 12.1 В примере кода клиента для time клиента происходит однократный вызов read и write. Что произойдет, если общий объем данных, поставляемых сервером, превысит допусти мую нагрузку или будет переполнен буфер? Каким образом следует модифицировать клиента, чтобы иметь возможность управлять данными, для которых требуется много кратная выдача read? А теперь о сервере. Что будет происходить, если после выполнения вызова write возвращаемое значение оказывается меньше, чем длина строки? 12.2 Сравнение wait и waitpid. Модифицированная версия обработчика сигнала SIGCHLD использует waitpid и цикл. Можно ли использовать в таком цикле wait для решения про блемы управления множеством сигналов?
Программные упражнения 12.3 Модифицируйте типовой сервер так, чтобы он производил рестарт вызова accept если его исполнение было прервано сигналом. 12.4 Модифицируйте Web-cepeep так, чтобы он вел учет всех поступивших запросов и запи сывал статус ответов. . 12.5 Когда Web принимает запрос на выполнение CGI-программы, то сервер передает ряд переменных в среду CGI программы. Определите, какие это переменные, и добавьте некоторые из них к Web-серверу, В главе, посвященной вопросам программирования в shell, рассмотрено, каким образом можно добавлять переменные в среду. 12.6 Web-cepeepa cgi-bin используют две основные системы для идентификации требова ний, по которым запускаются программы. Сервер, который был представлен в этой главе, идентифицировал программы по расширению.сдк Другой метод использует маршрутное имя.
442
Соединения и протоколы. Разработка Web-cepeepa
Если файл имеет в составе маршрутного имени каталог с именем cgi-bin, то такой будет файл запускаться на исполнение. Например, если поступит запрос на файл /cgi - bin/counter, то этот файл будет запущен сервером на исполнение. Модифицируйте сервер, в котором должна будет поддерживаться такая система идентификации. 12.7 Заголовки ответов. Модифицируйте Web-cepeep так, Чтобы он посылал клиентам боль ше информации в заголовке. Пример соединения в тексте показывает типичный набор полей в заголовке. Добавьте в сервер возможность добавлять эти поля в заголовок Webcepeepa. , 12.8 Метод HEAD. Модифицируйте Web-сервер так, чтобы он мог поддерживать обработ ку запроса HEAD. Ознакомьтесь с деталями в спецификациях HTTP. 12.9 Метод POST. Модифицируйте Web-сервер так, чтобы он мог поддерживать обработку запроса POST. Ознакомьтесь с деталями в спецификациях HTTP.
Проекты На основе материала, изученного в этой главе, вы можете написать версии следующих программ Unix: httpd, telnetd, fingerd, ftpd
Глава 13 Программирование с использованием дейтаграмм. Лицензионный сервер1
Цели Идеи и средства • • • • • • •
Программирование с использованием дейтаграмм, сокеты дейтаграмм. TCP и UDP. Лицензионный сервер. Программные билеты (tickets). Проектирование устойчивых систем. Проектирование распределенных систем. Unix-сокеты доменов.
Системные вызовы •
socket
•
sendto, recvfrom
1. Эта глава основана на материале лекций, которые были написаны и представлены Лоуренсом Де-Люка, когда он преподавал в Harvard Extension School в качестве ассистента. Лекции основаны на практическом материале.
444
Программирование с использованием дейтаграмм. Лицензионный сервер
13.1. Программный контроль Для выполнения программы необходима память, процесс, время центрального процес сора и ряд системных ресурсов. Всем этим управляет операционная система. Кроме того, для некоторых программ требуется еще иметь и разрешение от собственника программы на исполнение. Используя обычную терминологию, вам необходима лицензия на использование ряда программ, а в ряде лицензий указываются определенные ограничения в отношении про грамм. Например, лицензией может быть установлен предел на допустимое число пользо вателей, которым можно одновременно запускать программу. Лицензия, приобретенная на возможность работы десяти пользователей, может иметь одну стоимость, а лицензия для работы пятидесяти пользователей будет иметь совсем другую стоимость. Некоторые производители программного обеспечения устанавливают арендную плату на програм мные лицензии. Предполагается, что программы по такой лицензии не будут работать, когда истечет срок аренды. На использование программного обеспечения могут наклады ваться также ограничения, которые уже никак не связаны с юридическими отношениями. В школьной компьютерной лаборатории может быть установлено ограничение на время в течение дня, когда на компьютерах лаборатории нельзя запускать игровые программы. Некоторые собственники программного обеспечения используют систему “суда чести” (honor system) для того, чтобы установить правила на использование их программ. Они печатают условия лицензии на экране или бумаге и обращаются к пользователям с прось бой выполнять условия контракта. Другие собственники программного обеспечения соблюдают лицензионные правила на технологическом уровне. Один из технологических методов соблюдения программных лицензий - писать программы, которые соблюдают их собственные лицензии. Популярной технологией является проектирование прикладных программ, которые запрашивают разрешение на работу у лицензионного сервера. Такой сервер представляет собой процесс, которые сообщает прикладной программе может она выполняться или нет. Лицензионный сервер знает и соблюдает лицензионные правила. , (см. рисунок 13.1).
Рисунок 1 3 . 1 Лицензионный сервер дает разрешение Для выдачи запроса на разрешение и для получения гарантии на выполнение требуется установление коммуникаций между лицензированной программой и лицензионным сервером. Как работает лицензионный сервер? В этой главе мы рассмотрим модель клиент/сервер, которая будет использована при лицензионном управлении. При изучении этого материа ла мы изучим еще один вид сокетов - сокеты дейтаграмм. Кроме того, изучим другой адресный домен - Unix домен, сетевой протокол, в котором поддерживается состояние системы, а также некоторые подходы для установления средств по обеспечению безопас ности и надежности работы в системе клиент/сервер.
13.2. Краткая история лицензионного контроля
445
13.2. Краткая история лицензионного контроля Техника контроля за использованием программного обеспечения развивалась в течение мно гих лет. В эру автономных персональных компьютеров программное обеспечение с ограниче ниями на использование поставлялось на специальных дисках или поступало с ключевыми дисками, на треках которых был скрытый закодированный шифр. Секретный код было труд но копировать. Программа могла работать только тогда, когда такой диск был вставлен в дис ковод. Если вы потеряли специальный диск или пролили на него кофе, то вы уже не сможете запустить программу на исполнение. Но народ скоро раскрыл тайну копирования специаль ных дисков. Поэтому производителям программного обеспечения пришлось изобрести аппаратные ключи. Аппаратные ключи представляют собой адаптеры, которые вставляются в параллельный порт, последовательный порт или в порт USB. Лицензированная программа запускается на исполнение, только если она обнаруживает, что адаптер вставлен в компьютер. Если вы потеряли аппаратный ключ или забыли ключ на работе, когда принесли компьютер домой, то вы не сможете запустить программу. Использование компьютеров в составе сетей, работа в многопользовательских системах привнесли новые проблемы. Если десять пользователей хотят одновременно исполнять одну и ту же программу на одной мадшне или в сети, то должен ли каждый из таких поль зователей вставлять аппаратный ключ в порт сервера? Производители программного обеспечения для серверов предложили метод соблюдения лицензий, который является гибким и не обременяет законных пользователей дополнительными неудобствами. В сетевых и многопользовательских системах предлагается новое решение, а именно ли цензионный сервер. Лицензионная программа проверяет не наличие ключевого диска или ключа, а получает разрешение на исполнение от серверного процесса. Лицензионному серверу не грозит быть залитым кофе, его нельзя оставить в портфеле, но его могут разде лять на одном компьютере многие пользователи. Серверные процессы могут также рабо тать по таким алгоритмам, которые обеспечивают контроль за допустимым количеством пользователей программы, за тем, когда будет использоваться программа, где будет ис пользоваться программа и даже за тем, как будет использоваться программа. Поскольку большинство компьютеров работают в составе Internet, то метод контролируемого досту па (с помощью сервера) к программному обеспечению и данным становится все более популярным. Наш проект Наш лицензионный сервер будет отслеживать выполнение лицензионного ограничения по допустимому количеству пользователей, то есть сервер должен разрешать одновремен ное обращение к программе для одновременной работы с ней только определенному ко личеству пользователей и не более.
13.3. Пример, не связанный с компьютерами: управление использованием автомобилей в компании Компания приобрела лицензию, в которой установлен предел на число одновременно рабо тающих пользователей с программой. В компании может работать больше служащих, чем ус тановленный в лицензии предел на число пользователей программы. Но не всем служащим не обходимо сразу, одновременно работать с программой. Как обеспечить возможность использо вания программы с учетом выполнения таких ограничений и требований?
446
Программирование с использованием дейтаграмм. Лицензионный сервер
В нашей повседневной жизни очень большое количество систем разделяют фиксирован ное число каких-то предметов среди большего числа их пользователей. Рассмотрим здесь некую модель такого использования. Обратимся к примеру решения проблемы разделе ния среди водителей автомобилей компании. Пусть автомобильная компания имеет некое количество автомобилей в своем парке. Водителей в составе компании больше, чем число автомобилей в этой компании. Как можно управлять доступом водителей к автомобилям?
13.3.1. Описание системы управления ключами от автомобилей Можно управлять доступом к автомобилям с помощью системы выдачи ключей от ма шин. Если нет ключа от определенной машины в гнезде для его хранения, то вы не сможе те использовать данный автомобиль. Если ключ на месте, то вы можете вынуть его из гнезда, отметить в журнале, что вы забрали ключ, и использовать автомобиль. Когда вы попользовались машиной, то вы возвращаете ключ в гнездо для хранения и вычеркиваете ваше имя в списке пользователей, которым были выданы ключи. Такая процедура управ ления ключами изображена на рисунке 13.2.
Каково назначение журнала учета выдачи ключей? Целью системы управления ключами является лимитирование доступа к машинам. Значимым является наличие доступных ключей. Люди не всегда дисциплинированны. Водитель может забыть и не вернуть ключ, даже если он поставил машину в парк. Менеджер службы выдачи ключей может обнаружить и позвонить тому водителю, кто забыл вернуть ключ. Обратившись к учетному журналу, можно узнать, использует ли водитель сейчас автомобиль или забыл сдать ключ? Система выдачи ключей может быть использована в качестве полезной модели для управ ления доступом к программному обеспечению. Прежде чем заняться переводом этой мо дели в программный эквивалент, нам нужно более подробно описать систему ключей.
Компоненты системы управления ключами (a) Центральное место хранения ключей - место, где можно хранить ключи. (b) Менеджер ключей - некто, отвечающий за выдачу и возврат ключей. (c) Ключи - некоторая сущность, которую вам нужно получить. (d) Учетный список - место для хранения записей о взятии и возврате ключей.
13.3.2. Управление автомобилями в терминах модели клиент/сервер После рассмотрения состава системы выдачи ключей от автомобилей мы теперь опишем эту систему в терминах языка программирования клиент/серверных систем. Клиенты и сервер Сначала установим, кто является клиентом, а кто - сервером? Менеджер ключей имеет ресурс, который необходим водителям. В терминах сетевого программирования менед жера ключей мы будем называть сервером, а водителей - клиентами.
13.4. Управление лицензией
447
Протокол Далее. Что является протоколом? Что считать транзакцией? Протокол управления ключами СКМР (car-key-management protocol) имеет две транзакции: Получить ключ Клиент: Привет, я хотел бы получить ключ. Сервер: Пожалуйста, вот вам ключ 5. Или: Извините, ключей сейчас нет. Вернуть ключ назад Клиент: Я возвращаю ключ 5. Сервер: Благодарю. Коммуникационная система Далее. Как будет взаимодействовать клиент и сервер? В системе выдачи ключей от ав томобилей люди просто будут обмениваться фразами в диалоге друг с другом. Структуры данных Наконец, какие необходимы структуры данных для организации работы водителей и менеджера ключей? У менеджера ключей находится учетный журнал, где на каждый ключ заведена отдельная запись. Когда водитель берет ключ, то менеджер заносит в со ответствующую запись имя водителя. Когда водитель возвращает ключ, то менеджер вычеркивает имя водителя из этой записи. В следующей таблице иллюстрируется учетный лист журнала: Журнал учета ключей Номер ключа Водитель
1
adam(g>sales
2 3 4
* carol@support
Если в учетном листе нет имени водителя в учетной записи для какого-то ключа, то ключ еще не выдан. Ключ распределен (выдан), если в учетной записи для данного ключа есть имя водителя.
13.4. Управление лицензией Теперь мы преобразуем систему управления ключами в лицензионную систему управле ния.
13.4.1. Система лицензионного сервера: что делает сервер? На рисунке 13.3 изображена некая группа людей, у которых может возникнуть желание запустить на исполнение лицензионную программу. Наша система работает так: (a) Пользователь U стартует лицензионную программу Р. (b) Программа Р обращается к серверу S за разрешением на запуск. (c) Сервер S проверяет текущее число пользователей, которые уже работают с про граммой Р. (d) Если допустимый предел по количеству пользователей еще не достигнут, то сервер S разрешает доступ и программа Р начинает исполняться.
448
Программирование с использованием дейтаграмм. Лицензионный сервер
(е) Если же предел по количеству пользователей был достигнут, то сервер *S не дает разрешения на запуск. Программа Р сообщает пользователю U, что нужно попытаться выполнить ее запуск позже.
Лицензионный сервер мало чем отличается от серверной системы выдачи ключей от авто мобилей. В автомобильном варианте водители обращались к менеджеру ключей за разрешением пользоваться автомобилем. В программном варианте программа также обра щается к серверу за разрешением на свое исполнение. Это выглядит в отношении автомобильного варианта так, как если бы водитель обращался к автомобилю, а автомобиль запрашивал ключ у менеджера ключей2. Разработчик лицензионной программы пишет обе программы: приложение и сервер. Эти две программы составляют систему. Сервер предоставляет разрешение прикладной про грамме на ее запуск, а также отслеживает выполнение лицензионных требований. Если лицензионный сервер не работает, то прикладная программа не сможет получить разре шение на запуск и откажет пользователю в запуске. Мы использовали в качестве модели систему выдачи ключей от автомобиля. Как можно преобразовать детали этой системы в программную модель? Как лицензионная програм ма будет обращаться к серверу за разрешением на запуск? Как сервер будет гарантировать возможность запуска? Что можно считать программным эквивалентом ключа для автомо биля?
13.4.2. Система лицензионного сервера: как работает сервер? Ticket-модель Менеджер ключей выдает ключи. А что выдает лицензионный сервер? Рассмотрим примеры работы кинотеатров и стадионов. Вы платите деньги за право входа в киноте атр. Вам для этого выдают билет (ticket). Наш лицензионный сервер будет в ответ на запросы выдавать электронные билеты {tickets). На что должен быть похож цифровой билет? Клиенты и серверы при взаимодействии обмениваются текстовыми сообще ниями, Поэтому электронный билет должен быть строкой текста. Для его представле ния мы будем использовать следующий формат: р1(1.номер_билета. Например: 6589.3
2. Элу идею нельзя отбрасывать.. Механизмы и приборы становятся все более интеллектуальными и более взаи мосвязанными между собой. Поэтому вскоре ваш радиоприемник будет запрашивать сервер, можно ли сейчас проиграть вашу любимую мелодию?
13.4. Управление лицензией
449
Каждый билет состоит из идентификатора процесса (PID), которому выдается билет, а также из номера билета. Мы включили в состав билета PID, руководствуясь теми же соображениями, какие есть у авиакомпаний, которые печатают на авиабилете вашу фамилию при покупке вами авиабилета. Наличие вашей фамилии на авиабилете гаран тирует, что только вы можете использовать этот билет, а также, что это может помочь в ситуации, когда вы потеряли билет. А могут ли процессы потерять выделенные им билеты? Сервер и клиенты Прежде всего разберемся, кто есть клиент, а кто есть сервер. Лицензионный сервер содержит ресурс, который необходим программам: билеты. В терминах сетевого про граммирования сервер - это сервер, а приложение - это клиент. Протокол Во-вторых, что следует считать протоколом? Какие будут транзакции? Наш протокол управления билетами содержит две основные транзакции: Получение ключа Клиент: HELO мой_р1с1_такой-то Сервер: TICK выдается билет ИЛИ FAIL, если нет билетов Сдать билет обратно Клиент: GBYE идентификатор_ключа Сервер: THNX сообщение Мы определили текстовый протокол, состоящий из простых четырех символьных ко манд с аргументами. Команды похожи на команды, которые используют Web-клиенты и серверы. Коммуникационная система Далее следует решить, как будут передаваться эти короткие текстовые сообщения между клиентом и сервером? Далее мы рассмотрим этот вопрос. Структура данных Наконец, нужно решить, какие структуры данных необходимы клиентам и серверу? Мы будем использовать целочисленный массив, который будет выполнять роль учет ной страницы в журнале. Каждая запись в этом массиве будет соответствовать одному билету. Когда клиент получает билет, то менеджер записывает PID клиента в эту за пись. Когда клиент возвращает билет, то менеджер стирает PID клиента из записи в учетном списке. Все это представлено в качестве иллюстрации в таблице.
Учетная страница по выдаче билетов Номер билета 1 2 3 4
Процесс 1234 0 6589 0
Если элемент массива содержит 0 в поле для PID, то билет свободен. Если же в таком поле записан PID, то билет находится в использовании.
450
Программирование с использованием дейтаграмм. Лицензионный сервер
13.4.3. Коммуникационная система Как будет клиент выдавать запрос на билет и как сервер будет выдавать билеты? Намнужно будет использовать какой-либо вид межпроцессного взаимодействия. Клиенты и серверы обмениваются короткими сообщениями. Лицензионный сервер принимает запросы на билеты, обрабатывает их и отвечает на запросы, которые поступают от многих пользователей. Какую технику межпроцессных взаимодействия следует выбрать? Сигна лы - это короткие сообщения, но слишком уж короткие. Неименованные программные каналы соединяют только родственные процессы. Наиболее подходящим решением будет использование сокетов. Но и среди сокетов мы должны сделать выбор. Мы знаем о суще ствовании сокетов потоков, которые похожи на программные каналы, но поддерживают соединения между не родственными процессами. Другой тип сокетов называется сокета ми дейтаграмм. Сокеты дейтаграмм (или сокеты UDP) будут наилучшим выбором в на шем проекте.
13.5. Со кеты дейтаграмм С »помощью сокетов потока данные передаются от одного процесса к другому таким же образом, как в телефонной сети обеспечивается связь между абонентами. Вы устанавли ваете соединение, а затем используете соединение для поддержания байтового потока данных, непрерывного, двунаправленного, работающего по принципу программного канала. Сокеты потока используют сетевой протокол TCP {transmission control protocol). Сокеты дейтаграмм используют протокол UDP {user datagram protocol). Чем отличаются эти про токолы? Когда более целесообразно выбрать сокеты потока, а когда следует выбирать сокеты дейтаграмм? Как мы будем использовать сокеты дейтаграмм в программе?
13.5.1 Потоки (streams) и дейтаграммы Как происходит работа сокетов? Как происходит фактическая передача данных по Inter net? Что делает ядро, когда мы записываем данные в сокет потока? В чем при этом будет отличие от варианта, когда данные записывают в сокет дейтаграмм? Существование непрерывного, наполненного потока данных между двумя сокетами пото ка является иллюзией. Средства установления коммуникаций Internet разбивают ваш по ток данных на отдельные пакеты. Передача данных по сети будет выглядеть приблизи тельно так, как это изображено на рисунке 13.4.
Рисунок 1 3 . 4 Передача данных с помощью пакетов в Internet
13.5. Сокеты дейтаграмм
451
Разбиение больших “кусков” информации на небольшие пакеты также имеет свою анало гию в нашем быту. Представьте себе, что вам нужно послать документ, размером в 100 страниц, используя для этого службу срочной доставки. Что нужно будет сделать, если компания по срочным доставкам потребует от вас использовать для пересылки толь ко их конверты, которые вмещают только 20 страниц бумаги? Вы будете вынуждены раз ложить все страницы вашего документа в пять пакетов и каждый отдельный пакет закле ить и написать на нем адрес. Далее вы положите эти пять пакетов в почтовый ящик, а служба доставки сообщений доставит их (на что вы надеетесь) к месту назначения. У адресата кто-либо вскроет эти пять пакетов и составит из них одну стопку страниц, рас полагая их в правильном порядке в соответствии с содержанием вашего документа. Интернет работает аналогично службе срочной доставки сообщений. Данные, которые передаются через Internet, помещаются в контейнеры ограниченного размера. Большие “куски” данных разбиваются на более мелкие фрагменты, которые будут посылаться через сеть. Если вам нужно принять весь большой “кусок” данных сразу, то кто-либо на прини мающей стороне должен будет сложить принятые фрагменты в правильном порядке.
Рисунок 1 3 . 5 Коммуникации можно устанавливать либо с помошью соединения, либо без соединения. Сокеты потока выполняют для вас всю работу по разбиению, упорядочению и реассемб лированию. Сокеты дейтаграмм такую работу не делают. В следующей таблице представ лен список отличий между этими двумя средствами: UDP
TCP Потоки
Дейтаграммы
Фрагментация / Реассемблирование
Нет
Упорядочение
Нет
Надежность
Могут быть потери
Только при соединении Множество отправителей
При работе сокета потока ядро разбивает “куски” данных на пронумерованные фрагмен ты. Ядро на принимающей машине размещает принятые фрагменты в необходимом порядке, воссоздавая точную последовательность байт, которую переслал отправитель. При работе сокета дейтаграмм ядро не устанавливает меток на данные, которые необходи мы для упорядочения или реассемблирования после приема. При использовании сокетов потока доставка гарантируется, при использовании сокетов дейтаграмм - нет. На принимающей стороне сокета потоков ведется проверка номеров фрагментов в последовательности их приема, чтобы определить - все ли фрагменты были
452
Программирование с использованием дейтаграмм. Лицензионный сервер
приняты. Принимающая сторона извещает отправителя о потерянных фрагментах и пред лагает передать повторно копии потерянных частей. При работе сокетов дейтаграмм про верка на потерю пакетов не проводится и не выдаются требования на повторную передачу пропавших пакетов. Если пакет потерялся в Internet, то он уже не придет по назначению. При использовании TCP выполняет гораздо больше работы, чем при использовании UDP. Но UDP быстрее, проще и меньше загружает сеть. При использовании UDP сокетов происходит то же, что и при пересылке сообщений через почтовые ящики: отправители пишут на почтовой карточке адрес получателя; почтовая служ ба в конечном итоге доставляет эту карточку в ваш почтовый ящик; вы вынимаете карточку из почтового ящика. При работе с TCP для чтения одного сообщения от удаленного процесса, требуется выполнить следующую последовательность вызовов: accept, read, dose. Для нашего приложения UDP будет вполне подходящим вариантом. Клиенты посылают короткие сообщения, запрашивая разрешение на исполнение. Сервер посылает в ответ короткие сообщения о разрешении или, наоборот, об отказе. Клиенты и сервер обмени ваются этими сообщениями без проблем по установке соединений. Эти короткие сообще ния не нуждаются в фрагментации и реассемблировании. Надежность передачи тоже не столь существенна. Если потерялся запрос на билет или сам билет, то пользователь может попытаться еще раз запустить свою программу. В любом случае, сервер и клиенты либо будут работать на одной машине, либо в одной и той же секции сети. Поэтому риск по терять пакеты будет небольшим. Если выбрать UDP для построения Web-серверов или почтовых Ъерверов, то это будет плохое решение. Web-страницы и почтовые сообщения.могут быть большими по объему документами. Эти потоки данных должны приниматься полностью и в правильном порядке. Использование UDP будет вполне оправданным при передаче музыкальных или видео данных, когда потеря ноты или фрейма не будет значима по отношению к конечно му результату.
13.5.2. Программирование дейтаграмм Дейтаграмма - это сетевой эквивалент почтовой карточки. Дейтаграмма состоит из трех основных частей: адрес назначения, обратный адрес и сообщение (см. рисунок 13.6).
Сокет дейтаграмм - это бокс для поставки дейтаграмм. Отправитель указывает адрес сокета назначения. Сеть передает дейтаграмму от отправителя к указанному сокету. При нимающий процесс изымает дейтаграммы из сокета. Программы используют вызов sendto при отправлении дейтаграммы через сокет и вызов recvfrom при изъятии дейтаграммы из сокета (см. рисунок 13.7).
13.5.Сокетыдейтаграмм
453
Прием дейтаграмм. Программа dgrecv.c представляет собой пример сервера, исполь зующего дейтаграммы. Программа dgrecv.c назначает порт для сокета. Номер порта задает ся в командной строке. Далее программа входит в цикл, принимая и печатая дейтаграммы, которые будут посылать клиенты:
Г *********************************************************************** dgrecv.c ■
*/
Получатель дейтаграмм Использование: dgrecv номер.порта Действие: прослушивание указанного порта и вывод сообщений
«include <stdio.h> «include <stdlib.h> «include <sys/types.h> «include <sys/socket.h> «include «define oops(m,x) {perror(m);exit(x);} int make_dgram_server_socket(int); int get_internet_address(char *, int, int *, struct sockaddr jn *); void say_who_called(struct sockaddr jn *); int main(int ac, char *av[])
{ int port; /* использовать этот порт */ int sock; /* для данного сокета */ char buf [BUFSIZ]; /* для передачи сюда данных */ size_t msglen; /* длина данных */ struct sockaddrjn saddr; /* сюда помещается адрес отправителя */ socklen_t saddrlen; /* и длина адреса */ if (ас == 11| (port = atoi(av[1])) <= 0){ fprintf(stderr,"usage: dgrecv portnumber\n"); exit(1);
454
Программирование с использованием дейтаграмм. Лицензионный сервер
} /* получить сокет и назначить ему номер порта */ if((sock = make_dgram_server_socket(port)) == -1 ) oops("cannot make sockef,2); Г прием сообщений от этого порта */ saddrlen = sizeof(saddr); while((msglen = recvfrom{sock, buf, В UFSIZ, 0, &saddr, &saddrlen)) >0) { buf[msglen] = '\0'; printffdgrecv: got a message: %s\n", buf); say who called(&saddr);
} return 0;
} void say_who called(struct sockaddr in *addrp)
{ char host[BUFSIZ]; int port; get_internetaddress(host,BUFSIZ,&port,addrp); printf(" from: %s:%d\n", host, port);
}' Функции make_dgrarn_server_socket и get_intemet_address определены в файле dgram.c и будут представлены позже. Прием сообщения от сокета дейтаграмм происходит проще, чем прием сообщения через сокет потоков. Вызов recvfrom будет блокирован, пока не будет принята дейтаграмма. Когда прием закончится, то содержимое сообщения, обратный адрес и длина сообщения будут скопированы в буферы. Посылка дейтаграмм. Программа dgsend.c посылает дейтаграммы. Программа dgsend.c создает сокет, а затем этот сокет будет использован для посылки сообщения .сокету, который находится на машине с указанным именем и имеет указанный номер порта (что задается в командной строке): * dgsend.c * *
отправитель дейтаграмм Использование: dgsend имя_хоста номер_порта "сообщение" Действие: посылка сообщения по адресу имя хоста:номер порта
7 #include <stdio.h> #include <stdlib.h> «include <sys/types.h> tinclude <sys/socket.h> tinclude tdefine oops(m,x) {perror( m) ;exit(x);} int make_dgram_client_socket(); int make_internet_address(char *,int, struct sockaddrjn *); int main(int ac, char *av[])
{ int char
sock; *msg;
/* использовать этот сокет для посылки */ /* послать это сообщение 7
13.5.Сокеты дейтаграмм
455
struct sockaddr in saddr; /* поместить сюда адрес отправителя*/ if (ас != 4){ fprintf(stderr,"usage: dgsend host port 'message'\n”); . exit(1);
} msg = av[3]; /* получить сокет дейтаграмм */ if((sock = make_dgram_client_socket()) == -1) oops("cannot make socket",2); /* объединение имени_хоста и номера.порта для получения адреса назначения */ make_internet_address(av[ 1 ], atoi(av[2]), &saddr); /* послать строку через сокет по этому адресу */ if (sendto(sock, msg, strlen(msg), 0, &saddr, sizeof(saddr)) == -1) oops("sendto failed", 3); return 0;
} При разовом вызове sendto произойдет передача содержимого буфера на сокет по указан ному адресу. Функции поддержки. Детали обработки сокета и адресов сокетов реализованы в функци ях, которые находятся в программе dgram.c: jit'k'k'k-k'k'k'k'kick'k'kic'kic'k'k'kic'kick'kicickick’k'k'k-k'k'k'k'k'k'kirk'kicicic'k'kicick'k'k'k'k'k-kick'k'k'kie'k
* *
dgram.c Функции поддержки обрабртки дейтаграмм
*/ «include <stdio.h> «include «include <sys/types.h> «include <sys/socket.h> «include «include <arpa/inet.h> «include «include <string.h> «define HOSTLEN 256 “ int make_internet_address(); int make dgram server socket)int portnum)
{ struct sockaddrjn saddr; /* здесь строится наш */ char hostname[HOSTLEN]; /* адрес */ int sockjd; /* сокет */ sockjd = socket(PF_INET, SOCK_DGRAM, 0); /* получить сокет */ if (sockjd ==-1) return -1; /** построить адрес и связывание его с сокетом **/ gethostname(hostname, HOSTLEN); /* где я нахожусь? */ make_internet_address(hostname, portnum, &saddr); if (bind(sock_id, (struct sockaddr *)&saddr, sizeof(saddr)) != 0) return -1;
f
456
Программирование с использованием дейтаграмм. Лицензионный сервер return sockjd;
} int make dgram client socket()
{ return socket(PF JNET, SOCK DGRAM, 0);
} int make_internet_address(char ‘hostname, int port, struct sockaddrjn *addrp)
Г
* конструктор адреса Internet-сокета, использует имя_хоста и номер_порта *
(host,port) -> *addrp
7 { struct hostent *hp; ' bzero((void *)addrp, sizeof(struct sockaddr_in)); hp = gethostbyname(hostname); if(hp==NUIl) return -1; bcopy((void *)hp->h_addr, (void *)&addrp->sin_addr, hp->h_length); addф->sin_port = htons(port); addrp->sin_family = AF_INET; return 0;
}, int get_internet_address(char *host, int len, int *portp, struct sockaddrjn *addrp)
Г * извлечь имя_хоста и номер.порта из адреса сокета * *addrp-> (host, port)
7 { stmcpy(host, inet_ntoa(addrp->sin_addr), len); *portp = ntohs(addrp->sin_port); return 0;
} Создание сокета дейтаграмм аналогично созданию сокета потоков. Есть два отличия. Нужно установить в качестве типа сокета SOCK_DGRAM и не нужно вызывать listen. (Кроме того сервер сокета дейтаграмм не делает accept, а его клиент не должен соединять ся с сервером - не делает connect, г- Примеч. ред.) Компиляция и проверка работы. $ сс dgrecv.c dgram.с -о dgrecv $ ./dgrecv 4444 &
[1] 19383 $ сс dgsend.c dgram.c -о dgsend $ ./dgsend host2 4444 "testing 123"
dgrecv: got a message: testing 123 from: 10.200.75.200:1041
13.5.Сокеты дейтаграмм
457
$ps PID TTY TIME CMD 14599 pts/3 00:00:00 bash 19383 pts/3 00:00:00 dgrecv 19393 pts/3 00:00:00 ps
$ Мы откомпилировали и стартовали сервер, который будет прослушивать порт 4444. Затем был откомпилирован и запущен на исполнение клиент, который будет посылать текстовую строку в порт с номером 4444. Сервер принимает сообщение, а затем выводит сообщение и выводит адрес отправителя, который прислал сообщение. Сокет клиента адресуется именем хоста и номером порта. Ядро установило для клиента произвольный номер порта 1041. Из протокола команды ps видно, что сервер продолжает свою работу.
13.5.3. Обобщение информации о sendto и recvfrom sendto НАЗНАЧЕНИЕ
Послать сообщение от сокета
INCLUDE
tinclude <sys/types.h> «include <sys/socket.h>
ИСПОЛЬЗОВАНИЕ
nchars = sendto(int socket, const void *msg, size_t len, int flags, const struct sockaddr *dest, socklenj destjen);
АРГУМЕНТЫ
socket - идентификатор сокета msg - символьный массив, который нужно передать len - число передаваемых символов flags - битовый набор, с помощью которого устанавливаются свойства передачи обычно используют 0 dest - указатель на адрес удаленного сокета destjen - длина адреса
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки nchars - число посланных символов
sendto выполняет передачу данных от сокета к сокету назначения. Первые три аргумента
аналогичны аргументам в системном вызове write: идентификатор сокета, на который по сылается сообщение; символьный массив, который должен быть передан; число символов для передачи. После выполнения sendto возвращает, как и вызов write, количество реально переданных символов. С помощью аргумента flags можно задавать различные свойства про цедуры передачи. За деталями следует обратиться к документации. Последние два аргумен та определяют адрес сокета по месту назначения. Адрес сокета представляется в форме структуры. Как Internet-адрес он содержит IP-адрес хоста и номер порта. Другие типы адре сов будут содержать другие компоненты.
recvfrom НАЗНАЧЕНИЕ
Принять сообщение от сокета
INCLUDE
#include <sys/types.h> #include <sys/socket.h>
458
Программирование с использованием дейтаграмм. Лицензионный сервер
recvfrom ИСПОЛЬЗОВАНИЕ nchars = recvfrom(int socket, const void *msg, sizej len, int flags, const struct sockaddr *sender, socklenj *senderjen); АРГУМЕНТЫ
socket - идентификатор сокета msg - символьный массив len - число символов для приема flags - битовый набор, с помощью которого устанавливаются свойства прием обычно используют 0 sender ~ указатель на адрес удаленного сокета sender Jen - указатель на место, где содержится размер адреса удаленного сокета
КОДЫ ВОЗВРАТА
-1 — при обнаружении ошибки nchars - число принятых символов
читает дейтаграмму из сокета. Первые три аргумента recvfrom аналогичны аргу ментам в системном вызове read: идентификатор сокета, из которого читается сообщение; символьный массив, в который будут помещаться принятые данные; число символов для чте ния. После выполнения sendto возвращает, как и вызов read, количество реально принятых символов. С помощью аргумента flags можно задавать различные свойства процедуры прие ма. За деталями следует обратиться к документации. Последние два аргумента используются для определения адреса отправителя. Адрес сокета отправителя будет храниться в структуре, на которую указывает первый аргумент. Длина адреса должна быть определена в recvfrom. Это значение будет модифицировано, если действительный адрес имеет отличный размер. Если эти указатели нулевые, то адрес отправителя не был записан. recvfrom
13.5.4. Ответ на принятые дейтаграммы Примеры программ dgsend.c и dgrecv.c показали, как можно посылать данные от клиента к серверу. А как сервер может послать обратно свой ответ клиенту? В реальной жизни мо жет быть так, что кто-то прислал вам почтовую карточку с приглашением на обед. Как вы узнаете - куда послать вам свой ответ? Все очень просто: вы пошлете ответ по обратному адресу, который был указан в приглашении. Программа dgrecv2.c принимает сообщения от клиентов и отвечает на них, посылая благо дарственное сообщение по адресу клиента: *dgrecv2.cприемник дейтаграмм * Использование: dgrecv номер.порта * Действие: принять сообщение, отпечатать его и выдать ответ */• #include <stdio.h> #include <stdlib.h> «include <sys/types.h> «include <sys/socket.h> «include «define oops(m,x) {perror(m);exit(x);} int make_dgram_server_socket(int); int get_intemet_address(char *, int, int *, struct sockaddrjn *); void say_who_called(struct sockaddrjn *); void replyJo_sender(int,char *,struct sockaddrjn *, socklenj); int main(int ac, char *av[])
13.5. Сокеты дейтаграмм
459
{ int port; Г использовать этот порт */ int sock; Г для этого сокета */ char buf [BUFSIZ]; Г для приема сюда данных */ size_t msglen; Г длина данных*/ struct sockaddr jn saddr; Г поместить сюда адрес отправителя */ socklenj saddrlen; /* и длину адреса */ if (ас == 11| (port = atoi(av[1 ])) <= 0){ fprintf(stderr,"usage: dgrecv portnumber\n"); exit(1);
}
Г получить сокет и назначить ему номер порта */ if((sock = make_dgram_server_socket( port)) == -1) oopsf'cannot make socket",2); Г принимать сообщения от этого сокета */ saddrlen = sizeof(saddr); while((msglen = recvf rom(sock, buf, BUFSIZ.O, &saddr, &saddrlen)) >0) { buf[msglen] = '\0'; printff'dgrecv: got a message: %s\n", buf); say_who_called(&saddr); reply to_sender(sock,buf,&saddr,saddrlen);
} return 0;
} void reply to sender(int sock,char *msg,struct sockaddr in *addrp, socklen t len)
{ char reply[BUFSIZ+BUFSIZ]; sprintf(reply, "Thanks for your %d char message\n", strlen(msg)); sendto(sock, reply, strlen(reply), 0, addrp, len);
} void say who called(struct sockaddr in *addrp)
{ char host[BUFSIZ]; int port; get_intefnet_address(host, BUFSIZ, &port, addrp); printff' from: %s:%d\n", host, port);
} Программа-отправитель, естественно, должна быть модифицирована, с тем чтобы она мог ла принимать ответы. Это нужно сделать в качестве упражнения.
13.5.5. Итог по теме дейтаграмм Дейтаграммы - это короткие сообщения, которые передаются от одного сокета к другому. Отправитель использует системный вызов sendto, с помощью которого задается само сооб щение, его длина и место назначения, куда сообщение нужно передать. Принимающий процесс использует вызов recvfrom для приема сообщения через сокет. Дейтаграммы непо
460
Программирование с использованием дейтаграмм. Лицензионный сервер
средственно используют многоуровневую структуру адресации пакетов, которые переме щаются через Internet. Поэтому дейтаграммы менее нагружают сетевой код ядра и меньше загружают сетевой трафик. Дейтаграммы могут быть потеряны при передаче. Кроме того, они могут поступать в произвольном порядке. По этим двум причинам сокеты дейтаграмм лучше всего использовать для приложений, в которых простота, эффективность и скорость являются более важными характеристиками, чем целостность и согласованность. Сообщения, которые необходимы для протокола лицензионного сервера, являются про стыми. Приемлемым решением будет просто иметь один сервер, который будет^тринимать короткие сообщения, обрабатывать их и посылать обратно ответы, используя для это го технику дейтаграмм.
13.6. Лицензионный сервер. Версия 1.0 Давайте вернемся к проекту лицензионного сервера. Наш сервер должен лимитировать число попыток одновременного использования программы. Когда* пользователь пытается запустить программу с ограничением на запуск, то процесс будет обращаться к серверу за разрешением на запуск программы. Если количество лиц, использующих программу в текущий момент, не превышает преде ла, то лицензионный сервер разрешает запуск, о чем оповещается с помощью посылки процессу билета (ticket). Если же фиксируется, что в текущий момент с программой работает максимально возможное число пользователей, то программа вежливо сообщает пользователю, чтобы он попытался бы запустить ее позже. Или требуется приобрести ли цензию на программу, по которой допускается большее число одновременно работающих пользователей. Лицензионная программа и сервер взаимодействуют между собой посред ством двусторонних пересылок дейтаграмм. Алгоритм работы двух программ их взаимодействие будут выглядеть так:
srv
clnt get tick do your work ret tick exit
- HELO pid -TICK pid.t-GBYE pid.t--- THNX ---
wait for RQ recv RQ proc RQ reply to RQ
Клиентская и серверная программы состоят из двух файлов: небольшой файл, где содержится функция main, и большой файл, где содержатся функции по управления биле тами. Сначала мы рассмотрим работу клиента, а затем сервера.
13.6. Лицензионный сервер. Версия 1.0
13.6.1. Клиент. Версия 1 / х Iclntl .с
* Лицензионный сервер, клиент, версия 1 * Компонуется с объектными модулями Iclnt funcsl .о dgram.о
7 «include <stdio.h> int main(int ac, char *av[])
{ setup(); if (get_ticket() != 0) exit(0); do_regular_work(); release_ticket(); shut down();
} * dojegularjvork. Здесь выполнятся основная работа приложения 7 do regular work()
{ printff'SuperSleep version 1*0 Running - Licensed Software\n"); sleep( 10); /* наш патентованный алгоритм sleep */
} Обобщенное представление кода для клиента позволяет нам сделать такой вывод. Клиент получает билет, выполняет свою работу, освобождает (сдает) билет и затем заканчивается. В качестве лицензионной программы в данном обобщенном представлении использована специальная версия утилиты steep. Те, кто не удовлетворен работой стандартной версии sleep, может приобрести лицензию на использование этой версий. Естественно, кто это сделает, должен будет запустить наш специальный лицензионный сервер. В противном случае наша программа sleep откажется запускаться. Функции поддержки в Idnt_funcs1 .с: * Idnt funcsl .с: функции для клиента лицензионного сервера
7 «include <stdio.h> «include <sys/types,h> «include <sys/socket.h> «include «include Г Важные переменные
7 static int pid = -1; static int sd = -1; static struct sockaddr servaddr; static socklenj serv alen; static char ticket_buf{128|; . static have.ticket = 0;
/* Наш PID */ /* Наш сокет */ /* Адрес сервера */ /* длина адреса 7 /* буфер для хранения нашего билета 7 /* Флаг, который устанавливается при наличии билета 7
2
Программирование с использованием дейтаграмм. Лицензий «define MSGLEN 128 «define SERVER.PORTNUM 2020 «define HOSTLEN 512 «define oops(p) { perror(p); exit(1);} char xdo_transaction();
/* Размер наших дейтаграмм */ /* Наш номер порта */
Г ’ setup: получить pid, сокет и адрес лицейзионного сервера * IN - без аргументов * RET пусто, при ошибках заканчивается * замечение: предполагается, что сервер находится на том же хосте, на * котором находится клиент
7 setup()
{ char hostname[BUFSIZ]; pid = getpid(); /* для билетов и сообщений */ sd = make_dgram_client_socket(); /* для общения с сервером */ if (sd== -1) oops("Cannot create socket"); gethostname(hostname, HOSTLEN); /* сервер находится на том же хосте */ makejnternet_address(hostname, SERVER_PORTNUM, &serv_addr); serv alen = sizeof(serv addr);
} shut down()
{ close(sd);
} * get_ticket ж получает от лицензионного сервера билет * Результат: 0 при успехе, -1 - при неудаче
7 int get_ticket()
{ char ‘response; char buf[MSGLEN]; if(havejicket) /* не быть жадным */ return(O); sprintf(buf, "HELO %d", pid); /* составить запрос */ if ((response = do_transaction(buf)) == NULL) 7 return(-1); Г Произвести разборку отвёга и посмотреть, получен ли билет? * Если да, то сообщение будет таким: TICK ticket-string. * Если неудача, то сообщение будет таким: FAILfailure-msg
*/ if (stmcmp(response, "TICK”, 4) == 0){
,6. Лицензионный сервер. Версия 1.0 strcpy(ticket_buf, response + 5); /* получить идентификатор билете have_ticket = 1; /* установить этот флаг */ narrate("got ticket”, ticket_buf); return(O);
} if (strncmp(response,"FAIL",4) == 0) narrate)"Could not get ticket",response); else narratef’Unknown message:", response); return(-1); } Г get_ticket */
^*************************************************************************** * release_ticket * Возврат билета серверу * Результаты: 0 при успехе, -1 - при неудаче
*/' int release ticketQ
{ char buf[MSGLEN]; char ‘response; if(!have_ticket) /* билета нет */ return(O); /* возвращать нечего */ sprintf(buf, "GBYE %s", ticket_buf); /* составить сообщение 7 if ((response = do_transaction(buf)) == NULL) return(-1); Г проверка ответа * успех: THNX info-string * неудача: FAIL error-string
7 if (strncmpfresponse, "THNX", 4) == 0){ narrate("released ticket OK",""); return 0;
} if (strncmp(response, "FAIL", 4) == 0) narratef’release failed", response+5); else narratef'Unknown message:", response); return(-1); } Г release_ticket 7 * do_transaction * Посылает запрос на сервер и получает ответ от сервера * IN msg_p • сообщение для посылки * Результаты: указатель на строку сообщения или NULL - при ошибке * ЗАМЕЧАНИЕ: возвращается указатель на статическую память, которая * перезаписывается при каждом успешном вызове
464
Программирование с использованием дейтаграмм. Лицензионный сервер
*
Для достижения сверх секретности сравнивается retaddr с serv addr * (зачем?)
7 char *do transaction (char *msg)
{ static char buf[MSGLEN]; struct sockaddr retaddr; socklenj addrlen=sizeof(retaddr); int ret; ret = sendto(sd, msg, strlen(msg), 0, &serv_addr, serv_alen); if (ret==-1){ syserrC'sendto"); return(NULL);
}
Г Получение ответа */ ret = recvfrom(sd, buf, MSGLEN, 0, Retaddr, Saddrlen); if (ret == -1){ syserr("recvfrom"); return(NUU-);
} /* теперь возвращается собственно сообщение */ return(buf); } Г dojransaction */ * narrate: выводит сообщения на stderr для отладки и для демонстрационных * целей * IN msgl, msg2 : строки для вывода с pid и заголовком * RET пусто, заканчивается при ошибке
7 narrate(char *msg1, char *msg2)
{ f printf (stderr, "CLI ENT [%d]: %s %s\n", pid, msgl, msg2);
} syserr(char*msg1)
{ char buf [MSGLEN); sprintf(buf,"CLIENT [%d]: %s", pid, msgl); perror(buf);
} getjicket и release_ticket являются основными функциями в файле. Обе выполняют одну и ту
же последовательность действий: составляют короткий запрос, посылают сообщение на сервер, ожидают ответа от сервера, а затем проверяют и действуют в зависимости от по лученного ответа. getjicket обращается с запросом на билет, посылая команду HELO, за которой следует ее PID. Сервер удовлетворяет запрос, посылая назад сообщение в форме ТЮК ticket-id. Если сервер отвергает запрос, то он посылает назад сообщение в форме FAIL explanation.
13.6. Лицензионный сервер. Версия 1.0
465
releaseticket возвращает билет, что делается с помощью посылки команды GBYEticket-id. Если билет был принят, то сервер оправляет сообщение в форме THNX greeting. Если же билет был признан недействительным, то сервер отправляет сообщение в форме FAIL explanation. Почему билет может оказаться недействительным? Мы рассмотрим эту проблему и спо собы ее разрешения позже.
13.6.2. Сервер. Версия 1 ^*************************************************************************** * Iservl .с * Программа лицензионного сервера. Версия 1*/ «include <stdio.h> «include <sys/types.h> «include <sys/socket.h> «include «include <signal.h> «include <sys/errno.h> «define MSGLEN 128 /* Размер наших дейтаграмм */ int main(int ac, char *av[])
{ struct sockaddrjn client_addr; socklen_taddrlen=sizeof(client_addr); char buf [MSGLEN]; int ret; int sock; sock = setup(); while(1) { addrlen = sizeof(client_addr); ret = recvfrom(sock, buf, MSGLEN, 0,&client_addr,&addrlen); if (ret != -1){ buf [ret] = '\0'; narratefGOT:", buf, &client_addr); handle request(buf,&client addr,addrlen);
} else if (errno != EINTR) perror(''recvfrom");
} } В самом общем представлении лицензионный сервер - это цикл, в котором принимается дейтаграммный запрос, обрабатывается запрос и далее посылается ответ на запрос. Код для управления запросами представлен в программе Iserv_funcs1 .с: J-k'K'k'k'k'k'k-k-k'k-k-k-k-k-k-k-k-k'k'k'k'kick'k-k
* Isrv_funcs1 .с * Функции лицензионного сервера
7
«include <stdio.h> «include <svs/tvDes.h>
466
Программирование с использованием дейтаграмм. Лицензионный серв «include <sys/socket.h> «include «include «include <signal.h> «include <sys/errno.h> «define SERVER.PORTNUM 2020 «define MSGLEN 128 «define TICKET AVAIL 0 «define MAXUSERS 3 «define oops(x) {perror(x); exit(-1);}
Г
Г Номер порта нашего сервера */ Г Размер наших дейтаграмм */ Г Слот, доступный для использования */ /* Для нас могут работать только 3 пользователя */
**★***************************************************************;*********
* Важные переменные 7 int ticket_array[MAXUSERS]; intsd = -1; int num_tickets_out = 0; char *do_hello(); char *do_goodbye()
./* наш массив билетов */ /* Наш сокет */ Г Число выданных и невозвращенных билетов */
* setupQ - инициализация лицензионного сервера
7 setup()
{ sd = make_dgram_server_socket(SERVER_PORTNUM); if (sd== -i) oops("make socket''); free_all_tickets(); return sd;
} free_all tickets(}
{ int i; for(i=0; i<MAXUSERS; i++) ticket array[i] = TICKET AVAIL;
} j★* * shut down() - закрыть лицензионный сервер
7 shut down()
{ close(sd);
} Г
' handle_request(request, clientaddr, addrlen) * ветвление по коду запроса
7
.6. Лицензионный сервер. Версия 1.0 handle request(char *req,struct sockaddrjn *client, socklenj addlen)
{ char ‘response; int ret; /* выполнить действие и составить ответ */ if (strncmp(req, "НЕЮ", 4) == 0) response = do_hello(req); else if (strncmp(req, "GBYE", 4) == 0) response = do_goodbye(req); else response = "FAIL invalid request"; /* послать ответ клиенту */ narratef'SAID:", response, client); ret = sendto(sd, response, strlen(response),0, client, addlen); if (ret ==-1) perrorf'SERVER sendto failed");
}' * do_hel!o * Выдает билет, если имеется в наличии * IN msg_p сообщение, принятое от клиента * Результаты: указатель на ответ * ЗАМЕЧАНИЕ: результат помещается в статический буфер, который переписывается прй каждом вызове
*/ char ’do hello(char *msg p)
{ int x; static char replybuf[MSGLEN]; if(num_tickets_out >= MAXUSERS) return("FAIL no tickets available"); Г иначе найти свободный билет и отдать его клиенту */ for(x = 0; х<MAXUSERS && ticket_array[x] != TICKET.AVAIL; х++)
»
Г Закономерная проверка */ if(x == MAXUSERS) { narrate( "database corrupt","",NULL); retum("FAIL database corrupt");
} /* Найти свободный билет. Записать “имя” пользователя, т. е. PID, в массив * Представить билет в форме: pid.slot
*/ ticket_array[x] = atoi(msg_p + 5); /* получить PID из сообщения */ sprintf( replybuf, ’TICK %d.%d”, ticket_array[x], x); num_tickets_out++; return(replybuf);
468
Программирование с использованием дейтаграмм. Лицензионный сервер } Г do_hello */
^****************************************************************************
*do_goodbye * Возврат билета клиенту * IN msg_p сообщение, принятое от клиента * Ответ: указатель на ответ ЗАМЕЧАНИЕ: результат помещается в статический буфер, который * переписывается при каждом вызове
7 char *do goodbye(char *msg_p)
{ int pid, slot; /* компоненты билета */
Г Пользователь возвращает билет. Нужно выдать сообщение в ответ на принятый * обратно билет. Это сообщение будет таким:
*
GBYE pid.slot
'р- if((sscanf((msg_p + 5), "%d.%d", &pid, &slot) != 2) || (ticket_array[slot] !== pid)) { narratef’Bogus ticket", msg_p+5, NULL); returnf'FAIL invalid ticket");
}
Г Билет действителен. Все в порядке. */ ticket_array[slot] = TICKET_AVAI L; num_ticketsout--; Г Ответить * I return('THNXSeeya!"); } Г do_goodbye */
у**************************************************************************** * narrate!) - дополнительная информация для отладки и для учетных целей
7 narrate(char *msg1, char *msg2, struct sockaddr in *dientp)
{ fprintf(stderr,"\t\tSERVER: %s %s", msgl, msg2); if (clientp) fprintf(stderr,"(%s:%d)", inet_ntoa(clientp->sin_addr), ntohs(clientp->sin_port)); putc('\n', stderr);
}
Здесь были представлены три основных функции: handlejequest
Запросы состоят из четырех-символьных команд, за которыми следуют аргумент. Сервер проверяет команду и вызывает далее соответствующую функцию. Даже если команда ошибочная, сервер должен послать клиенту некий ответ, иначе клиент оста нется блокированным, ожидая поступления ответа.
13.6. Лицензионный сервер. Версия 1.0
469
do_hello
Команда HELO служит для выдачи запроса на билет. Сервер ищет в массиве ключей свободный элемент. Запись, в которой в поле PID находится 0, обозначает свободный ключ. Сервер использует специальную переменную numjicketsout, где будет храниться значение числа выданных и еще не возвращенных билетов. Сервер будет осуществ лять поиск в таблице при поступлении каждого запроса, а эта переменная будет пока зывать, есть ли еще билеты. do_goodbye
С помощью команды GBYE выдается запрос на возврат билета. Билет - это строка, в ко торой содержится PID клиента и номер билета. Сервер сравнивает PID и номер билета с данными, которые представлены в учетном списке ticket_array. Если обнаруживается совпадение, то сервер вычеркивает имя клиента из списка и благодарит клиента. Если обнаруживается несовпадение, то где-то произошла ошибка. Если бы вы работали в службе регистрации в авиакомпании и пассажир предоставил вам билет на самолет, в котором указаны номер и фамилия, не представленные в базе данных авиакомпании, то вы задали бы пассажиру такой вопрос: “Где вы приобрели этот билет? И, наконец, кто вы сами такой?” Проблему подделки билетов мы обсудим позже. А теперь проверим работу нашей программы.
13.6.3. Тестирование Версии 1 Откомпилируем программу сервера и запустим ее на исполнение в фоновом режиме: $ сс iservl .с Iserv_funcs1 .с dgram.c -о Iservl $ ./Iservl & [1 ]25738
Откомпилируем программу клиента и выполним четыре запуска этой программы: $ сс tclntl .clclnt funcsl .с dgram.c -о Iclntl $ ./Iclntl &./lclnt1 &./lclnt1 & ./Iclntl & SERVER: GOT: HELO 25912 (10.200.75.200:1053) SERVER: SAID: TICK 25912.0 (10.200.75.200:1053) CUENT [25912]: got ticket 25912.0 SuperSleep version 1.0 Running - Licensed Software SERVER: GOT: HELO 25913 (10.200.75.200:1054) SERVER: SAID: TICK 25913.1 (10.200.75.200:1054) CUENT [25913]: got ticket 25913.1 SuperSleep version 1.0 Running - Licensed Software SERVER: GOT: HELO 25915 (10.200.75.200:1055) SERVER: SAID: TICK 25915.2 (10.200.75.200:1055) CUENT [25915]: got ticket 25915.2 SuperSleep version 1.0 Running - Licensed Software ' SERVER: GOT: HELO 25914 (10.200.75.200:1059) SERVER: SAID: FAIL no tickets available (10.200.75.200:1059) CLIENT [25914]: Could not get ticket FAIL no tickets available SERVER: GOT: GBYE 25912.0 (10.200.75.200:1053) SERVER: SAID: THNXSeeya! (10.200.75.200:1053) CUENT [25912]: released ticket OK SERVER: GOT: GBYE 25913.1 (10.200.75.200:1054) SERVER: SAID: THNX See ya! (10.200.75.200:1054)
470
Программирование с использованием дейтаграмм. Лицензионный сервер CLIENT [25913]: released ticket OK SERVER: GOT: GBYE 25915.2 (10.200.75.200:1055) SERVER: SAID: THNX See ya! (10.200.75.200:1055) CLIENT [25915]: released ticket OK
Результаты выглядят не совсем такими, что можно были бы получить в реальной системе. Тем не менее, вы видите, что сервер принимает запросы и выдает по ним билеты. Клиенты по запросу получают билеты и начинают свою работу. Вы видите по полученным результатам, что процессу 25914 не повезло и он не получил билета. К тому моменту, когда этот процесс выдал запрос, все билеты были выданы. А вот процесс 25915 получил билет. Если вы будете запускать этот тест несколько раз, то получите различные результаты.
13.6.4. Что еще нужно сделать? Лицензионный сервер версии I работает хорошо. Сервер управляет запросами и поддерживает список процессов, которые получили от него билеты. Клиенты получают биле ты от сервера, выполняют свою работу и возвращают билеты после того, как все сделали. Все выглядит идеально. Но окружающий нас мир не является идеальным. Программное обеспечение и пользователи не всегда делают то, что должны делать или хотели бы де лать. Как могут возникнуть неправильные действия? Как мы должны реагировать в ответ на возникновение ошибочных ситуаций?
13.7. Программирование с учетом существующих реалий Наша система лицензиойного обслуживания работает корректно до тех пор, пока сотруд ничают два процесса. В ряде случаев при работе программ возникают проблемы. Что про изойдет, если исполнение приложения SuperSleep будет закончено по инициативе пользо вателя, или если при исполнении возникнет ситуация, связанная с нарушением адресации и приложение было убито ядром? Что тогда случится с выделенным билетом для прило жения? Что произойдет, если аварийно закончит работу лицензионный сервер? Что про изойдет, если после всего этого лицензионный сервер опять будет перезапущен? Программы, которые должны работать в обычных условиях реального мира, должны быть подготовлены к возникновению различных разрушительных ситуаций. Мы рассмотрим два возможных случая: случай, когда клиент аварийно заканчивается, и случай, когда аварийно заканчивается сервер.
13.7.1. Управление авариями в клиенте Если клиент аварийно заканчивается, то не будет возвращен билет, который он взял у сервера (см. рисунок 13.8). Если процесс аварийно заканчивается то он не возвращает свой билет
Запись в списке выдачи для умершего процесса
Рисунок 13.8 Клиент уносит билет с собой в могилу
13.7. Программирование с учетом существующих реалий
471
В примере с автомобилями может случиться, что водитель не вернул ключ от автомобиля потому что его уволили, или он погиб, или ушел домой, или просто пропал. Как такие события отразятся на системе? Учетный лист показывает, что билет или ключ от автомо биля находится все еще в использовании. Другие процессы или водители не смогут по лучить этот ключ. В программной модели, если достаточное число процессов попадут в аварию, то весь учетный список будет заполнен пометками о выдаче ключей. И уже ни кто не сможет запустить программу, в которой нужно подтвердить разрешение на запуск. Менеджер службы выдачи ключей может повлиять на ситуацию, когда не были возвраще ны ключи. Он просто может позвонить тем людям, у которых остались ключи. Через ре гулярный промежуток времени менеджер будет пролистывать весь список учета и зво нить каждому водителю, задавая ему один и тот же вопрос: “ Вы все еще используете взятый ключ? “ Если мейеджер получает ответ, то он вычеркивает имя этого водителя из списка. Чем более часто менеджер будет проверять людей, у которых находятся ключи, тем более точным будет список. Лицензионный сервер может использовать такую же технику. Через определенный регу лярный интервал лицензионный сервер будет обращаться к массиву билетов и проверять, существуют ли те процессы,. которые представлены в этом массиве? Если обнару живается, процесс больше не существует, то лицензионный сервер может вычеркнуть его имя из списка, освобождая ( восстанавливая) тем самым выданный процессу билет. Чем более часто лицензионный сервер будет выполнять такую функцию, тем более точен будет список.
Восстановление потерянных билетов: планирование Куда следует поместить код для восстановления потерянных билетов? Как мы будем вызывать его? В нашем сервере выполняются две независимые операции: ожидание вхо дящих запросов от клиентов и через регулярные промежутки времени операция по вос становлению потерянных билетов. Планирование таких действий по восстановлению будет достаточно простым. Необходимо использовать вызовы alarm и signal для регулярно го вызова процедуры восстановления. Мы ранее использовали эту технику в главе, где бы ла рассмотрена программа с перемещением сообщения. Наш новый поток управления в программе будет выглядеть так, как показано на рисунке 13.9.
main() set alarm loop wait for req Запрос ^ recv req cancel alarm process req Ответ reply to req restore alarm
у
ticket_reclaim() check all tickets set alarm
T Рисунок 13.9 Использование alarm для планирования процедуры восстановления билетов
472
Программирование с использованием дейтаграмм. Лицензионный сервер
Когда мы разрабатывали программу, которая сразу выполняла две работы, то мы должны были позаботиться о взаимовлиянии функций. Возникнет ли проблема, если нашему серверу, когда он занят обработкой клиентского запроса, будет послан сигнал SIGALRM, по которому нужно будет заниматься восстановлением билетов? Разделяют ли эти две операции некие переменные или структуры данных? Да, разделяют. При выдаче и возврате билетов модифицируется учетный список. И при выполнении функции восстановления биле тов учетный список может также быть модифицирован. Может ли из-за этой зависимости ме жду двумя функциями возникнуть опасность нарушения целостности такой структуры дан ных? Ответ на этот вопрос остается в качестве упражнения. Чтобы быть застрахованным от проблем, мы будем выключать службу alarm на время обработки запросов.
Восстановление потерянных билетов: кодирование Мы хотим восстановить билеты процессов, которые умерли. Как можно определить, не умер ли тот или иной процесс? Можно использовать рореп, а затем в выводе команды ps искать PID процессов, которым были выданы билеты. Есть более быстрое, более простое решение. Оно основано на использовании специального свойства системного вызова kill. Определить, существует ли в текущий момент некий процесс, можно с помощью посылки проверяемому процессу сигнала с номером 0. Если процесс не существует, то ядро не бу дет передавать сигнал, а возвратит ошибочный код и установит в переменной errno значе ние ESRCH. Мы используем это свойство в новой версии функции ticketjeclaim, которая на ходится в программе Iserv_funcs2.c: #define RECLAIM JNTERVAL 60 * через каждые 60 секунд*/
/* работа по восстановлению будет производиться
^**************************************************************************** * ticket_reclaim * Просматривает список выданных билетов и восстанавливает билеты мертвых * процессов * Результаты: нет
*1 void ticket reclaim!)
{ inti; char tick[BUFSIZ]; for(i = 0; i < MAXUSERS; i++) { if{(ticket_array[i] != TICKETAVAIL) && (kill (ticket_array [i], 0) == -1) && (errno == ESRCH)) { /* нет процесса */ sprintf(tick, "%d.%d", ticket_array[i],i); narrate("freeing", tick, NULL); v ticket_array[i] = TICKET_AVAIL; num tickets out--;
} }
i
alarm( RECLAIM JNTERVAL); f* сброс службы alarm */
13.7. Программирование с учетом существующих реалий
473
Далее мы добавим этот код в main, чтобы можно было планировать выполнение функции восстановления билетов и можно было бы выключать alarm при проведении обычной обработки запроса. Эта модифицированная версия находится в программе Iserv2.c: int main(int ас, char *av[])
{ struct sockaddr clientaddr; socklenj addrien=sizeof(clientjiddr); char buf[MSGLEN]; int ret, sock; void ticket_reclaim(); /* дополнение к версии 2 */ unsigned timejeft; sock = setup(); signal(SIGALRM, ticket_reclaim); /* запуск восстановителя билетов */ alarm(RECLAIMJNTERVAL); /* после этого задержка */ while) 1) { addrlen = sizeof(client_addr); ret = recvfrom(sock,buf,MSGLEN,0,&client_addr,&addrlen); if(ret!=-1){ buf [ret] = '\0’; narrate("GOT:", buf, &client_addr); time_left = alarm(O); handle_request(buf,&client_addr,addrlen); alarm(time left);
} else if (errno != BNTR) perror(”recvfrom");
} } После таких минимальных дополнений наш лицензионный сервер будет восстанавливать невозвращенные билеты по схеме периодического планирования. А почему бы не зани маться восстановлением потерянных билетов, лишь когда у сервера нет билетов и клиент может получить отказ на его запрос? Не будет ли такое решение лучше, чем предыдущее?
13.7.2. Управление при возникновении аварийных ситуаций на сервере Крах сервера имеет два серьезных последствия. Во-первых, теряется учетный список. Не остается информации о том, каким процессам были выданы билеты. Во-вторых, кли енты далее не смогут работать, поскольку программы, которая разрешает работу, не суще ствует. Очевидным решением будет повторный запуск сервера (см. рисунок 13.10).
474
Программирование с использованием дейтаграмм. Лицензионный сервер
При повторном запуске сервера могут запускаться и новые клиенты, но возникают две но вые проблемы. Первая новая проблема заключается в том, что массив выданных билетов во вновь старто вавшем сервере сначала будет пустым. Сервер имеет “свежий” набор не выданных биле тов. Но до возникновения аварийной ситуации массив выданных билетов мог быть полностью заполненным. А новый сервер готов с охотой выдавать разрешение на работу новым клиентам. Уничтожение и повторный старт сервера аналогичен процедуре печата нья денег. Другая новая проблема заключается в том, что могут быть клиенты, которые имеют билеты, которые были получены от предыдущего сервера. Они должны быть опо вещены о том, что теперь эти билеты объявлены фиктивными. Легализация билетов Решение этих двух проблем заключается в использовании механизма легализации биле тов. Механизм легализации предполагает, что каждый клиент посылает серверу через регулярные интервалы времени копию своего билета. Клиент при обращении к серверу посылает ему дейтаграмму, которая по смыслу является вопросом: ” Вот мой билет. Он все еще пригоден? ” (см. рисунок 13. И).
В билете должен содержаться индекс массива и PID. Сервер обращается к таблице. Если слот пустой, то сервер должен предположить, что он выдал билет клиенту в своей “преж ней жизни”. Сервер должен будет добавить запись в таблицу. Постепенно, по мере того как клиенты будут представлять свои билеты на признание их Легальности, таблица будет повторно заполнена. Перестроение таблицы в сервере при проведении процедуры легализации билетов решает проблему потери таблицы, но приводит к другим проблемам. Если новый клиент обратит ся с запросом на билет до того, как произойдет полное восстановление таблицы, то сервер может выдать билет под йомером, который уже был выдан другому клиенту.
13.7. Программирование с учетом существующих реалий
475
Когда клиент, у которого уже был билет с таким же номером, представит свой билет для проверки его на легальность, то сервер не подтвердит легальность этого билета. Можно предложить решение, когда сервер признает сразу нелегальными все билеты, которых нет в таблице. Клиенты, которые владеют такими отвергнутыми билетами, про сто должны будут запросить новый билет. Будет ли такое решение лучше? Добавление проверки легальности в протокол Проверка легальности билета - это новая транзакция в протоколе: Клиент: VALD tickid Сервер: GOOD или FAIL, если билет нелегален
Сделаем изменения в клиенте и сервере с целью добавления средства проверки легаль ности билетов. Добавление к клиенту средства проверки легальности билетов Добавим средство для проверки легальности билета клиента путем добавления функции проверки легальности и путем вызова этой функции из функции main в программе. Поток управления в клиенте теперь будет иметь вид, показанный на рисунке 13.12.
Клиент может планировать проведение проверок на легальность билета, используя для этого любую, наиболее подходящую схему. Клиент может установить таймер и проводить проверку на легальность по схеме периодического планирования. Если клиентская про грамма представляет собой электронную таблицу, то проверка на легальность может про ходить после выполнения определенного объема вычислений. Лицензионная версия игры в пинг-понг может проводить проверку легальности после каждого удара по шарику. В клиентской программе SuperSleep интервал в 10 секунд может быть разбит на два ин тервала по 5 секунд. Между этими интервалами можно проводить проверку на легаль ность билета программы. Это изменение нужно сделать в качестве упражнения. Проверка легальности билетов на сервере Для добавления проверки легальности билетов на сервере нужно сделать два изменения. Прежде всего, необходимо добавить функцию для проверки легальности:
Программирование с использованием дейтаграмм. Лицензионный сервер
476
у*********************************************************************** * do_validate * Проверяет легальность билета клиента * IN msg_p сообщение, принятое от клиента * Результат: указатель на ответ * ЗАМЕЧАНИЕ: результат записывается в статический буфер, который переписывается * при каждом вызове.
*/ static char *do validate(char *msg)
{ int pid, slot; /* компоненты билета */
Г сообщение будет в формате: VALD pid.slot - разобрать его и проверить на легальность */ if {sscanf (msg+5, ”%d .%d”, &pid, &slot>==2 && ticket_array [slot]==pid) returnf'GOOD Valid ticket"); /* плохой билет */ narratef'Bogus ticket", msg+5, NULL); returnf’FAIL invalid ticket");
} Сделаем еще добавления в handle_request: handle request(char *req,struct sockaddr in ‘client, socklen t addlen)
{ char ‘response; int ret; Г обработка и подготовка ответа */ if (strncmp(req, "HELO", 4) == 0) response = do_hello(req); else if (strncmp(req, "GBYE", 4) == 0) response = do_goodbye(req); else if (strncmp(req, "VALD", 4) == 0) response = do_validate(req); else response = "FAIL invalid request"; /* послать ответ клиенту */ narrate("SAID:", response, client); ret = sendto(sd, response, strlen( response), 0, client, addlen); if (ret ==-1) perrorfSERVER sendto failed”);
}
13.7.3. Тестирование версии2 Теперь откомпилируем и протестируем эти новые версии нашего клиента и сервера. Тест предполагает, что будут убиваться клиенты и сервера и будут повторно стартовать новые клиенты и новые сервера. Обратите внимание на P1D и сообщения от программ. Для дос тижения целей тестирования клиент будет “спать” на двух интервалах по 15 секунд каж дый, а сервер будет проводить проверку легальности билетов каждые 5 секунд. Результа ты тестирования будут такими:
17. Программирование с учетом существующих реалий
$ сс Iserv2.c Iserv_funcs2.c dgram.c -о Iserv2 $ сс Iclnt2.c Iclnt_funcs2.c dgram.c -о Iclnt2 $ ./Iserv2 & # стартовать сервер [1] 30804 $ ./Iclnt2 &./lclnt2 &./lclnt2 & # запуск на исполнение трех кли [2] 30805 [3] 30806 [4] 30807 $ SERVER: GOT: НЕЮ 30805 (10.200.75.200:1085) SERVER: SAID: TICK 30805.0 (10.200.75.200:1085) CLIENT [30805]: got ticket 30805.0 SuperSleep version 1.0 Running - Licensed Software SERVER: GOT: HELO 30806 (10.200.75.200:1086) SERVER: SAID: TICK 30806.1 (10.200.75.200:1086) CUENT [30806]:'got ticket 30806.1 SuperSleep version 1.0 Running - Licensed Software SERVER: GOT: HELO 30807 (10.200.75.200:1087) SERVER: SAID: TICK 30807.2 (10.200.75.200:1087) CUENT [30807]: got ticket 30807.2 SuperSleep version 1.0 Running - Licensed Software
$ kill 30806 # убить клиента [3]- Terminated./lclnt2 SERVER: freeing 30806.1 SERVER: GOT: VALD 30805.0 (10.200.75.200:1085) ' SERVER: SAID: GOOD Valid ticket (10.200.75.200:1085) CUENT [30805]: Validated ticket: GOOD Valid ticket SERVER: GOT: VALD 308Q7.2 (10.200.75.200:1087) SERVER: SAID: GOOD Valid ticket (10.200.75.200:1087) CUENT [30807]: Validated ticket: GOOD Valid ticket
$ kill 30804 # убить сервер [1] Terminated ./Iserv2
$ ./Iserv2 & # стартовать новый сервер [5] 30808
$ SERVER: GOT: GBYE 30805.0 (10.200.75.200:1085) SERVER: Bogus ticket 30805.0 SERVER: SAID: FAIL invalid ticket (10.200.75.200:1085) CUENT [30805]: release failed invalid ticket SERVER: GOT: GBYE 30807.2 (10.200.75.200:1087) SERVER: Bogus ticket 30807.2 SERVER: SAID: FAIL invalid ticket (10.200.75.200:1087) CUENT [30807]: release failed invalid ticket
$ ./clnt2
# начать исполнение нового клиента
SERVER: GOT: HELO 30809 (10.200.75.200:1087) SERVER: SAID: TICK 30809.0 (10.200.75.200:1087) CUENT [30809]: got ticket 30809.0
478
Программирование с использованием дейтаграмм. Лицензионный сервер SuperSleep version 1.0 Running - Licensed Software SERVER: GOT: VALD 30809.0 (10.200.75.200:1087) SERVER: SAID: GOOD Valid ticket (10.200.75.200:1087) CUENT [30809]: Validated ticket: GOOD Valid ticket SERVER: GOT: GBYE 30809.0 (10.200.75.200:1087) SERVER: SAID: THNXSeeya! (10.200.75.200:1087) CUENT [30809]: released ticket OK ./Iclnt2 [2] Done ./Iclnt2 [4]- Done
$ $ps PID TTY 23509 pts/3 30808 pts/3 30810 pts/3
TIME CMD 00:00:00 bash 00:00:00 Iserv2 00:00:00 ps
$ Фу! Тест закончился успешно. Поработайте сами с этими программами и посмотрите, как они взаимодействуют.
13.8. Распределенные лицензионные сервера Лицензионная программа и лицензионный сервер связаны между собой через сокеты, а сокеты могут соединять процессы, которые развиваются на разных хостах. Теоретиче ски клиент может работать на одной машине, а сервер может работать на другой, что и происходит в Internet, когда Web броузеры и Web сервера работает на различных маши нах. А есть ли проблемы при запуске клиентов и серверов на разных машинах? Да, есть.
Проблема 1: Дублирование идентификаторов процессов (PID) На одной машине все PID являются уникальными. Но это не относится к сети. Нет ничего ошибочного или необычного в том, что изображено на рисунке 13.13:
В билетах и в таблице билетов содержится номер билета и PID. В ситуации, которая пока зана на рисунке 13.13, лицензионный сервер будет считать, что он выдал три билета одно му и тому же процессу. Это будет расценено, как ошибка. Процесс нуждается только в од ном билете для своей работы. Запрос дополнительных билетов от клиента может быть расценен, как наличие не выявленной программной ошибки у клиента.
13.8. Распределенные лицензионные сервера
479
Мы можем решить проблемы дублирования PID за счет расширения формата у билетов, а также за счет того, что содержимое таблицы билетов будет включать нечто, что могло бы идентифицировать хост, на котором работает сервер.
Проблема 2: Восстановление потерянных билетов Сервер посылает сигнал 0 всем процессам, которые имеют билет. При использовании мо дернизированной таблицы билетов сервер будет знать, на каком хосте работает клиент.
Однако, сервер не может посылать сигналы процессам на другие машины (см. рисунок 13.14). Если лицензионный сервер захочет послать сигнал процессу, который находится на host3, то для выполнения этого действия лицензионный сервер должен будет послать требование на host3. А почему бы не запустить лицензионный сервер на каждом хосте сети? Каждый такой локальный сервер будет тогда поддерживать свою службу восстановления потерянных би летов (см. рисунок 13.15).
При использовании локальных серверов решается проблема получения сигналов от дру гих хостов, но опять возникают такие вопросы: Какой сервер выдавал билет? Как сделать, чтобы основной сервер имел бы коммуникации с локальными серверами? Кому клиент должен посылать билет для определения его легализации?
480
Программирование с использованием дейтаграмм. Лицензионный сервер
Проблема 3: Крах хостов Что произойдет, если перестает работать одна из машин, а не программа ? Как сможет главный сервер, если он все еще работает, восстанавливать билеты? Как смогут клиент ские программы, если они все еще работают, проверять легальность своих билетов? Если произойдет крах главного сервера, то кто будет выдавать билеты?
Модели распределенной лицензионной системы Каким образом можно построить лицензионную систему, в которой одновременно в со ставе обслуживающей системы задействованы несколько машин? Есть три метода. Рас смотрим детали проекта, сильные и слабые стороны различных вариантов. Нужно также представлять себе, как в . каждом из вариантов будет работать система, если случится авария или неисправность с клиентом, сервером, компьютером или с сетью.
Решение 1: С центральным сервером общаются локальные сервера На каждой машине есть локальный сервер, аналогичный тому, что мы написали. Каждый клиент общается со своим локальным сервером. Локальный сервер передает запрос цен тральному серверу. Центральный сервер передает назад билет или отказ на выдачу биле та. Локальный сервер записывает и передает ответ клиенту. Локальный сервер также дол жен пресекать попытки нарушить установленные пределы. Например, попытку нарушить предел на максимальное число запусков программы, установленный для этой машины, или попытку запуска программы вне установленного времени дня.
Решение 2: С центральным сервером общаются все Клиенты посылают запросы непосредственно серверу на конкретном хосте. Локальные сервера работают на каждом хосте, но эти сервера не общаются с локальными клиентами. Вместо этого локальные сервера выступают в роли агентов центрального сервера по вос становлению билетов.
Решение 3: Локальные сервера общаются с локальными серверами На каждой машине есть локальный сервер, подобный тому, что мы написали. Каждый клиент общается со своим локальным сервером. Все локальные сервера общаются друг с другом. Каждый раз, когда клиент запрашивает билет, то локальный сервер обращается ко всем другим серверам с вопросом, сколько билетов они выдали. Если общее число вы данных билетов меньше установленного лицензий предела, то локальный сервер выдает билет клиенту.
13.9. UNIX-сокеты доменов Наш лицензионный сервер использует стандартный способ адресации сокетов на основе указания идентификатора хоста и номера порта. При использовании Internet адресов сервер на одной машине может принимать запросы от клиентов другой машины как в ло кальной, так и в глобальных сетях. А что, если клиенту нужно поддерживать связь только сервером своей собственной маши ны? Это происходит в двух моделях распределенной лицензионной системы. Может ли сокет быть использован для внутренних коммуникаций?
13.9.1. Имена файлов, как адреса сокетов Есть два вида соединений - потоки и дейтаграммы. И есть также два типа адресации соке тов - Internet-адреса и локальные адреса. Internet-адрес состоит из идентификатора хоста и номера порта. Локальный адрес - это имя файла. Его обычно называют адресом Unix-doмена.
13.9. UNIX-сокеты доменов
481
Не имя хоста, не номер порта, а именно имя файла (такое как, например, /dev/log, /dev/printer или /tmp/lserversock). Два имени сокета /dev/log и /dev/printer используются во многих системах Unix. Имя /dev/log используется сервером (syslogd), который ведет журнал системы. Программы, которые же лают записать сообщение в системный журнал, посылают дейтаграммы в сокет с адресом /dev/log. В некоторых системах печати в Unix используется сокет по адресу /dev/printer.
13.9.2. Программирование с использование сокетов доменов Для того, чтобы ознакомиться, как строятся клиент/серверные системы, которые исполь зуют сокеты доменов, мы напишем систему журналирования. Примером системы журна лирования является файл wtmp. В файл wtmp записывает информация о всех входах и вы ходах пользователей в/из системы, а также информация о других коммуникациях. Систе мы журналирования используются системными программами поддержки работоспособ ности и программами по поддержанию безопасности, которые ведут записи по мере появ ления подозрительной активности. Сервер-журнал (log server) является “писцом”. Клиен ты посылают сообщения серверу, а сервер добавляет эти сообщения к файлу, который может модифицировать только он. Сервер-журнал может поддерживать этот файл в любом месте, где он захочет, в любом формате, который он захочет. Никому из клиентов не нужно знать об этих деталях. Мы будем использовать Unix доменный адрес для сокета дейтаграмм для нашего серверажурнала. На этот сокет могут посылать дейтаграммы только клиенты, которые находятся на той же машине, на которой находится сокет. Далее представлен к^д клиента и сервера для системы журналирования, где используются сокеты доменов. Сервер создает сокет и связывает (binds) с ним адрес: ^************************************************************************ ’ logfiled.c - простой сервер ведения журнала, использующий дейтаграммные * сокеты доменов * Использование: logfiled »имя_журнала
7 <stdio.h> «include <sys/types.h> «include <sys/socket.h> «include <sys/un.h> «include «include «define MSGLEN 512 tdefine oops(m,x) {perror(m); exit(x);} tdefine SOCKNAME “/tmp/logfilesock" int main(int ac, char *av[])
{ int sock; struct sockaddr_un addr; socklenj addrlen; char msgfMSGLEN]; int I; char socknamefl = SOCKNAME; timej now; _ int msgnum = 0;
/* сюда будут читаться сообщения */ /* это адрес */
482
Программирование с использованием дейтаграмм. Лицензионный сервер char *timestr;
Г построение адреса */ addr.sun family = AF_UNIX; /* семейство адресов AF_UNIX */ strcpy(addr.sun_path, sockname); /* адресом является имя файла */ addrlen = strl en (sockname) + sizeof(addr.sun_family); sock = socket(PF_UNIX, SOCK_DGRAM, 0); /*заметьте: семейство протоколов PFJJNIX */ if (sock == -1) oops("socket",2); Г связывание адреса */ if (bind(sock, (struct sockaddr *) &addr, addrlen) == -1) oops("bind", 3); /* чтение и запись */ while( 1)
{ I = read(sock, msg, MSGLEN); /* прочитать задание на работу для DGRAM */ msg[l] = '\0'; /* это будет строка */ time(&now); timestr = ctime(&now); timestr[strlen(timestr)-1 ] = '\0'; /* отметка newline */ printf("[%5d] %s %s\n”, msgnum++, timestr, msg); fflush(stdout);
} } Мы все также используем socket и bind для создания сокета для сервера. Тип сокета SOCK DGRAM, а семейство протоколов - PF UNIX3. Адресом сокета является имя файла. Код клиента будет короче: j'kick'k'kick'k-k-k'k-k'k-k'k'k'k-k
* logfilec.c - клиент системы журналирования. Посылает сообщения серверу-журналу * Использование: logfilec "здесь_находится сообщение"
7
'
«include <stdio.h> «include <sys/types.h> «include <sys/socket.h> «include <sys/un.h> «define SOCKET "Дтр/logfilesock" «define oops(m.x) { perror(m); exit(x);} main{int ac, char *av[])
{ int sock; struct sockaddr_un addr; socklenj addrlen; char sockname[] = SOCKET; char *msg = av[1]; if (ac != 2){ 3. Вместо PF_UNIX может быть использован PF_LOCAL.
13.10. Итог: сокеты и сервера
483
fprintf(stderr,"usage: logfilec 'message'\n”); exit(1);
} sock = socket(PF_UNIX, SOCK_DGRAM, 0); if (sock ==-1) oops("socket",2); addr.sunjamily = AFJJNIX; strcpy(addr.sun_path, sockname); addrlen = strlen(sockname) + sizeof(addr.sun_family); if (sendto(sock,msg, strlen(msg), 0, &addr, addrlen) == -1) oops("sendto",3);
) Мы используем socket для создания сокета и sendto для посылки сообщения. Сервер прини мает сообщение и затем печатает сообщение, предваряя его номером сообщения и вре менной отметкой. Вот пример работы: $ сс logfiled.c -о logfiled $ ./logfiled » visitorlog& 1500
$ сс logfilec.c -о logfilec $ ./logfilec 'Nice system. Swell software!' $ ./logfilec "Testing this log thing.” $ ./logfilec "Can you read this?" $ cat vistorlog [ 0] Mon Aug 20 18:25:34 2001 Nice system. Swell software! [ 1 ] Mon Aug 20 18:25:44 2001 Testing this log thing. [ 2] Mon Aug 20 18:25:48 2001 Can you read this?
Эти две короткие программы показывают, как можно использовать сокеты доменов и де монстрируют концепцию построения сервера-журнала. Еще одним свойством этой систе мы является то, что она реализует режим autoappend без использования флага 0_APPEND. Сервер выбирает по одному сообщению и присоединяет это сообщение к тексту файла. Даже если несколько клиентов одновременно пошлют сообщения серверу, то механизм управления сокетом обеспечивает последовательную обращение к сокету.
13.10. Итог: сокеты и сервера Сокеты представляют собой мощное, многоцелевое средство для передачи данных между процессами. Мы рассмотрели два типа сокетов и два типа адресов сокетов: домен сокет SOCK_STREAM SOCKDGRAM
PFJNET PFJJNIX Связность, межмашинная связь Связность, локальная связь Дейтаграммы, межмашинная связь Дейтаграммы, локальная связь
В последних нескольких главах мы имели дело с проектами, где использовались три из этих четырех комбинаций. Помните об этой диаграмме, когда у вас возникнет потребность работать с Unix программами и когда вы будете разрабатывать собственные проекты. Какого сорта сообщения вы посылаете? Насколько часто вы хотели бы, чтобы они приходили?
484
Программирование с использованием дейтаграмм. Лицензионный сервер
Заключение Основные Идеи •
•
•
• •
•
Дейтаграммы представляют собой короткие сообщения, которые передаются от одно го сокета к другому. Сокеты дейтаграмм не требуют установления соединения. Каж дое сообщение содержит адрес назначения. Сокеты дейтаграмм (IDP) проще, быстрее и меньше загружают систему, чем сокеты потоков (TCP). Лицензионный сервер - это программа, которая отслеживает выполнение правил использования лицензионных программ. Лицензионный сервер выдает право на исполнение программ. Реализация этого права представлена в форме коротких биле тов. Лицензионный сервер ведет учет - какие билеты и каким процессам были выделены. Эта информация хранится во внутренней базе данных. Этим лицензионный сервер отличается от Web сервера. Сервера, которые имеют дело с записями о состоянии системы, должны быть спроек тированы так, чтобы реагировать на возникновение аварий у клиентов и серверов. Для работы системы лицензирования могут быть использованы несколько машин сети. Возможны несколько вариантов построения таких систем. У каждого варианта есть сильные и слабые стороны. Сокеты могут использовать два вида адресов: сетевой и локальный. Сокеты с локаль ными адресами называют сокеты доменов Unix или именованными сокетами. Эти сокеты используют имена файлов в качестве адресов и могут обмениваться данными с сокетами, которые находятся на одной и той же машине.
Что дальше? Н;ами было рассмотрено два метода, которые используют сервера по управлению множе ством запросов. Лицензионный сервер принимает запросы в форме дейтаграмм и выдает ответы по одному во времени. Web сервер принимает запросы как поток данных и исполь зует fork для того, чтобы выдавать параллельно ответы на несколько запросов. При по строении серверов может быть использован еще и третий вариант. Процесс может исполь зовать технику нитей (threads), что позволяет запускать на исполнение одновременно не сколько функций. Концепция и техника использования нитей будет рассмотрена далее.
Исследования 13.1 В примерах, где используются сокеты потоков, сервер не использует адрес клиента, ко-*
гда он отвечает ему на запрос. Как сервер узнает, куда послать сообщение для клиента? 13.2Как процесс 25915 победил процесс 25914 в борьбе за билет? (Речь идет о работе клиент/ серверной модели в параграфе 13.6.3 - Примеч. перев,) Рассмотрите последовательность операций от момента созданий каждого из клиентских процессов и моментом прибытия за проса на сервер. В мультизадачных системах процессы мультиплексируют процессор. Где должны прерываться процессы, чтобы получить результаты, которые были показаны по мере тестирования? 13.3 Как вы будете использовать один лицензионный сервер, для управления доступом к двум или большему числу программ? Опишите изменения в протоколе, структурах данных и программной логике, чтобы можно было поддерживать лицензирование не скольких программ.
Заключение
485
13.4 Существует ли потенциальная угроза для списка билетов, если функция ticket reclaim была вызвана во время обычной обработки клиентского запроса? Не будут ли проти воречивыми состояние массива и значение счетчика в этой точке кода? Как обработчик модифицирует массив и счетчик? Как могут сказаться неожиданные изменения этих величин на работу обычных функций управления? 13.5 Многократное использование P1D. Идентификаторы присваиваются процессам при их создании. Рассмотрим такую последовательность событий. Клиентский процесс, у которого PID равен 7777, получил билет, а затем неожиданно “умирает”. Вскоре по сле смерти клиента другой пользователь запускает программу и порождается новый процесс, которому был также присвоен PID, равный 7777. Когда начинает работать функция ticket_reclaim, то она обнаружит, что процесс 7777 существует. Билет, ко торый был выдан ранее процессу 7777, не был возвращен. В текущем процессе 7777 работает совсем другая программа и ему не был выделен билет Как разрешить пробле му в такой ситуации? Что нужно модифицировать в системе, чтобы избежать возник новения таких ситуаций? 13.6 Один из методов предотвращения потери массива ticket_array на сервере - писать данные в таблицу, которая будет содержаться дисковом файле. Как нужно изменить сервер, что бы реализовать эту схему (схема backup)? Предположим, что клиент (customer) предна меренно убил сервер с тем, чтобы вызвать появление новых билетов. Как будет работать схема backup в такой ситуации? 13.7 У клиента есть билет, который он получил от предшествующей версии сервера. Сервер, который слишком долго ждет проверки на легальность билетов, может обнару жить, что доступных билетов больше нет, когда к нему клиент вновь обратится за би летом. ( Вероятно, имеется в виду ситуация, когда клиент, уже имеющий билет, обра щается к серверу для легализации своего билета, а у сервера нет свободных позиций Примеч. перев.). Найдите решение в такой ситуации. Клиент не должен получить разрешение на продолжение, поскольку это приведет к нарушению установленного ли цензионного максимума. Но клиента также нельзя внезапно закончить. 13.8 Сравните три модели распределенного лицензионного контроля. Насколько они от вечают тому набору вопросов, которые были приведены в тексте? 13.9 Написание сокетов. Мы использовали write и sendto для передачи данных от одного сокета к другому. Изучите справочный материал относительно send и sendmsg. Какие есть отличия между этими методами? 13.10 Аналогия между службой выдачи ключей для автомобилей и дейтаграммами - боль ше, чем метафора. Представьте себе, что в каждой автомобильной компании есть неко торый GPS прибор, с помощью которого автомобиль может определить свое место на хождения. В автомобиле есть также компьютер и спутниковый модем, который соеди няет компьютер с Internet. Представьте также, что в автомобилях также можно управ лять зажиганием, но не с помощью автомобильного ключа, а с помощью магнитной карты, которая должна вставляться в считыватель на приборной панели. Разработайте систему, которая дает возможность водителям заказывать и использовать автомобили компании, а также позволяет менеджеру автомобильного парка следить за водителями и за местом расположения каждого автомобиля.
486
Программирование с использованием дейтаграмм. Лицензионный сервер
Программные Упражнения 13.11 Модифицируйте программу dgrecv.cc тем, чтобы она выводила бы помимо адреса от правителя время, когда было принято сообщение, а также номер сообщения. Нумера цию сообщений следует вести с нуля. Вывод должен иметь такой формат: dgrecv: got a message: testing 123 from: 10.200.75.200:1041 at: Sun Aug 19 10:22:27 EDT 2001 msg#: 23 13.12 Написать клиентскую программу dgrecv2.c, как добавление к программе dgsend.c. 13.13 Печать статуса сервера. Лицензионный сервер хранит таблицу, в которой содержится информация о клиентах, которым были выделены билеты. Что нужно сделать, чтобы сервер по вашему желанию распечатал бы содержание этой таблицы? Возможность про смотра этой таблицы может помочь вам при отладке и тестировании сервера. Можно для этого использовать сигналы - стандартный механизм для связи с процессами сервера. Модифицируйте программу Iservl так, чтобы она в ответ на сигнал SIGHUP выводила бы содержимое таблицы на стандартный вывод. Можете проверить работоспособность этой модификации с помощью команды kill 'HUP serverpid. 13.14 "Сбормусора”, метод 2. Модифицируйте лицензионный сервер так, чтобы он вызы вал ticket reclaim только в случае, когда клиенту отказано в праве на запуск программы. Каковы преимущества и недостатки такого решения? 13.15 Модифицируйте программу Iclnt2.c так, чтобы она засыпала бы на пять секунд, а затем проверяла бы легальность своего билета. Если билет легален, то клиент засыпает еще на пять секунд, а затем циклически повторяет действия. Если билет признан не легальным, то клиент должен попытаться получить другой билет. Если этот запрос будет удовле творен, то процесс продолжает нормально работать. Если же нет, то процесс сообщает пользователю о возникшей проблеме при общении с лицензионным сервером и заканчи вает свою работу. 13.16 Модифицируйте наш shell или программу с перемещением текста из предыдущих глав так, чтобы они использовали бы лицензионный сервер. Где следует добавить код для подтверждения легальности билета? Что вы будете сообщать пользователю, если билет стал не легальный, поскольку произошла авария на сервере? 13.17 Модифицируйте клиентский и серверный коды, где обеспечивается поддержка биле тов, так, чтобы можно было указывать IP адрес хоста. Будете ли вы изменять таблицу билетов? Не забудьте при внесении изменений включить в код функции подтвержде ния легальности билетов. 13.18 Реализуйте одну из трех моделей распределенного лицензионного контроля. 13.19 Одна из проблем при построении системы журналирования заключается в том, что со общения являются анонимными. Модифицируйте систему так, чтобы сообщения, ко торые будут записываться в журнал, содержали бы имя того, кто прислал сообщение. 13.20 Чтение из сокета. При построении сервера-журнала мы использовали вызов read. Напишите две новых версии сервера. В одной нужно использовать recvfrom, а в другой recv. В чем отличие этих методов при получении данных? С деталями следует познако миться в документации. 13.21 Какие изменения необходимо сделать в кодах лицензионного сервера и клиента с тем, чтобы можно было бы использовать сокеты доменов? Объясните, почему клиент должен использовать bind?
Заключение
487
13.22 Сетевой агент для раздачи игральных карт . В первой главе мы рассматривали Inter net вариант игры в бридж. В любой распределенной карточной игре программное обес печение должно моделировать одну колоду карт, гарантируя при этом, что два клиента не смогут иметь на руках одинаковые карты. Напишите две программы cardd и cardc, которые будут использовать сокеты дейта грамм для управления колодой карт. В начале работы серверная программа должна перемешать карты. Клиентская программа запускается с командной строки с тем, что бы получить карты от дилера. Пример запуска:
$ cardc get 5 4D АН 2DTDKC
показывает, что пользователь запросил пять карт и получил четверку бубен, туз червей, двойку бубен, десятку бубен и короля треф. Убедитесь, что ваша программа не выдает дважды одну и ту же карту и что в протоколе отмечается, что агент кончает раздавать карты. Какие другие транзакции было бы по лезно добавить в протокол?
Проекты Используя материал этой главы, как базовый, следует изучить и написать версии таких программ Unix: talk, rwho, streaming video servers
Глава 14 Нити. Параллельные функции
Цели Идеи и средства • • • • • •
Нить исполнения. Мультинитьевые программы. Создание и уничтожение нитей. Разделение данных между нитями становится безопасным при использовании средст ва mutex. Синхронизация передачи данных с помощью условных переменных. Передача аргументов нитям.
Системные вызовы и функции • • •
pthread_create, pthreadjoin pthread_mutex lock, pthread_mutex_unlock pthread_cond_wait, pthread_cond_signal
14.1. Одновременное выполнение нескольких нитей Не знаю как вы, а меня на самом деле приводят в состояние, близкое к помешательству, Web-страницы, которые заполнены чем-то мигающим, танцующим, кружащимся, ани мированными изображениями и рекламами. Хотя такие страницы и вызывают раздраже ние, но при их рассмотрении возникает чисто технический вопрос: как одна программа может выполнять одновременно несколько различных дел? Web-программы, где одновременно выполняется несколько действий, не только ани мируют изображения. Ваш Web-броузер может выгружать и раскомпрессировать такие изображения, которые находятся на различных серверах по всему миру. Web-броузер вы гружает эти изображения параллельно. Как может Web броузер одновременно выгружать и раскомпрессировать несколько изображений?
14.2. Нить исполнения
489
Но не только броузер может выполнять несколько дел одновременно. Web-cepeep может читать изображения с диска и может одновременно посылать их броузерам, поскольку он связан , возможно, с сотнями Web-броузерами. Как сервер может одновременно осущест влять такого рода пересылки данных?
Не нужно думать, что мы не догадываемся! Мы уже знаем о многозадачности! Нами уже был изучен такого рода вопрос. В главе, посвященной видеоиграм, мы рас сматривали, как можно использовать один интервальный таймер и два счетчика для того, что управлять сразу перемещением по двум измерениям. В других главах мы рассматри вали, как shell и Web-cepeep используют fork и ехес для создания новых процессов, чтобы запустить на исполнение несколько параллельных программ. Почему бы ни использовать эти идеи? При использовании fork и ехес мы одновременно запускали на исполнение несколько про грамм. А что произойдет, если мы попытаемся одновременно запустить на исполнение не сколько функций или сделать несколько одновременных обращений к одной и той же функции? В этой главе мы будем изучать нити (threads). Нити связаны с исполнением функций. Нить рассматривается по отношению к функции в том же качестве, как процесс по отно шению к программе, т. е. нить - это среда, в которой будет исполняться функция. Мы бу дем писать программы, в которых будут одновременно исполняться несколько функций, причем все функции будут находиться в составе одного и того же процесса. Нашей основной целью будет создание программы, которая будет заполнять текстовый экран анимированными сообщениями. Мы создадим эту программу на основе модифика ции программы Web-cepeepa, который управлял одновременно требованиями для получе ния листингов каталогов и получения содержимого файлов. Причем нужно будет все вы полнить без создания новых процессов.
14.2. Нитьисполнения Что же все-таки представляет собой нить? Что она делает? Как можно создать нить? Начнем с изучения обычной программы, которая исполняет код последовательно - коман ду за командой. Затем, после внесения двух небольших изменений, мы включим их в про грамму и запустим две функции для параллельного выполнения.
14.2.1. Однонитьевая программа Рассмотрим такую программу:
Г hello_single.c - программа из одной нити, которая выдает hello world */ «include <stdio.h> «define NUM 5 main()
{ void print_msg(char *); print_msg("hello"); print_msg("world\n");
} void print_msg(char *m)
490
Нити. Параллельные функции for(i=0; i
} } В программе hello_single.c функция main делает два функциональных вызова,‘один за другим. При каждом выполнении функции выполняется цикл. Результат отображает внутренний поток управления:
$ сс hello_single.c -о hello_single $ ./hello_single hel loheilohel lohelioh el loworld world world world world
$ Каждое сообщение будет выдаваться с задержкой в 1 секунду. Программа при этом потра тит 10 секунд на свое выполнение. На рисунке 14.1 показан поток управления при испол нении программы.
Сначала поток управления входит в функцию main. Затем происходит обращение к функ ции printjnsg, далее поток управления проходит к следующей команде в функции main. После этого управление передается снова к функции print_msg для вторичного ее исполне ния. После исполнения функции управление передается в функцию main, в которой больше нет команд. Поэтому происходит выход из этой функции. Такой путь, полученный при трассировке команд по мере их исполнения в программе, и будет называться нитью исполнения. Традиционные программы имеют одну-единственную нить исполнения. Даже программы, где используются операторы goto и где исполь зуются рекурсивные подпрограммы, имеют одну, хотя и достаточно запутанную нить ис полнения.
14.2. Нить исполнения
491
14.2.2. Мультинитьевая программа А что произойдет, если мы попробуем одновременно обратиться к функции printjnsg, что
бы параллельно исполнить эту функцию, что будет аналогично тому, когда с помощью fork запускались одновременно два процесса? Модифицированная предшествующая картинка представлена на рисунке 14.2. Единственная нить исполнения входит в функцию main. Эта нить создает затем новую
нить, в составе которой запускается на исполнение функция print_msg. Начальная нить до ходит до следующей команды, где она стартует еще одну нить, в составе которой запуска ется второй раз на исполнение функция printjnsg. Наконец, начальная нить переходит в со стояние ожидания - ждет, когда две нити объединятся, а далее функция main моежт быть завершаена. Первая нить выходит из функции main. Деятельность людей-это постоянная мультинитьевая задача управления. Родитель, которо му требуется выполнить несколько дел, может выполнять их последовательно. Но можно поступить и так. Родитель ловит на улице двух своих детям и поручает одному из них купить молоко в бакалейном магазине, а другому поручает вернуть книги в библиотеку. Родитель остается ждать, когда они возвратятся после выполнения своих поручений. После чего он может идти домой. Нить подобна ребенку, которому вы поручили выполнить для вас некоторое дело. Если вы за хотите запустить на исполнение сразу несколько заданий, то вам необходимо иметь несколько детей. Если в программе есть требуется запустить на исполнение одновременно несколько' функций, то программа создаст для этого несколько нитей. Такой программой является hellojnulti.c, где поток управления соответствует потоку, изображенному на рисунке 14.2:
Г hello_multi.c - мультинитьевая программа для вывода hello world */ #include #include #define NUM 5 main()
<stdio.h>
{ pthreadj t1, t2; /* две нити */ void *print_msg(void *); pthread_create(&t1, NULL, print_msg, (void *)"hello"); pthread_create(&t2, NULL, print.msg, (void *)"world\n’’); pthread join(t1, NULL); pthreadjoin(t2, NULL);
} void *orint msafvoid *m)
492
Нити. Параллельные функции
{ char *ср = (char *) m; int i; for(i=0; i
} return NULL; >
Обратите внимание на изменения, которые произошли по отношению к первоначальной программе. Во-первых, мы включили новый заголовочный файл. Файл pthread.h содержит определения типов данных и прототипы функций. Во-вторых, мы определили две пере менные t1 и t2 типа pthreadj. Они представляют две нити, что аналогично двум детям, которых родитель сажает в автомобиль, и которые будут у него на побегушках. Каждой точке в потоке управления на диаграмме соответствует строка кода. Давайте рас смотрим детально каждую инструкцию кода: pthread_create(&t1, NULL, print_msg, (void *)Mhello")
Это функциональный вызов, с помощью которого выдается требование типа: “Сын мой, пожалуйста, запусти на исполнение функцию print_msg и передай ей аргумент hello”. Первый аргумент в функциональном вызове - это адрес нити. Второй аргумент - это ука затель на атрибуты нити. Указатель NULL обозначает атрибуты по умолчанию. Третий аргумент - это функция, которая должна быть выполнена в составе нити. Последний аргумент - указатель на аргумент, который вы хотели бы передать выполняемой функ ции. ! С помощью такой одной команды создается новая нить с определенными атрибутами. Эта новая нить запускает на исполнение функцию print_msg с аргументом ’’hello”. pthread_create(&t2, NULL, prirrtmsg, (void *)"world\n")
Это функциональный вызов, с помощью которого создается новая нить с атрибутами по умолчанию. Эта новая нить выполнения,запускает на исполнение функцию print_msg с аргументом " wor!d\n ". pthreadjoin(t1, NULL)
Аналогично отцу, который ждет, когда его два сына возвратятся после выполнения своих заданий, функция main будет в этом месте кода ждать возвращения с маршрутов своих двух нитей. Функция pthread join имеет два аргумента. Первый аргумент указыва ет на нить, возвращение из которой ожидается. Второй аргумент указывает на ячейку, куда будет помещено значение при выходе из нити. Если в качестве указателя записан NULL, то возвращаемое значение не воспринимается. pthreadjoin(t2, NULL)
Функция main ожидает окончания работы другой нити. Теперь откомпилируем и запустим программу на исполнение: $ сс hellojnulti.c -Ipthread -о hellojnulti $ ./hello_multi helloworld helloworld
14.2. Нить исполнения
493
helloworld helloworld helloworld
'$ Эта программа будет исполняться в течение только пяти секунд, поскольку два цикла будут работать параллельно. Существующие различия в алгоритмах управления нитями могут при вести к тому, что вы получите вывод на экран, который будет отличен от приведенного выше. Заметьте, насколько оказался гибким механизм нитей. В данном случае мы одновременно запустили на исполнение одну и ту же функцию, задавая при запуске разные аргументы. Мы можем также легко выполнить запуск различных функций, которые будут исполняться параллельно.
14.2.3. Обобщенная информация о функцииpthread_create pthread_create НАЗНАЧЕНИЕ
Создание новой нити
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ
int pthread_create( pthreadj *thread, pthread_attrt *attr, void *(*func)(void *), void *arg);
АРГУМЕНТЫ
thread - указатель на переменную типа pthreadj attr - указатель на переменную типа pthead_attrj или NULL func - функция, которая будет запущена в составе нити arg - аргумент для передачи в функцию
КОДЫ ВОЗВРАТА
Код ошибки - при обнаружении ошибки 0 - при успешном окончании
Функция pthread_create создает новую нить выполнения и вызывает функцию func(org) для исполнения в составе этой нити. Для новой нити можно задавать атрибуты с помощью аргумента attr. Аргумент func определяет функцию, для которой задан один указатель на аргумент и указатель на значение, которое будет выработано при выходе из функции. Аргумент функции и возвращаемое значение определены как указатели типа void *, что позволяет передавать упомянутые выше значения произвольного типа. Если в качестве attr задан NULL, то используются атрибуты по умолчанию. Далее будет проведено обсуждение атрибутов. Функция pthread_create возвращает 0 при успешном вы полнении и ненулевой код ошибки в противном случае. pthreadjoin НАЗНАЧЕНИЕ
Ожидание окончания нити
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ
int pthread join(pthread_t thread, void **retval)
АРГУМЕНТЫ
thread - ожидаемая нить, retval - указатель на переменяю с кодом возврата.
КОДЫ ВОЗВРАТА
Код ошибки - при обнаружении ошибки 0 - при успешном окончании нити
Нити. Параллельные функции
494
Функция pthreadjoin блокирует вызывающую нить до тех пор, пока не закончится нить thread. Если аргумент retval не равен нулю, то код возврата из нити будет помещен в пере менную, на которую указывает retval. Функция pthreadjoin возвращает 0, когда произойдет возврат из нити. Функция pthreadjoin возвращает ненулевой код ошибки в случае возникновения ошибки. Ошибкой для нити будет ожидание завершения нити, которая не существовала. Будет ошибкой ждать нить, если ее ждет кто-то еще. Ошибкой для нити будет ожидание собственного окончания. Программирование с использованием нитей подобно установлению соглашений между несколькими людьми при необходимости выполнить ими ряд задач. Если вы будете ‘корректно управлять проектом, вы можете достичь более быстрого выполнения работы. Но вы должны быть уверены, что ваши исполнители не станут выполнять работы по-своему и что они будут выполнять необходимые задачи в правильной последовательности. Рассмотрим теперь средства для взаимодействия и координации нитей.
14.3. Взаимодействие нитей Процессы взаимодействуют между собой, используя для этого программные каналы, сокеты, сигналы, механизм exit^vait и среду. При работе с нитями все обстоит гораздо про ще. Нити исполняют функции в составе одного процесса. Так что, подобно любым функ циям в том же процессе, нити разделяют глобальные переменные. Они могут взаимодей ствовать между собой путем установления значений глобальных переменных и путем чте ния этих переменных. Одновременный доступ к памяти - это мощное, но одновременно и опасное свойство нитей.
14.3.1. Пример 1: incrprintc /* incprint.c - одна нить производит инкремент, а другая печатает
*/
tinclude tinclude tdefine NUM 5 int counter = 0; main()
{
<stdio.h>
pthreadj t1; /* одна нить */ void *prlnt_count(void *); /* ее функция */ int i; pthread create(&t1, NULL, print count, NULL); for(i = 0; i
}
pthreadJoin(t1, NULL);
}
void *print_count(void *m)
{
inti; for(i=0; i
}
return NULL;
14.3. Взаимодействие нитей
495
В программе incprint.c используется две нити. Начальная нить выполняет цикл, в котором раз в секунду производится инкремент счетчика counter. До входа в цикл начальная нить создает новую нить. Эта новая нить запускает на исполнение функцию, которая печатает значение счетчика counter. Обе функции, main и print_count, выполняются в одном и том же процессе. Поэтому каждая из них имеет доступ к переменной counter. На рисунке 14.3 по казаны две функции и глобальная переменная.
Когда функция main изменяет значение счетчика counter, то функция print counter видит сразу это изменение. В данном случае отпадает необходимость посылать новое значение счетчика через программный канал или счетчик. Теперь откомпилируем и запустим про грамму: $ сс incprint.c -Ipthread -о incprint $ ./incprint count =1 count = 2 count = 3 count = 4 count = 5
Кажется, что программа работает правильно. Одна функция модифицирует переменную, а другая функция читает значение этой переменной и отображает это значение на экране. Этот пример показывает, что функции, которые работают в составе отдельных нитей, раз деляют глобальные переменные. Наш следующий пример более интересный.
14.3.2. Пример 2: twordcount. с До появления компьютеров студенты, чтобы быть уверенными, что их курсовая работа имеет требуемый объем, считали вручную количество слов в тексте курсовой работы. Представьте себе студента, у которого на руках курсовая работа на 10 страницах. Этот студент будет сам считать количество слов в тексте из 10 страниц. Или он может найти 10 студентов и дать каждому из них отдельную страницу, в которой требуется подсчитать количество слов. Такой подсчет слов, который будет производиться параллельно на 10 страницах, пройдет гораздо быстрее. Программ wc в Unix позволяет найтй количество строк, количество слов, количество сим волов в тексте одного или более файлов. Обычно эта программа реализована как одна нить. Как можно создать мультинитьевую программу, которая могла бы подсчитывать и печатать общее число слов в двух файлах?
496
Нити. Параллельные функции
Версия 1: Две нити, один счетчик В нашей первой версии будет создаваться отдельная нить для подсчета слов в каждом фай ле. Эта идея проиллюстрирована на рисунке 14.4.
Далее представлен код данной версии - twordcountl .с:
[* twordcountl .с - вариант с нитями для счетчика слов для двух файлов. Версия 1 */ «include <stdio.h> «include «include int total_words; main(int ac, char *av[])
{
pthreadj t1, t2; /* две нити */ void *count_words(void *); if (ac != 3){ printff'usage: %sfile1 file2\n", av[0]); exit(1);
}
total_words = 0; pthread_create(&t1, NULL, count_words, (void *) av[1]); pthread_create(&t2, NULL, count_words„(void *) av[2]); pthread join(t1, NULL); pthreadjoin(t2, NULL); printf("%5d: total words\n", total_words);
}
void *count_words(void *f)
{
char ‘filename = (char *) f; FILE *fp; int c, prevc = '\0'; if ((fp = fopen(filename, "r")) != NULL){ while((c = getc(fp)) != EOF){ if (!isalnum(c) && isalnum(prevc)) total_words++; prevc = c;
}
fclose(fp); } else perror(filename); return NULL;
14.3. Взаимодействие нитей
497
В функции count_words отслеживаются отдельные слова по признаку: в конце слова должен был быть алфавитно-цифровой символ, за которым следует символ, не являющийся алфа витно-цифровым. По этому алгоритму пропускается последнее слово в файле, а строка вида “U.S.A” рассматривается как состоящая из трех слов. Откомпилируем и протестиру ем программу: $ сс twordcountl .с -Ipthread -о twcl $ ./twcl /etc/group/usr/dict/words 45614: total words
$ wc -w /etc/group /usr/dict/words 58 /etc/group 45402 /usr/dict/words 45460 total При работе twordcountl будут получены результаты, которые отличаются от результатов
при работе обычной версии команды wc. Причиной тому является использование раз личных правил обнаружения конца слова. Последовательность действий. В программе есть более тонкая проблема, чем правило определения конца слова. Обе нити инкрементируют один и тот же счетчик и могут делать это потенциально одновременно. Какие неприятности это может вызвать? В языке С не специфицировано, каким образом будет выполняться на компьютере операция вида total_words++. Может быть, что выполнение этой операции будет происходить так: total_words = total_words +1.
То есть программа заносит текущее значение переменной в регистр, добавляет к содержи мому регистра 1, затем полученный результат в регистре заносится обратно в память. Что произойдет, если обе нити попытаются одновременно выполнить инкремент счетчика, используя последовательность выборка - суммирование - запоминание?
На рисунке 14.5 показано, что обе нити выбирают одно и то же значение, инкрементируют значение регистра, а затем сохраняют в ячейке новое значение. Выполняется два инкре мента, но значение счетчика увеличится только на единицу. Как можно предотвратить влияние нитей друг на друга? Рассмотрим два решения.
498
Нити. Параллельные функции
Версия 2: Две нити, один счетчик, один mutex Общественный шкафчик для хранения, типа тех, что используются в аэропортах и на терми налах автобуса, открыт до тех пор, пока он кому-либо не понадобится. Когда некоторый человек бросает в приемник несколько монет и получает взамен ключ, то уже никто, кроме него, не будет иметь доступа к этому шкафчику. Позже, когда этот человек возвратится и от кроет ключом шкафчик (деблокирует пространство хранения), то любой человек может опять использовать его для хранения. Если наши две нити разделяют общий счетчик как средство хранения данных, то им понадобится некий способ “повесить замок” на эту пере менную. В системах, где используются нити, допустимо использовать переменные, которые называют mutual exclusion locks (замки взаимного исключения). С помощью этого механизма достигается воз можность строить систему нитей, в которой будет предотвращаться одновременный доступ к некоторой переменной, функции или к другим ресурсам. В модифицированной версии программы twordcount2.c показано, как создавать и использовать средство mutex. (В русскоязычной литературе вместо перевода названия mutual exclusion lock часто исполь зуют аббревиатуру mutex. Далее в тексте используется это сокращение. - Примеч. пер.) /* twordcount2.c - Работа со счетчиком слов с помощью нитей для двух файлов */ Г Версия 2: использование mutex для блокировки счетчика */ «include <stdio.h> «include «include int total words; /* счетчик и замок к нему */ pthread_mutex_t counter Jock = PTHREAD_MUTEX_INITIAUZER; mainfintac, char*av[])
{ pthreadj t1, t2; /* две нити */ void *count_words(void *); if (ac != 3){ printf("usage: %s filel file2\n", av[0]); exit(1);
} total_words = 0; pthread_create(&t1, NULL, count_words, (void *) av[1]); pthread_create(&t2, NULL, count_words, (void *) av[2]); pthread join(t1, NULL); pth read join (t2, NULL); printf("%5d: total words\n", total words);
} void *countwords(void *f)
{ char ‘filename = (char *) f; FILE *fp; int c, prevc = ’\0’; if ((fp = fopen(filename, "r")) != NULL){ while((c = getc(fp)) != EOF){ if (lisalnum(c) && isalnum(prevc)){ pthread_mutexJock(&counter_lock);
14.3. Взаимодействие нитей
499 total_words++; pthread.mutex unlock(&counter_lock);
} prevc = с;
} fclose(fp); } else perror(filename); return NULL;
} Программа теперь выглядит аналогично тому, что изображено на рисунке 14.6:
Мы добавили в программе только три строки. Сначала мы определили глобальную пере менную counter Jock типа pthread_mutex_t. Переменной было присвоено начальное значение. Затем мы изменили count_words так, чтобы обеспечить чередование шагов по инкременту счетчика между вызовом pthread_mutexJock и вызовом pthreadjmitex_unlock. Теперь двум нитям гарантируется безопасное разделение одного счетчика. Если одна нить вызовет pthread_mutex_lockпосле того, как другая нить закрыла mutex, то первая нить будет бло кирована до тех пор, пока не будет деблокирован mutex. После того как mutex будет разбло кирован, будет также разблокирован вызов pthread_mutex_lock. Теперь нить может проводить инкремент счетчика. На mutex может находиться произвольное число нитей, которые будут ждать, когда будет разблокирован mutex. Когда некая нить разблокирует mutex, то система управления нитями передаст управление на развитие только какой-то одной нити среди дру гих ожидающих. Но следует учитывать, что средство mutex будет полезно тогда, когда все ни ти взаимодействуют с его помощью. Если некая нить захочет инкрементировать счетчик, не используя при этом mutex, то ничто не может ее остановить.
pthreadjnutexjock НАЗНАЧЕНИЕ
Ожидание и блокировка mutex
INCLUDE
«include
ИСПОЛЬЗОВАНИЕ
int pthread_mutex_lock(pthread_mutex_t ‘mutex)
АРГУМЕНТЫ
mutex - указатель на объект взаимного исключения
КСДЫ ВОЗВРАТА
0 - при успешном окончании Код ошибки - при обнаружении ошибки
500
Нити. Параллельные функции
С помощью pthreadjnutexjock производится блокировка заданного mutex. Если mutex не за блокирован, то он закрывается и становится собственностью нити, которая выполнила этот вызов. Код возврата при этом из pthread_mutex_lock будет равен 0. Если при обращении оказывается, что mutex уже заблокирован другой нитью, то вызывающая нить будет при остановлена до тех пока, mutex не будет разблокирован. Если при выполнении вызова воз никла ошибка, то pthreadjnutexjock возвращает код ошибки. pthreadjnutexjjnlock НАЗНАЧЕНИЕ
Разблокировка mutex
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ
int pthread_mutex_unlock(pthread_mutex_t *mutex)
АРГУМЕНТЫ
mutex - указатель на объект взаимного исключения
КОДЫ ВОЗВРАТА
0 - при успешном окончании Код ошибки - при обнаружении ошибки
При выполнении pthread_mutex_unlock снимается блокировка указанного mutex. Если в дан ный момент на этом mutex были блокированы нити, то одной из них разрешается блокиро вать mutex, pthread mutex unlock возвращает 0 при нормальном окончании. И возвращает не нулевой код ошибки при возникновении каких-то проблем. Мы рассмотрели случай, когда все работает хорошо. А что произойдет, если нить попыта ется заблокировать mutex, который она уже заблокировала? Что произойдет, если нить заканчивается и не выполнила до этого момента разблокирования mutex? В разных систе мах управления нитями управление такими ситуациями происходит по-разному. Обрати тесь к вашей документаций за деталями. Нужен ли нам mutex? Если обе нити должны одновременно модифицировать одну и ту же переменную, то они могут использовать mutex, чтобы обеспечить правильный доступ к переменной. При использовании mutex программа работает более медленно. На про верку замка, на установку замка, на освобождение замка в отношении каждого слова из обоих файлов суммарно будет выполнено много действий. Более эффективное решение будет основано на том, что каждая нить получает в свое распоряжение собственный счетчик.
Версия 3: Две нити, два счетчика, несколько аргументов для нитей В следующей версии программы, которая занята подсчетом слов, мы откажемся от mutex и вместо этого предоставим каждой нити собственный счетчик. После окончания работы нитей значения двух счетчиков будут суммированы. Как мы установим эти счетчики для нитей и как нити возвратят обратно полученные значения в счетчиках? В обычном варианте в однонитьевой программе функция подсчета слов возвращает число слов в вызывающую функцию. Нить может возвратить значение при обращении к pthread_exit. Это значение далее можно получить с помощью pthreadjoin. В справочнике приведены детали. Мы будем использовать другой, более простой метод. Вместо метода, когда нить передает назад значение счетчика, вызывающая нить может передать для функции указатель на переменную. И далее функция будет в состоянии ин крементировать эту переменную. Но при передаче этого указателя возникает проблема. Мы уже передаем нити имя файла. А при обращении к pthread_create можно передавать только один аргумент. Как можно передать при обращении к нити имя файла и имя счетчика? Легко. Мы создадим структуру с двумя элементами и передадим адрес ^той структуры. Вот каким станет код:
.3. Взаимодействие нитей
Г twordcount3.c - Работа со счетчиком слов с помощью нитей для двух файлов *
- Версия 3: по одному счетчику на файл
7 #include <stdio.h> «include tinclude struct arg_set { Г два значения в одном аргументе 7 char *fname; Г файл для обработки */ int count; /* число слов */
}; main(int ас, char *av[])
{ pthread_t t1, t2; /* две нити 7 struct arg_set argsl, args2; /* два набора аргументов */ void *count words(void *); if (ac != 3)f printf("usage: %s filel file2\n", av[0]); exit(1);
}. args1.fname = av[1]; argsl .count = 0; pthread_create(&t1, NULL, count_words, (void *) &args1); args2.fname - av[2]; args2.count = 0; pthread_create(&t2,.NULL, count_words, (void *) &args2); pthread join(t1, NULL); pthreadJoin(t2, NULL); printf(”%5d: %s\n“, argsl .count, av[1 ]); printf("%5d: %s\n", args2.count, av[2]); printf("%5d: total words\n", argsl .count+args2.count);
} void *count words(void *a)
{ struct arg_set *args = а; Г сброс аргумента правильного типа */ FILE *fp; int с, prevc = ’\0'; if ((fp = fopen(args->fname, "r")) != NULL){ while((c = getc(fp)) !=EOF){ if (!isalnum(c) && isalnum(prevc)) args->count++; prevc = c;
} fclose(fp); }else perror(args->fname); return NULL;
502
Нити. Параллельные функции
Мы решили проблему передачи двух аргументов с помощью определения структуры, в которой будет содержаться имя фащт и число слов в этом файле. В функции main мы определили две такие структуры как локальные переменные, инициализировали струк туры и передали адреса структур нитям (см. рисунок 14.7). При передаче указателей на локальные структуры не только отпадает необходимость в mutex, но мы также отказыва емся от использования и глобальных переменных.
Прц каждом обращении к count_words будет использован указатель на разные структуры, так что нити будут читать различные файлы и инкрементировать значения через разные указатели. Структуры являются локальными переменными в функции main. Поэтому память, которая выделяется для счетчиков, сохраняется до тех пор, пока не кончится исполнение main.
14.3.3Взаимодействие между нитями: итог Процесс содержит все свои переменные в пространстве данных. Все нити, которые были запущены в процессе, имеют доступ к таким переменным. Если эти переменные никто не изменял, то нити могут их читать и использовать их значения без каких-либо аномалий. Если некие нити в процессе будут намерены модифицировать переменную, то все другие нити, которые используют значение переменной, должны использовать некоторый метод по установлению правильного взаимодействия, чтобы предотвратить искажение данных. В каждый момент времени только одна нить должна использовать переменную. Выше были представлены три версии программы для подсчета слов, три метода, позво ляющие разделять данные между процессами. Первый метод, который был использован в программе twordcountl .с, использовал метод, который предоставляет нитям возможность модифицировать одну и ту же переменную напрямую, без установления каких-либо форм взаимодействия. Такая программа работает, но работает неправильно. Второй метод, который использован в программе twordcount2.c, был основан на применении mutex. Метод гарантировал, что только одна из нитей сможет в любой момент инкремен тировать значение разделяемого счетчика. Эт*а програ(мма работает, но требует многократ ного вызова функций по проверке, установке и снятию ’’замка” (блокировки). Третий метод, который использован в программе twordcount3.c, был основан на применении не одного разделяемого счетчика, а на создании отдельного счетчика для каждой нити. Нити в программе больше не разделяют переменную, поэтому нет необходимости исполь зовать некоторое средство для организации взаимодействия нитей друг с другом. Но нити по-прежнему взаимодействуют с начальной нитью. В частности, начальная нить не долж на читать значение счетчика до тех пор, пока не завершатся две другие нити. Для этого
14.4. Сравнение нитей с процессами
503
начальная нить использует pthreadjoin, чтобы блокироваться до тех пор, пока не закончатся другие нити. Когда эти нити закончат работу, то вызов pthreadjoin будет разблокирован, что обеспечит эффективный неблокированный доступ к счетчику. Функции main будет пре доставлена возможность прочитать значение. В третьей версии также показано, каким образом можно передавать нити несколько аргу ментов при вызове функции. Для этого} мы создали одну структуру, в которой содержались все необходимые аргументы, а затем был передан адрес этой структуры. Нить может чи тать и модифицировать любой член данной структуры. Другие функции, если они имеют доступ к этой структуре, увидят в структуре новые значения. Естественно, если более чем одна нить захочет изменять эти значения, то должен быть использован механизм mutex, чтобы предотвратить искажение данных.
14.4. Сравнение нитей с процессами Процессы, как сущности, были введены в Unix с самого начала. Нити появились позже. Модель процесса ясна и универсальна. Концепция нитей появилась по многим причинам. В настоящее время используются несколько типов нитей, с различными атрибутами. Мы рассмотрим примеры таких нитей на примере использования интерфейса, названного POSIX threads. Мы не рассматриваем вопросы эффективности и планирования. Ответы на эти вопросы зависят от версии Unix и от версий нитей, которые вы используете. Процессы отличаются от нитей рядом фундаментальных свойств. Каждый процесс имеет: собственное пространство данных, файловые дескрипторы и идентификатор процесса PID. Нити же разделяют: одно пространство данных, набор файловых дескрипторов и PID. Меха низм реализации нитей является значимым для программистов. Разделяемое пространство данных. Рассмотрим систему управления базой данных, которая управляет большой, сложной трехуровневой базой данных. Один процесс может быть занят обслуживанием множества запросов от клиентов. Переменные не изменяются. При разделении такого набора данных не возникает каких-либо проблем. Теперь рассмотрим программу, где для управления памятью используются вызовы таНоси free. Для одной нити выделяется участок памяти,, в которой нить намерена хранить одну строку. Когда эта нить не заблокирована, то другие нити могут обратиться к free и освобо дить этот участок памяти. В начальной нити может быть использован указатель на уже ос вобожденную память или, еще хуже, освобожденный участок памяти будет перераспреде лен для хранения новых данных. Использование механизма нитей может привести к накоплению памяти. Программист, испуганный возможностью потерять память, которая происходит при работе нитей с ука зателями на память, может перестраховаться и никогда не возвращать память. Функции, которые в однонитьевой модели возвращают указатели на статические локаль ные переменные, на смогут работать в мультинитьевой модели. В такой модели несколь кими нитями одновременно может быть активизирована одна и та же функция. Короче говоря, если количество разделяемых переменных будет большим, а переменные не будут хорошо определены, то отладка приложения, которое использует мультинитьевую модель, превратится в кошмар. Разделяемые файловые дескрипторы. Файловые дескрипторы автоматически дуб лируются при выполнении fork. Поэтому дочерний процесс сразу получает набор файло вых дескрипторов. Если дочерний процесс закрывает файловый дескриптор, который он унаследовал от процесса-отца, то файловый дескриптор у процесса-отца остается откры тым. В мультинитьевой программе возможна передача одного и того же файлового
504
Нити. Параллельные функции
дескриптора двум разным нитям. Но при этом речь идет именно об одном и том же файло вом дескрипторе. Если некая функция в одной нити закроет файл, то этот файловый де скриптор будет закрыт для всех нитей процесса. А для других нитей может еще требовать ся связь с файлом. fork, ехес, exit, сигналы. Все нити разделяют один и тот же процесс. Если нить вызвала ехес, то ядро произведет смену программы - текущая программа будет заменена на новую. Все работающие нити будут потеряны, что для них будет неприятным сюрпризом. Если одна из нитей вызовет exit, то это приведет к окончанию процесса. Что будет, если по вине нити прозошла ошибка, связанная с нарушением адресации (segmentation violation) или произошли какие-то еще системные ошибки и нить по этой причине должна быть аварийно закончена? В такой ситуации будет закончен весь процесс, а не только такая аварийная нить. Вызов fork создает новый процесс, который содержит точную копию кода и данных вызы вающего процесса. Если функция в одной нити вызывает fork, то будут ли дублированы другие нити в новом процессе? Нет. В новом процессе будет запущена только одна нить та, которая вызвала fork. Что произойдет, если нить была занята модификацией неких дан ных и в это время другая нить вызвала fork? Будут ли в этой ситуации сохранены данные в новом процессе? При работе с нитями с сигналами дело обстоит еще сложнее. Процессы могут принимать все виды сигналов. А кто принимает сигнал? Каждая из нитей или нет? Как быть с сигна лами, которые возникают по причина нарушения адресации памяти или при возникнове нии ошибок при передаче по шине? Кто должен получить такие сигналы? Обратитесь к документации за разъяснениями об особенностях обработки сигналов нитями. Эксперимент над вами. В этой главе было сделано введение в базовые идеи, касающиеся нитей, были рассмотрены основные проблемы и проекты, что дает вам возможность поду мать о том, что вы получите при использовании нитей. Хорошим упражнением будет про граммирование двух решений одной и той же проблемы: одно программируется с исполь зованием механизма нитей, а другое программируется с использованием процессов. Какое из программных решений будет более ясным на этапах проектирования, кодирования и отладки? Какой из вариантов выполняется более быстро? Какой вариант в большей степени перено сим на разные версии Unix?
14.5. Уведомление для нитей Обратимся еще раз к мультинитьевой программе подсчета слов. Представьте себе, что вы председатель комиссии по выборам в большом городе. Когда закончатся выборы, то не большие избирательные участки закончат считать голоса раньше. У них на участке заре гистрировано меньше избирателей, и поэтому требуется меньше времени на подсчет голо сов. Вы, как председатель, не объявляете общий итог голосования до тех пор, пока не по ступят сведения от всех избирательных участков. Но вы хотели бы ознакомиться с каж дым отчетом, когда он поступит из участка. Подсчет слов в файле происходит аналогично подсчету голосов на участках. Некоторые файлы будут длиннее других, и поэтому потребуется больше времени для их обработки. Интересно посмотреть, что случится при обращении вида: twordcount большой_файл неболыиой_файл
Начальная нить использует pthread_wait для того, чтобы ждать, пока не финишируют две другие нити. В нашем примере вторая нить получит свой результат задолго до того, как получит результат первая нить. Как может более быстрая нить уведомить начальную нить о том.-что она выполнила свою оаботу?
14.5. Уведомление для нитей
505
Как может одна нить уведомить другую нить? Когда нить закончит подсчет слов, то она кончает свою работу. Как эта нить может оповестить начальную нить о том, что у нее уже готовы результаты? При использовании механизма процессов системный вызов wait заканчивался (заканчивалось ожидание), когда заканчивался произвольный дочерний про цесс. Можно ли использовать подобный механизм в отношении нитей? Может ли некая нить ждать окончания работы произвольной нити? Нет. Нити не могут работать по такому сценарию. Нити не имеют родителя, поэтому не очевидно - кого следует уведомлять при окончании работы нити.
14.5.1. Уведомление для центральной комиссии о результатах выборов Когда избирательные комиссии закончат подсчет голосов, то они должны будут отправить полученные результаты в центральную комиссию. Рассмотрим следующую систему, которая работает в центральной комиссии и собирает результаты от участковых комиссий (как это ни странно звучит, но в данной ситуации все происходит точно так, как происхо дит передача уведомлений между нитями об определенных событиях). (a) В центральной избирательной комиссии устанавливается почтовый ящик для прие* ма сообщений о проведенном голосовании. Этот почтовый ящик может хранить только одно сообщение о голосовании от какой-либо комиссии. Такой почтовый ящик имеет "флажок, который может взводиться. (b) В центральной комиссии ждут поднятия флажка на почтовом ящике. (c) Председатель участковой комиссии посылает сообщение о своих результатах вы боров в почтовый ящик. (d) Председатель участковой комиссии взводит флажок на почтовом ящике (это будет называться сигнализацией). • (е) В центральной комиссии видят, что флажок на почтовом ящике поднялся. Тогда в ответ в центральной комиссии выполняются следующие шаги: из почтового ящика выбирается сообщение от участковой комиссии; производится обработка данных, которые поступили в сообщении; переход обратно в состояние ожидания (переход на пункт (Ь). На первый взгляд такая схема выглядит достаточно мудреной. Отправитель передает дан ные в контейнер для сообщений, а затем устанавливает флаг, который оповещает получа теля данных, что информация поступила в почтовый ящик.
506
Нити. Параллельные функции
На рисунке 14.8 изображена центральная избирательная комиссия и две участковых. Каж дая участковая комиссия отправляет отчет о голосовании в почтовый ящик и оповещает об этом центральную комиссию. В центральной комиссии после этого вынимают отчет из почтового ящика. Техническим термином, который используется для описания действия по подъему флажка на почтовом ящике, будет термин сигнализация с помощью флага. Мы можем сказать, что получатель ждет,, когда с помощью флага будет просигнализиро вано о поступлении отчета о голосовании. Действия в отношении этого флажка не имеют ничего общего с действиями над обычными сигналами в Unix. Лишь сам термин и общая идея будут действительно совпадать. На рисунке показана еще одна важная деталь: замок на почтовом ящике для отчетов о го лосовании. Почтовый ящик может одновременно хранить только один отчет. Тем самым подчеркивается, что он является критическим ресурсом. Лишь только одна персона в любой момент времени может иметь доступ к ящику. Использование замка немного усложняет процедуру, но замок обеспечивает надежность хранения. Вся процедура с ис пользованием замка будет такой: (a) В центральной комиссии устанавливается почтовый ящик для приема отчетов о голо совании. В этом ящике можно хранить только один отчет. Почтовый ящик оборудован флажком, который можно взвести и потом, после сброса, опять взвести. Почтовый ящик имеет mutex, который может быть заблокированным или нет. (b) В центральной комиссии разблокируют ящик ичначинают ждать, когда с помощью флага комиссию оповестят о поступлении отчета. (c) Председатель участковой комиссии ждет, когда он сможет блокировать почтовый ящик. Если почтовый ящик в текущий момент не пустой, то председатель участковой комиссии разблокирует ящик и опять ждет, наблюдая за флагом, когда нужно будет по вторно блокировать ящик. Председатель посылает отчет в почтовый ящик. (d) Председатель участковой комисси с помощью флага сигнализирует о поступлении отчета в почтовый ящик и снимает блокиррвку с почтового ящика. (e) В центральной комиссии выходят из состояния ожидания, поскольку обнаружили подъм флажка на ящике. Председетатель центральной комиссии блокирует почтовый ящик. Далее он вынимает из почтового ящика отчет о голосовании. Потом производится обработка поступивших данных. Председатель центральной комиссии сигнализирует с помощью флага, если этого ждут в участковой комиссии . Передается управление на шаг (Ь).
14.5.2. Программирование с использованием условных переменных Теперь мы перенесем принцип избирательной системы в программу для подсчета слов. В избирательной системе были использованы три объекта: контейнер, флаг и замок. Эти элементы соответствуют трем элементам в мире программирования: переменная, услов ный объект и mutex. На рисунке 14.9 изображены три нити и три переменные. В одной переменной находится указатель на счетчик слов. Другая переменная выступает в роли условной переменной. Третья переменная будет хранить mutex.
14.5. Уведомление для нитей
507
А теперь обратимся к алгоритму программы. Начальная нить запускает две нити, которые будут заниматься счетом. Далее начальная нить переходит в состояние ожиданий поступле ния результатов от счетных нитей. Для этого начальная нить обращается к pthread_cond_wait и ждет, когда с помощью флага будет просигнализировано об ожидаемом событии. Этот вызов блокирует начальную нить. Когда счетная нить закончит подсчет, она готова передать результат, помещая указатель на глобальную переменную в почтовый ящик mailbox. В начале нить должна приобрести за мок для почтового ящика, затем нить должна проверить почтовый ящик. Если почтовый ящик не пустой, то нить разблокирует его и будет ждать уведомления с помощью флага о возможности вновь заблокировать почтовый ящик. Далее нить помещает результат в почтовый ящик. Наконец, счетная нить с помощью условной переменной flag и вызова pthreadjX)nd_signal уведомляет о данном событии. Такое уведомление с помощью флага при водит к побудке начальной нити, которая была блокирована на этом флаге, когда она вы звала pthread_COnd-wait. Разбуженная начальная нить поспешит открыть почтовый ящик. Она попытается заблокировать этот почтовый ящик, но блокировка все еще поддерживается счетной нитью.
Когда счетная нить снимет блокировку с помощью pthread_mutex_unlock, то начальная нить сумеет, наконец, ее установить на почтовый ящик. Теперь за этой нитью закреплена бло кировка. Оригинальная нить выбирает сообщение из ящика, сообщение выводится на экран, добавляется полученное значение счетчика слов к общей сумме, производится сиг нализация с помощью флага, если счетная нить находится в состоянии ожидания. Затем управление вновь передается на pthread_cond_wait,#iTO приводит к атомарному разблокиро ванию mutex и блокировке нити до момента, когда вновь будет установлен флаг. Последовательность шагов, описанная в предшествующих параграфах, в точности соот ветствует тому, что было представлено в описании работы избирательных участков. Эти шаги были реализованы в коде программы twordcount4.c:
Г twordcount4.c - Программа подсчета слов в двух файлов, использующая *
механизм нитей. - Версия 4: использование условной переменной предоставляет счетным
Нити. Параллельные функци
508 *
нитям легкую возможность оповещать о готовности
результатов.
*/ «include <stdio.h> «include «include struct arg_set { char *fname; int count;
Г два значения в одном аргументе*/ Г файл для обработки */ Г счетчик слов */
struct arg set *mailbox; pthread mutex t lock = PTHREAD MUTEX INITIALIZER; pthread_cond_t flag = PTHREAD.CONDJNltlAUZER; main(int ac, char *av[])
{ pthreadj t1, t2; struct arg_set argsl, args2; void *count_words(void *); int reportsjn = 0; int total_words = 0; if (ac != 3){ printf("usage: %sfile1 file2\n”, av[0]); exit(1);
/* две нити */ /* два набора аргументов */
} pthread_mutex_lock(&lock); f теперь блокировка ящика для сообщений */ argsl .fname = av[1]; argsl .count = 0; pthread_create(&t1, NULL, count_words, (void *) &args1); args2.fname = av[2]; args2.count = 0; pthread_create(&t2, NULL, count_words, (void *) &args2); while(reports_in < 2){ printf("MAIN: waiting for flag to go up\n"); pthread_cond_wait(&flag, &lock); /* wait for notify */ printf("MAIN: Wow! flag was raised, I have the lock\n”); printf("%7d: %s\n", mailbox- >count, mailbox->fname); total_words += mailbox->count; if (mailbox == &args1) pthread join(t1 .NULL); if (mailbox == &args2) pthreadjoin(t2,NULL); mailbox = NULL; pthread_cond_signal(&flag); reports_in++; printf(”%7d: total words\n", total_words);
14.5. Уведомление для нитей
509
} void *count_words(void *а)
{ struct arg_set *args = a; /* скопировать аргументы */ FILE *fp; int c, prevc = '\0'; if ((fp = fopen(args->fname, ”r")) != NULL){ while((c = getc(fp)) != EOF){ if (lisalnum(c) && isalnum(prevc)) args->count++; prevc = c;
} fclose(fp); } else perror(args->fname); printf("COUNT: waiting to get lock\n''); pthread_mutex_lock(&lock); /* получить почтовый ящик */ printff'COUNT: have lock, storing data\n"); if (mailbox != NULL) pthread_cond_wait (&flag, Slock); mailbox = args; /* здесь поместить указатель на наши аргументы */ -printf("COUNT: raising flag\n"); pth read_cond_signal( &flag); /* установить флаг */ printff'COUNT: unlocking box\n"); pthread_mutex_unlock(&lock); /* освободить почтовый ящик */ return NULL; !\
} .1 Л ПО результатам работы можно отследить последовательность возникновения различных событий: / $ сс twordcount4.c-Ipthread-о twc4 j $ ./twc4 /etc/group /usr/dict/words COUNT: waiting to get lock MAIN: waiting fof flag to go up COUNT: have lock, storing data COUNT: raising flag COUNT: unlocking box MAIN: Wow! flag was raised, I have the lock 195: /etc/group MAIN: waiting for flag to go up COUNT: waiting to get lock COUNT: have lock, storing data COUNT: raising flag COUNT: unlocking box MAIN: Wow! flag was raised, I have the lock 45419: /usr/dict/words 45614: total words
/
Нити. Параллельные функции
510
По выведенной странице результатов нет возможности ощутить возбуждения при ночном подсчете голосов, но первый результат пришел быстро, а второму сообщению предшест вовала ощутимая задержка.
14.5.3. Функции для работы с условными переменными Флаг на почтовом ящике, который одна нить использовала для уведомления другой нити о по лученном результате, является условной переменной. Нити могут использовать следующие обращения к функциям для установления связей с помощью условных переменных: pthread_cond_wait НАЗНАЧЕНИЕ
Блокировка нити на условной переменной
INCLUDE
#include
ИСПОЛЬЗОВАНИЕ
int pthread_cond_wait(pthread_condJ *cond, pthread_mutex_t"*mutex);
АРГУМЕНТЫ
cond - указатель на условную переменную mutex - указатель на mutex
КОДЫ ВОЗВРАТА
0 - при успешном окончании Код ошибки - при обнаружении ошибки
При обращении к pthread_cond_wait вызывающая нить будет блокирована до тех пор, пока другая нить не оповестит через условную переменную condo неком событии. pthread_cond_wait всегда используется с mutex. При своей работе pthread_cond_wait атомарно освобождает mutex, а затем переходит к ожиданию на условной переменной. Результат будет не определен, если вы не произвели блокировку mutex до вызова данной функции. Прежде чем передать управление в вызывающую нить, функция pthread_COnd_wait атомарно выполняет relock в отношении указанного mutex. pthread_cond_signal N НАЗНАЧЕНИЕ
Разблокировка нити, ожидающей на условной переменной
INCLUDE
tinclude
ИСПОЛЬЗОВАНИЕ
int pthread_cond_signal(pthread_cond_t *cond);
АРГУМЕНТЫ
cond - указатель на условную переменную
КОДЫ ВОЗВРАТА
0 - при успешном окончании Код ошибки - при обнаружении ошибки
С помощью функции pthread_cond_signal производится отметка о событии, которое ассо циировано с условной переменной cond. Тем самым производится разблокирование одной из ожидающих нитей на этой условной переменной. Если на условной переменной ждут несколько нитей, то только одна из них будет разблокирована.
14.5.4. Обратимся опять к Web Мы рассмотрели базовые принципы и возможности, касающиеся нитей POSIX. Мы знаем теперь, как создавать нити, как организовать ожидание, когда нити будут закончены, как организовать корректное разделение данных нитями. Мы также знаем, как нити могут оповещать другие нити о произошедших событиях. Мы знаем вполне достаточно, чтобы можно было использовать нити для разработки Web-cepeepa и для разработки приложе ния, которое управляет усложненной анимацией.
14.6.1Ш>-сервер, который использует механизм нитей
511
14.6. Web-cepeep, который использует механизм нитей В предыдущей главе нами уже был разработан Web-cepeep. В этом сервере был использо ван вызов fork для создания новых процессов, которые управляют запросами от клиентов. Web-cepeep выполнял три операции: посылал по запросу листинги каталогов, посылал по запросу содержимое файлов и посылал по запросу вывод программ CGI. В сервере пона бился новый процесс, чтобы запустить программу CG1. Но в сервере не было необходимо сти создавать новый процесс для получения листинга каталога или для чтения файла.
14.6.1. Изменения в нашем Web-cepeepe Мы модифицируем наш исходный Web-cepeep в нескольких направлениях. Наиболее важной модификацией будет замена вызова fork на pthread_create. Теперь процессы не будут управлять клиентскими запросами. Запросы будут управляться с помощью отдельных нитей. Сделаем еще два таких изменения. Прежде всего мы удалим возможность обработки про грамм CGI. Эту возможность можно будет добавить позже. Далее, мы напишем еще одну собственную версию функции для получения листинга каталогов. В начальной версии был использован вызов ехес для запуска на исполнение стандартной команды Is.
14.6.2. При использовании нитей появляются новые возможности Использование нитей вместо процессов предоставляет нам добавить новые свойства серверу: возможность вести внутреннюю статистику. Персонал, который запускает серверы, всегда заинтересован в получении информации - как долго работал сервер, какое количество требований было принято сервером, общий объем данных, который был выдан сервером в окружающий мир. Все требования к серверу разделяют одно и то же пространство памяти. Поэтому мы будем использовать разделяемые переменные, чтобы хранить там такую статистику. Как пользователь может получить доступ к этим статистическим данным? Мы добавим специ альный URL для сервера: status. Когда удаленный пользователь затребует этот URL, то сервер пошлет ему в ответ внутренние статистические данные.
14.6.3. Предотвращение появления зомби для нитей: отсоединение нитей Рассмотрим теперь еще одну деталь, которая носит технический характер. Во всех про граммах в этой главе в отношении каждой нити было обращение вида: pthread join. Нити ис пользуют системные ресурсы. Если бы мы не вызывали pthread join, то эти ресурсы не были бы возвращены системе после окончания нити. Это было бы аналогично случаю, когда по сле вызова malloc не выдается free. В программе для подсчета слов начальная нить ждет окончания счетных нитей, поскольку она должна получить общий результат. В Web-cepeepe нет необходимости ждать нити, которые управляют запросами клиентов. Эти нити не возвращают какой-либо полезной информации. Мы можем создавать нити, окончания которых можно не ожидать (с помощью операции join). В таких отсоединенных нитях автоматически производится освобождение их ре сурсов по мере окончания работы нитей. В отношении таких нитей не разрешается выпол нять действие join. Для создания отсоединенных нитей необходимо передать специальный атрибут для функции pthread_create:
512
Нити. Параллельные функции
Г создание отсоединенной нити */ pthreadt t; pthread_attr_t attr_detached; pthread_attr_init(&attr_detached); pthread_attr_setdetached(&attr_detached, PTHREAD_CREATE_DETACHED); pthread_create(&t, &attr_detached, func, arg);
14.6.4. Код Полный код для мультинитьевого Web-cepeepa:
Г twebserv.c - минимальный web-cepeep с использованием механизма нитей * (версия 0.2) * Использование: tws номер_порта * Свойства: поддержка только команды GET * запуск в текущем каталоге * создание нити для управления каждым запросом * поддержка URL специального назначения для выдачи информации о внутреннем состоянии * Трансляция: сс twebserv.c socklib.c -Ipthread -о twebserv
7
tinclude <stdio.h> tinclude <sys/types.h> tinclude <sys/stat.h> tinclude <string.h> tinclude tinclude <stdlib.h> tinclude tinclude tinclude Г сервер находится здесь */ time_t server_started; int server_bytes_sent; int serverjequests; main(int ac, char *av(])
{
int . sock, fd; int *fdptr; pthread_t worker; pthread_attr_t attr; void *handle_call(void *); if (ac== 1)f fprintf(stderr,"usage: tws portnum\n"); exit(1);
}
sock = make_server_socket(atoi(av[1])); if (sock == -1) {perrorfmaking socket"); exit(2);} setup(Sattr); [* здесь находится основной цикл: получить запрос, управление запросом в * новой нити 7 while(1){ fd = accept(sock, NULL, NULL); server_requests++; fdptr = malloc( sizeof (int)); *fdptr = fd; pthread_create(&worker, Sattr, handle_call,fdptr);
.6. Web-cepeep, который использует механизм нитей
} } Г
’ инициализация статусных переменных и * установка атрибута нити для отсоединения
7
setup(pthread attr_t *attrp)
{
pthread_attr_init(attrp); pthread_attr_setdetachstate(attrp,PTHREAD_CREATE_DETACHED); time(&server_started); server_requests = 0; server bytes_sent = 0;
}
void *handle_call(void *fdptr)
{
FILE *fpin; char request[BUFSIZ]; int fd; fd = *(int *)fdptr; free(fdptr); /* получить fd из аргумента 7! fpin = fdopen(fd, "r"); /* буфер ввода */ ' fgets(request,BUFSIZ,fpin); /* чтение требования от клиента */ • printffgot a call on %d: request = %s", fd, request); skip_rest_of_header (fpi n); process_rq(request, fd); f* rq процесса клиента 7 fclose(fpin);
}
j-k______ _______ _______ _ _ _ ._________ ___...___ _____ _ _ _ ______________ _ _______________ *
skip_rest_of_header( FI LE *) пропуск всех требований на информацию пока не будет обнаружен CRNL skip rest of_header (FI LE *fp)
{
char buf [BUFSIZ]; while(fgets(buf,BUFSIZ,fp) != NULL && strcmp(buf,"\r\n") != 0)
i } Г
.................................................................. process_rq(char *rq, int fd) выполнение действия по требованию и запись ответа в fd управление требованием в новом процессе rq - это команда HTTP : GET /foo/bar.html HTTP/1.0
process_rq(char *rq, int fd)
{
char cmdfBUFSIZ], arg[BUFSIZ]; if (sscanf(rq, "%s%s", cmd, arg) != 2) return; sanitize(arg); printffsanitized version is %s\n", arg); if (strcmp(cmd,"GET") != 0) not imolementedH:
4
Нити. Параллельные фуш else if (built_in(arg, fd))
I
else if (not_exist(arg)) do_404(arg, fd); else if (isadir(arg)) do_ls(arg, fd); else do_cat(arg, fd);
} Г
*
убедиться, что все маршруты - ниже текущего каталога
7
sanitize(char *str)
{
char src = dest = str; while(*src){
*src, *dest;
if(strncmp(src,'y../',4) == 0)
src += 3; else if (strncmp(src,"//',2) == 0) src++; else *dest++ = *src++;
}
*dest = '\0'; if (*str — ’/’) strcpy(str,str+1); if (str[0]=='\0' || strcmp(str,"./")==01| strcmp(str,"./..")==0) strcpy(str,".");
}
Г Здесь управление встроенными URL 7 built_in(char *arg, int fd)
{
FILE *fp; if (strcmp(arg,"status") != 0) return 0; http_reply(fd, &fp, 200, "OK", "text/plain",NULL); fprintf(fp,"Server started: %s", ctime(&server_started)); fprintfjfp,"Total requests: %d\n", server_requests); fprintf(fp,”Bytes sent out: %d\n", server_bytes_sent); fclose(fp); return 1;
}
http_reply(int fd, FILE **fpp, int code, char *msg, char *type, char ‘content)
{
FILE *fp = fdopen(fd, "w"); int bytes = 0; if (fp != NULL){ bytes = fprintf(fp,"HTTP/1.0 %d %s\r\n", code, msg); bytes += fprintf(fp,"Content-type: %s\r\n\r\n", type); if (content) bytes += f printf (fp, "%s\r\n", content);
}
fflush(fp); if(fpp)
.6. Web-cepeep, который использует механизм нитей *fpp = fp; else fclose(fp); return bytes;
} * I*........................................................................................................................ первые простые функции: notjmplemented(fd) и do_404( item ,fd)
незадействованная HTTP-команда нет такого объекта
................ 7..........................................................................................................*/ not implemented(int fd)
{'
http_reply(fd, NULL, 501 ,"Not Implemented", "text/plain", 'That command is not implemented");
}
do_404(char *item, int fd)
{'
http_reply(fd,NULL,404,"Not Found","text/plain", 'The item you seek is not here");
) I*. - -__- - -_.........______ -____ ___ __------__ __...___ - -__ _____ секция листинга каталога isadir() использует stat, not exist() использует stat
...............................:................................................ 7 isadir(char *f)
{
struct stat info; return (stat(f, &info) != -1 &&S ISDIR(info.st mode));
}
not exist(char *f)
{'
struct stat info; retu r n{ stat(f, &i nfo) == -1);
)
do_ls(char 'dir, int fd)
{
DIR *dirptr; struct dirent *direntp; FILE *fp; int bytes = 0; bytes = http_reply(fd,&fp,200,"OK","text/plain",NULL); bytes += fprintf(fp,"Listing of Directory %s\n", dir); if ((dirptr = opendir(dir)) != NULL){ while(direntp = readdir(dirptr)){ bytes += fprintf(fp, "%s\n", direntp->d_name);
)
closedir(dirptr);
}
fclose(fp); server_bytes_sent += bytes;
} /*............................................................................................................................ функции для выполнения cat для файлов. file_type(filename) использует 'расширение': что используется в cat :..........................................................................7
Нити. Параллельные функции
516 char * file type(char *f)
{
char *cp; if ((cp = strrchr(f,!= NULL) return cp+1; return
)
Г do_cat(filename,fd): послать заголовок, а затем и содержание */ do_cat(char *f, int fd)
{"
char ‘extension = file_type(f); char *type = "text/plain"; FILE *fpsock, *fpfile; int c; int bytes = 0; if (strcmp(extension,"html") == 0) type = "text/html"; else if (strcmp(extension, "gif') == 0) type = "image/gif’; else if (strcmp(extension, "jpg") == 0) type = "image/jpeg”; else if (strcmp(extension, "jpeg") == 0) type = "image/jpeg"; fpsock = fdopen(fd, "w"); fpfile = fopenff, "r"); if (fpsock != NULL && fpfile != NULL)
{
}
bytes = http_reply (fd, &fpsock,200,'"OK" .type, NULL); while((c = getc(fpfile)) !=EOF){ putc(c, fpsock); bytes++; fclose(fpfile); fclose(fpsock);
}
server_bytes_sent += bytes;
) Заметим, что мы получили работающую программу. Но есть одна проблема: для хранения статистики были использованы разделяемые переменные. Эти разделяемые переменные в данном решении не защищены средствами блокировки. В качестве упражнения добавь те механизм mutex.
14.7. Нити и анимация При реализации Web-cepeepa не было необходимости в использовании нитей. Для управ ления параллельными запросами вполне достаточно было использовать fork. С другой стороны, в работе Web-броузера будет трудно обойтись без использования нитей, с помо щью которых можно достаточно просто анимировать изображения и различные рекламы. Еще один пример использования нитей - управление анимацией. Таймер посылает через регулярные отрезки времени сигнал SIGALRM. В обработчике сигнала используют счетчики. Они определяют моменты времени, когда следует перемещать изображение.
14.7. Нити и анимация
517
14. 7. /. Преимущества нитей Программа, которая использует механизмы обработчика сигнала и интервальный таймер, работает. Однако использование в программе вместо этих механизмов нитей привнесет улучшение во внутренней и внешней структуре. Итак, внутри программы будут использо ваны два независимых потока управления: поток управления анимацией и поток управления данными с клавиатуры (см. рисунок 14.10).
Нити при непосредственной реализации представляются нам анимационным кодом, ко торый будет отличен от кода для работы с данными, поступающими от клавиатуры. Нити разделяют переменные, которые определяют позицию и скорость анимации, как показано на рисунке 14.ll.
Естественно, анимацией управляет интервальный таймер, который не показан. В данном случае решение на основе использования нитей позволяет нам сконцентрировать внима ние на программных компонентах. При использовании нитей становится очевидным второе преимущество относительно подхода, где были использованы обработчики сигналов и таймеры. Современные библио теки нитей таковы, что допускают запуск различных нитей на различных процессорах, обеспечивая при этом правильное решение при одновременном исполнении нитей. При рассмотрении задачи анимации таким результатом будет формирование законченных тра екторий перемещения, скоростей, распределение текстур. При этом происходит выполне ние каждой нити на своем собственном процессоре, что приводит к более быстрой работе.
Нити. Параллельные функции
518
14.7.2. Программа bounceld. с, построенная с использованием нитей Сравните начальную версию программы bounceld.c с новой, двухнитьевой версией bounceld.c:
Г tbouncel d.c: управление анимацией с использованием двух нитей . Замечание: одна нить управляет анимацией, другая управляет вводом от клавиатуры Компиляция: сс tbounceld.c -Icurses -Ipthread -о tbounceld
*
7 #include <stdio.h> #include <curses.h> #include «include <stdlib.h> tinclude Г Разделяемые переменные, которые используют две нити. Здесь необходим * mutex.
7 tdefine MESSAGE" hello" int row; Г текущая строка */ int col; /* текущая колонка 7 int dir; f* направление перемещения */ int delay; [* задержка между перемещениями */ main()
{ int ndelay; /* новая задержка */ int с; /* пользовательские входные данные7 pthread_t msg.thread; /* нить */ void *moving_msg(); initscr(); /* инициализация curses и терминала 7 crmode(); noecho(); clear(); row =10; /* здесь начало */ col = 0; dir = 1; /* добавить 1 к счетчику строк */ delay = 200; /* 200ms = 0.2 секунд */ if (pthread_create(&msg_thread1NULL1moving_msg,MESSAGE)){ fprintf(stderr,"error creating thread"); endwin(); exit(0);
} while(1) { ndelay = 0; с = getch(); if (c == 'Q') break; if (c== ' ’) dir = -dir; if (с == T && delay > 2) ndelay = delay/2; if (c == ‘s') ndelay = delay * 2;
14.7. Нити и анимация
519
if (ndelay > 0) delay = ndelay;
} pthread_cancel(msg_thread); endwin();
} void *moving_msg(char *msg)
{ while(1) { usleep(delay*1 ООО); /‘спать*/ move(row, col); /* установить курсор в требуемую позицию */ addstr(msg); /* перестроить (redo) сообщение */ refresh(); /* и показать его */ /* переместиться на следующую позицию и проверить необходимость * выполнения */ col += dir; /* переместиться в новую колонку */ if (col <= 0 && dir ==-1) dir = 1; else if (col+strlen(msg) >= COLS && dir == 1) dir = -1;
} } В чем отличие этой новой версии от начальной, построенной на основе модели управле ния сигналами? Основное отличие в том, что функция main создает новую нить для выпол нения в ней функции moving_msg. Функция moving_msg выполняет простой цикл: sleep, move, проверка на необходимость смены направления, повтор. Тем временем в другой части это го же процесса в main выполняется такой простой цикл: getch, обработка, повторение. В полученном варианте программы используются глобальные переменные, с помощью которых фиксируется состояние шарика. Нам необходимы глобальные переменные в версии, где использована модель управления сигналами, поскольку мы не могли пере дать аргументы обработчику сигнала. Но нити могут принимать аргументы. Мы можем улучшить эту программу, создав структуру для помещения в нее установок, как мы уже делали в версиях 3 и 4 при реализации программы для счета слов на основе использования модели нитей.
14.7.3. Множественная анимация: tanimate.c Как можно одновременно анимировать несколько изображений? В мультинитьевой про грамме для подсчета слов были запущены параллельные нити для выполнения подсчета слов. Каждая работа со своим файлом и со своим счетчиком слов. Давайте используем тот же принцип для запуска нескольких одновременно развивающихся анимаций. Такая анимационная программа tanimate.c, где использованы нити, является развитием про граммы tbOuncel.c. При обращении к tanimate.c задается до десяти текстовых строк в качестве аргументов. В программе организуется анимация каждого аргумента, который будет отобра жаться на отдельной строке. Каждое текстовое сообщение будет передвигаться на своей строке с собственной скоростью и в собственном направлении. Это немного напоминает картинку, которую мы видим на Web-странице. Например, при выполнении команды: tanimate ’Buy this' ’Drive this car' ’Spend Money here' Consume ’Buy!'
520
Нити. Параллельные функции
на экране будет несколько анимированных строк, которые перемещаются со сменой на правления, как показано на рисунке 14.12.
Пользователь может изменить направление любого из сообщений, нажимая для этого на клавиши“0”,“1”,— С помощью программы можно анимировать любой список из текстовых строк, даже если эти текстовые строки являются командами Unix. Попробуйте выполнить: tanimate ‘ls‘ tanimate ‘users' tanimate ‘date*
В программе tanimate запускается анимационная функция в нескольких нитях. При каждом запуске в функцию передается разный набор аргументов. Через аргументы задается тек стовое сообщение, строка, направление перемещения и скорость:
Г tanimate.с: анимация нескольких строк на основе использования * HMTeii.curses, usleep() * * * * * *
*/
Основная идея: одна нить на одну анимируемую строку одна нить для работы с терминалом разделяемые переменные для взаимодействия Компиляция: compile сс tanimate.c -Icurses -Ipthread -о tanimate Необходимо: блокировать разделяемые переменные управление экраном возложить на отдельную нить
tinclude <stdio.h> tinclude Courses. h> tinclude tinclude <stdlib.h> tinclude tdefine MAXMSG 10 tdefine TUNIT 20000 struct propset { char *str; int row; int delay; int dir;
/* предельное количество строк */ /* время в микросекундах */ /* сообщение */ Г строка */ /* задержка в микросекундах */ /*+1 или-1*/
}; pthread mutex t mx = PTHREAD MUTEX INITIALIZER;
7. Нити и анимация int main(int ас, char *av[])
{ int с; pthreadj thrds[MAXMSG]; struct propset props[MAXMSG]; void *animate(); int num_msg; int i; if (ac== 1){ printf( "usage: tanimate string. ,\n"); exit(1);
Г входные данные от пользователя */ Г нити */ Г свойства строки */ /* функция */ /* число строк */
} num_msg = setup(ac-1 ,av+1 .props); Г создать все нити */ for(i=0; i
} /* обработка данных от пользователя */ while(1) { с = getchf); if (с == 'Q') break; if (c=='') for(i=0;i= '0' && с <= '9’){ i = с - 'O'; if (i < num_msg) props[i].dir = -props[i].dir;
} }
Г закончить исполнение всех нитей */ pthread_mutexJock(&mx); for (i=0; i
} int setup(int nstrings, char *strings[], struct propset propsO)
{ int num_msg = (nstrings > MAXMSG? MAXMSG : nstrings); int i; Г установить значение номеров строк и скоростей для каждой текстовой строю * задаваемого сообщения */
2
Нити. Параллельные ф .srand(getpid()); for(i=0; i
/* сообщение */ Г строка */ /* скорость */ Г +1 или -1 */
}
Г установить curses 7 initscr(); crmode(); noecho(); clear(); mvprintw(UNES-1l0,",Q' to quit, ,0'..,%d' to bounce",num_msg-1); return numjnsg;
} /* код, который запускается в каждой нити */ void *animate(void *arg)
{ struct propset *info = arg; int len = strlen(info->str)+2; int col = rand()%(COLS-len-3); while(1)
Г указатель на блок info */ /* +2 для заполнения */ /* пространство для заполнения */
{ usleep( info- >delay*TU N IT); pthread_mutex_lock(&mx); Г только одна нить */ move(info->row, col); /* может вызвать curses */ addch(''); Г при одновременном обращении */ /* Поскольку я сомневаюсь в */ addstr(info- >str); addch(''); /* реентерабельности, */ I* в парковке курсора */ move(LINES-1 .COLS-1); refresh!); /* и его показе, */ Г то действие с curses будет таким */ pthread_mutex_unlock(&mx); /* перемещение элемента в следующую колонку и проверка на необходимость смены * направления
7 col += info- >dir; if (col <= 0 && info- >dir == -1) info- >dir = 1; else if (col+len >= COLS && info- >dir == 1) info- >dir = -1;
14.7. Нити и анимация
523
14.7.4. Mutexes и tanimate.c Откомпилируем программу tanimate.c и запустим ее на исполнение. В коде представлены три основные секции: секция инициализации, секция с функцией для анимации сообще ния, секция, содержащая цикл для ввода данных от пользователя и для обработки этих данных. В цикле работы с пользователем запускается начальная нить. Функция animate запускается в нескольких нитях. В программе tanimate возможно запускать сразу до двена дцати нитей. При одновременной работе двенадцати нитей требуется синхронизация. Что в данной программе является разделяемыми ресурсами и как можно предотвратить их не правильное использование? Искажение данных: динамическая инициализация mutex. Анимационная функция ис пользует и модифицирует значения, которые в информационной структуре представляют позицию, скорость и направление перемещения. Когда пользователь хочет изменить направление перемещения для некоторого сообщения, то нить, которая управляет работой с терминалом, изменяет член dire этой структуре. Поскольку структура является разделяе мой, то для проведения в ней изменений значений необходимо использовать mutex, чтобы предотвратить искажение данных. Как теперь поступить - ввести один mutex для всех переменных направления или создать для каждой переменной dir свой. Лучшим вариантом все же будет создание для каждой структуры свой mutex. Нить анимации и нить терминала будут использовать этот mutex, когда они будут читать и модифицировать дан ные в структуре. Модифицированная структура тогда будет такой: struct propset { char *str; /* сообщение 7 int row; /* строка */ int delay; /* задержка в микросекундах 7 int dir; Г +1 или -1 7 pthread_mutex_t lock; /* mutex для dir 7
}; Тогда инициализация в setup будет выглядеть так: for(i=0; Knumjrisg; i++){ props[i].str = strings[i]; props[i].row = i; props[i]. delay = 1+(rand()%15); props[i].dir = ((rand()%2)?1 :-1); pthread mutex init(&props[i].lock,NULL);
/* сообщение 7 Г строка 7 /* скорость 7 Г +1 или -1 7
} Другие измененийя в коде следует выполнить в качестве упражнения. Искажения при выводе на экран: критические секции. Разделяемыми ресурсами в про грамме являются не только переменные направления. Всеми нитями анимации будут раз деляться также экранные функции и функции curses, которые модифицируют экран. Поэтому будем использовать mutex mx, чтобы предотвратить одновременный доступ к curses-функциям. Чтобы убедиться в необходимости такой блокировки, рассмотрим такие вызовы средств управления экраном в animate: move, addch, addstr.. refresh. Что произойдет, если две нити по пытаются одновременно выполнить эту последовательность? Или, например, что будет, если две нити будут обращаться к curses в таком порядке: move, addch, addch, addstr, addstr...
524
Нити. Параллельные функции
Первая нить будет смещать курсор в одну позицию экрана, а другая будет смещать этот же курсор в другую позицию. Первая нить будет выводить свой текст на экран, предполагая при этом, что курсор расположен в позиции, куда она его передвинула. Но выводимый текст будет выведен, начиная с позиции, которую определила вторая нить. Библиотека curses ничего не знает о нитях. При использовании таких функций они не должны прерываться другими нитями. Такие функции не являются реентерабельными. Чтобы гарантировать использование в каждый момент времени только одной функции curses, воспользуемся механизмом mutex. В библиотеке curses содержатся внутренние структуры данных. Мы будем использовать mutex для предотвращения искажений структур данных, которые управляются системны ми библиотеками. Это делается точно так, когда мы использовали mutex для предотвраще ния искажений данных при работе с собственными структурами данных.
14.7.5. Нить для curses Не только механизм mutex позволяет предотвратить для curses возможность искажения данных. Есть еще один метод - нужно создать новую нить для управления всеми вызова ми к функциям управления экраном (см. рисунок 14.13). Вы можете воспринимать эту нить по управлению выводом на экране аналогично де партаменту общественных отношений для большого бизнеса. Любой департамент, который хочет опубликовать информацию , посылает запрос в департамент общественных отношений. Сотрудники департамента позаботятся о получении сообщения. Нить по управлению экраном работает аналогично департаменту общественных отноше ний. Любая нить, которая хочет вывести на экран сообщение, должна послать запрос для SMT (screen-management thread - нить управления экраном).
В нашем случае нити будут посылать запросы для вывода своих текстов на экран. Поэто му каждый запрос может быть представлен как структура, в которой содержатся значения строки, колонки и сама строка. Анимационные нити являются поставщиками таких сооб щений, a SMT будет принимать и обрабатывать сообщения.
Заключение
525
Такая система с несколькими производителями запросов и одним потребителем анало гична системе, которая была запрограммирована в программе счета слов, где были ис пользованы нити. Нам потребуется переменная, в которой будут сохраняться поступив шие сообщения. Потребуется mutex, чтобы предотвратить искажения текстов сообщений. Также потребуется условная переменная для того, чтобы уведомить SMT о том, что ани мационная нить послала сообщение. Централизация и абстрагирование при управлении экраном придают программе большую гибкость. Если заменить curses на какую-то другую систему для управления дисплеем, то потребуется изменить только функции, которые будут использованы в SMT. SMT может даже взаимодействовать с удаленным графическим сервером через программный канал или сокет, без анимационных нитей. Данный проект остается для выполнения в качестве упражнения.
Заключение Основные идеи •
Нить исполнения - это поток управления при исполнении программы. С помощью библиотеки pthreads программа может запускать одновременно на исполнение несколько функций. • Функции, которые исполняются одновременно, имеют свои локальные переменные, но могут также разделять все глобальные переменные и динамически распределяемые данные. • Когда нити разделяют переменную, то нужны гарантии, что нити не получат доступ к этой переменной каким-то другим способом. Чтобы гарантировать, что в любой момент времени с разделяемой переменной будет работать только одна нить, нити должны использовать mutex для блокировки одновременного доступа. • Когда у нитей появляется необходимость в координации или синхронизации своих действий, то они могут использовать для этого условную переменную. Одна нить может ожидать на условной переменной наступления некоторого события, а другая нить будет сигнализировать через эту переменную о наступлении такого события. • Нитям требуется использовать блокировку mutex для предотвращения одновременно го доступа к функциям, которые работают с разделяемыми ресурсами. Таким же образом должны быть защищены и функции, которые не являются реентерабельными.
Чтодальше? Программа может использовать несколько процессов, которые взаимодействуют через программные каналы, файлы, сокеты, используют сигналы. В программе могут быть так же использованы и несколько нитей, которые могут взаимодействовать и координировать свои действия через разделяемые переменные, файлы, блокировки (замки) и сигналы. В последней главе наше внимание будет сфокусировано на межпроцессных взаимодейст виях. Сколько методов взаимодействий есть в Unix? Какой из них может быть выбран как лучший для конкретного приложения?
526
Нити. Параллельные функции
Исследования 14.1 Поэкспериментируйте с основными операциями над нитями посредством внесения
некоторых изменений в программу hellojnulti.c. Прежде всего добавьте еще одно или два сообщения, чтобы убедиться, насколько легко добавлять в программу новые нити для исполнения. Далее, измените print_msg так, чтобы число повторений было бы равно длине текста строки. Выводите текст сообщения после каждого обращения к pthreadjoin, чтобы отслеживать происходящее в программе. Предугадайте результат. 14.2 Использование программного канала в tanimate. Будет забавным результат, если позво лить программе tanimate читать список текстовых строк для нее со стандартного ввода. Читать по одному сообщению в строке. Тогда должна будет работать такая команда: who | tanimate
Добавление такой возможности в программу не является тривиальным делом. Для до бавления такой возможности вам понадобится читать строки со стандартного ввода и затем перенаправлять стандартный ввод на терминал с тем, чтобы curses смог бы чи тать символы в неканоническом режиме. Подсказка: откройте /dev/tty и выполните dup для стандартного ввода. 14.3 В главе, посвященной видеоиграм, мы блокировались с помощью сигнальных масок, с помощью которых предотвращались прерывания во время выполнения критических секций кода. Сравните метод использования сигнальных масок и обработчиков сигна лов с методом нитей и методом блокировок mutex, а также с методом условных пере менных.
Программные упражнения 14.4 Humu создают нити. В программе hellO_multi.c начальная нить создает две нити для печати. Напишите новую версию, в которой нить, которая выводит сообщение “hello”,
образует новую нить для печати “world\n”. Какая из нитей будет ждать окончания нити для вывода “world\n”? Почему? 14.5 В программе twordcountl .с используются три нити: начальная нить и две нити для под счета слов. Но начальной нити нечего делать. Напишите версию программы, в которой начальная нить сначала подсчитывает число слов в первом файле и создает вторую нить, которая должна подсчитывать число слов во втором файле. Результат получим быстрее, чем раньше? Это решение лучше? 14.6 Обработка ошибок. Функция C0unt_W0rds сообщает о возникновении ошибки, если ей не удается открыть заданный для нее файл. Другая нить тем не менее продолжает рабо тать. Хорошо ли это? Модифицируйте программу так, чтобы count_words вызывала exit, если не удается открыть файл. 14.7Масштабирование числа обрабатываемых файлов. Как можно расширить использо вание методов, которые были использованы в программе twordcount2.c и twordcountS.c так, чтобы программы могли бы обрабатывать столько файлов, сколько будет задано при обращении к этим программам в командной строке? Модифицируйте обе эти програм мы так, чтобы они могли бы принимать на обработку произвольное число файлов, име на которых задаются как аргументы в командной строке. Какую версию будет проще модернизировать с тем, чтобы она управляла произвольным количеством файлов? Какое решение будет более эффективным?
Заключение
527
14.8 Процессы или нити.
(a) Напишите версию программы для подсчета слов. Она использует fork, чтобы соз дать новые процессы для работы над отдельными файлами. Вам необходимо разрабо тать систему для дочерних процессов, которые могли бы посылать их результаты обратно процессу-отцу для проведения над ними дальнейшей обработки. Не исполь зуйте при этом значение аргумента в вызове exit, поскольку это число не может быть более 255. Используйте одно из преимуществ от использования fork, которое заключа ется в возможности запускать обычную команду wc -w для подсчета слов в файле. (b) Напишите версию программы для подсчета слов, которая состоит из единственной нити. (c) Сравните эти три версии (процессы, мультинитьевую версию, однонитьевую версию) по легкости проектирования, простоте кодирования, скорости выполнения и мобильности возможностей. Встретились ли неожиданности при сравнении? 14.9 Управление несколькими файлами. Расширьте возможности программы twordcount4.c так, чтобы она могла бы управлять более чем двумя файлами, имена которых задаются как аргументы командной строки. 14.10 Программа twordcount4.c использует глобальные переменные. Удалите глобальные переменные и сделайте их локальными для функции main. Передайте указатели на эти переменные как члены структуры, которая используется для передачи аргументов при обращении к нитям. 14.11 Добавьте блокировку с помощью mutex в программу twebserv.c для защиты программы, где содержатся статистические данные. 14.12 Удалите все глобальные данные из программы tbounceld.c, используя вместо них структуру, где содержатся все свойства перемещаемых сообщений. 14.13 Для работы с переменными разделяемого состояния в программе tbounceld.c необходи мо использовать mutex. Какие условия гонок могут возникнуть в этой программе? Какие могут возникнуть эффекты в программе, если две нити начнут мешать друг другу?. 14.14 В варианте программы с однострочной анимацией был использован контроль за скоростью. Пользователь мог нажать клавиши “s” и “f”, чтобы увеличить или умень шить задержки между перемещениями. Добавьте механизм управления скоростью для версии с многими сообщениями. 14.15 Все сообщения в программе tanimate.c перемещаются по горизонтали. Измените про грамму так, чтобы некоторые строки могли бы перемещаться вверх и вниз, а другие строки перемещались бы влево и вправо. Как вы будете управлять коллизиями? 14.16 Модифицируйте программу tanimate так, чтобы для управления экраном использова лась бы отдельная нить. Нити, которые представляют сообщения, должны посылать сообщения нити, управляющей экраном. Разработайте систему для взаимодействий между нитями. 14.17 Мулътинитъевый finger-cepeep. Напишите fingep-cepeep, где использовалась бы многонитьевая модель. Сервер принимает на входе одну строку. Затем он блокирует эту строку в базе данных пользователей и посылает назад информацию о том, что запись о сопоставлениях в данной строке была выполнена. Сервер должен выполнять такие действия: (a) Загружает базу данных пользователя в память. (b) Запускает новую, отсоединенную нить для каждого запроса.
528
Нити. Параллельные функции
(c) Сервер записывает число попаданий (вхождений) для каждой записи. (d) С помощью специального запроса STATUS возвращается статистика. (e) Сервер обновляет свою внутреннюю базу данных, если он принял SIGHUP. 14.18 Curses-cepeep. В последней секции о программе tanimate дискутировался вопрос о разделении функций по отображению, функций для работы со временем и функций по обработке данных. Напишите клиент/серверную версию программы tanimate, где необходимо использовать дейтаграммные сокеты для посылки простых запросов из программы tanimate к curses-cepeepy общего назначения. Curses-cepeep поддерживает простой протокол, состоящий из двух команд: CLEAR очистить дисплей DRAW R С Any string передать текст "Any string” в строку R, начиная с колонки С
В модифицированной версии tanimate не должно быть обращений к функциям curses. Вместо этого передаются сообщения для curses-cepeepa. Curses-cepeep принимает такие сообщения и выводит строки. Когда вы получите работающую версию, то перей дите к изучению идей и проекта оконной системы XII для Unix.
Глава 15 Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
Цели Идеи и средства • • • • • •
Блокирование на вводе от нескольких источников: select и poll. Именованные программные каналы. Разделяемая память. Файлы блокираторы. Семафоры. Обзор средств IPC.
Системные вызовы и функции •
select, poll
•
mkfifo
•
shmget, shmat, shmctl, shmdt
•
semget, semctl, semop
Команды •
talk lpr
530
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
15.1. Выбор при программировании В давние времена, когда два человека хотели каким-то образом пообщаться, их выбор был невелик: либо поговорить, либо запустить камнем. (Последнее можно воспринимать либо как юмор автора, либо как описание известного для школьников способа вызова подружек на прогулку. -Примеч. пер.). У современных людей выбор для общения значительно боль ший: телефон, электронная почта, обычная почта, экспресс новости, курьерская связь, го лосовая почта, пейджерная связь, выдача сообщений на монитор, приватный разговор или бросание камнями. У каждого метода есть свои сильные и слабые стороны. Какой из вари антов выбрать? Почему так много таких вариантов общения? Программисты в Unix также могут выбрать подходящий для них вариант для установле ния коммуникаций между процессами. Но опять же каждый метод имеет свои преимуще ства и свои недостатки. На основании чего можно сделать выбор? Мы начнем изучения механизма talk. Это команда в Unix, с помощью которой пользователи могут интерактивно общаться - посылать текстовых сообщений друг другу. Мы проведем сравнение и обсу дим других возможных методов, которые существуют в Unix для организации передачи информации между процессами.
15.2. Команда talk: Чтение многих входов Команда talk в Unix представляет собой один из вариантов для организации межпроцес сных взаимодействий. С помощью команды talk пользователи получают возможность передавать набираемые на клавиатуре сообщения между двумя терминалами. Команда talk работает даже в варианте, когда терминалы различных компьютеров соединены между собой через Интернет (см. рисунок 15.1).
Рисунок 15.1 Команда talk при работе в сети При выполнении команды talk экран разделяется на две области: верхнюю и нижнюю. Рабо та терминала происходит в посимвольном режиме (character-by-character). При наборе поль зователями текста этот текст одновременно отображается на обоих экранах. Символы, которые набирает пользователь, отображаются в верхней области его экрана. А на экране его собеседника эти символы будут отображаться в нижней области экрана. При своей работе команда talk использует сокеты, как это показано на рисунке 15.2. Программа talk считывает символы и записывает их по месту назначения. Но в отличие от других программ, которые мы изучали, команда talk ждет поступления ввода сразу от двух файловых дескрипторов.
531
15.2. Команда talk: Чтение многих входов
15.2.1. Чтение из двух файловых дескрипторов Команда talk принимает данные от клавиатуры и сокета. Символы, которые поступают от клавиатуры, копируются в верхнюю область экрана и передаются через сокет другому пользователю. Символы, которые читаются из сокета, добавляются в нижнюю область экрана. Пользователи при работе с командой talk могут производить набор текстов с произвольной скоростью и в произвольном порядке во времени. Команда talk должна быть готова читать данные из какого-то источника в любой момент времени. Программы ориентированы на очевидные, простые протоколы. Сервер ждет поступления требования на выполнение read или recvfrom, а затем посылает от вет с write или sendto. Но пользователи не всегда выдерживают очередность в разговоре. Что делает команда talk? Совершенно определенно, что команда talk не будет работать так: while(1){ read(fd_kbd, &с, 1); waddch(topwin, с); write(fd_sock, &с, 1); read(fd_sock, &с, 1); waddch(botwin, с);
Г читать g клавиатуры */ /* добавить на экран */ /* послать другому пользователю */ /* прочитать сообщение от другого пользователя */ /* добавить на экран */
} Что произойдет, если второй пользователь на другой стороне будет чем-либо занят, а первый пользователь ничего не набирает на клавиатуре и ждет сообщения? Тогда про грамма будет заблокирована на первом системном вызове read, и данные другим пользова телем получены не будут. Метод, код которого представлен выше, будет работать только в случае, если пользователи будут соблюдать строгую очередность во времени при наборе сообщений. А почему бы не установить для файлового дескриптора режим не блокируемый? Для этого мы можем вызывать fcntl и установить флаг 0J40NBL0CK. При установленном не бло кируемом режиме при работе вызова read сразу происходит выход из вызова. При этом результат значения вызова будет равен 0, если при попытке чтения на входе нет данных. Не блокируемый режим будет работать, но при таком подходе слишком непроизводитель но тратится время центрального процессора. При каждом чтении выполняется системный вызов. Программа может сотни и тысячи раз выполнять код, который проверяет наличие данных на входе и который находится в ядре. Проверки будут продолжаться до того мо мента, когда появится на входе хотя бы один символ.
532
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
15.2.2. Системный вызов select В Unix есть системный вызов select, который специально разработан для программ, которые хотели бы быть заблокированы на вводе от нескольких файловых дескрипторов. Принцип работы достаточно простой: (a) Создается список файловых дескрипторов, за которыми необходимо наблюдать. (b) Этот список передается вызову select. (c) Системный вызов select блокирует процесс, пока не поступят данные от любого фай лового дескриптора из списка. (d) Системный вызов select устанавливает определенные разряды в значении перемен ной для оповещения, по какому из дескрипторов поступили входные данные. Программа selectdemo.c ждет ввода от двух устройств: /* selectdemo.c: контролирует ввод от двух устройств И использует timeout * Использование: selectdemo dev1 dev2 timeout * Действие: оповещение о вводе из каждого файла и * сообщение об окончании таймаута
*/ «include <stdio.h> tinclude <sys/time.h> tinclude <sys/types.h> tinclude tinclude tdefine oops(m,x) {perror(m); exit(x);} main(int ac, char *av[))
{ int fd1, fd2; Г контролируемые файловые дескрипторы */ struct timeval timeout; /* интервал ожидания */ fd_set readfds; /* те дескрипторы, за которыми будет наблюдение при вводе*/ int retval; Г возврат из select */ if (ас != 4){ fprintf(stderr,"usage: %s file file timeout”, *av); exit(1);
} /** открытие файлов **/ if ((fd1 = 0pen(av[1],0_RD0NLY)) == -1) oops(av[1], 2); if ((fd2 = open(av[2] ,0_RD0NLY)) == -1) oops(av[~2], 3); maxfd = 1 + (fd1>fd2?fd1 :fd2); while(1) { /** образовать список дескрипторов, за которым будет наблюдение **/ FD_ZERO(&readfds); /* очистка всех разрядов */ FD_SET(fd1, &readfds); /* установить бит для fd1 */ FD_SET(fd2, &readfds); /* установить бит для fd2 */ /** установить величину таймаута **/ timeout.tv_sec = atoi(av[3]); Л установить значение в секундах */
15.2. Команда talk: Чтение многих входов
533
timeout.tvusec = 0; Г нет useconds */ [** ожидание ввода **/ retval = select{ maxfd, &readfds, N ULL, N ULL, &timeout); if( retval == -1) oops("select",4); if (retval > 0){ /** проверка разрядов для каждого наблюдаемого файлового дескриптора **/ if (FD_ISSET(fd1, &readfds)) showdata(av[1],fd1); if (FDJSSET(fd2, &readfds)) showdata(av[2], fd2);
} else printf("no input after %d seconds\n", atoi(av[3]));
} } showdata(char *fname, int fd)
{ char buf [BUFSIZ]; int n; printf("%s:", fname, n); fflush( stdout); n = read(fd, buf, BUFSIZ); if(n==-1) oops(fname,5); write(1, buf, n); write(1, "\n", 1);
} Приведенный программный код представляет собой реализацию четырех шагов, рас смотренных выше. Список файловых дескрипторов хранится в форме битового набора в переменной типа fd_set С помощью макросов FD.ZERO, FD_SET и FDJSSET производится очи стка всех разрядов, установка конкретного разряда и проверка значения требуемого разря да в битовом наборе fd_set. Нам хотелось бы контролировать сразу два источника данных. Поэтому мы вызываем макрос FD_SET для обоих дескрипторов. При обращении к select также задается значение таймаута. Если данные не поступили в течение установленного времени, то происходит выход из select. В программе selectdemo.c длительность интервала ожидания задается в секундах через аргумент при обращении к программе. Мною был проверен такой код: $ сс selectdemo.c -о selectdemo $ ./selectdemo /dev/tty /dev/mouse 1О hello /dev/tty: hello no input after 4 seconds no input after 4 seconds testing /dev/tty: testing
534
Средства межпроцессного взаимодействия (IPCJ. Как можно пообщаться? Я переместил мышь /dev/mouse: ( /dev/mouse: /dev/mouse: я
В этой программе иллюстрируется возможность программы ожидать, пока на входе не появятся данные либо с клавиатуры, либо от мыши. Более интересной была бы програм ма, которая вместо вывода сообщения производила бы некую обработку входных данных. Обобщенная информация о системном вызове select select НАЗНАЧЕНИЕ
Синхронизация при мультиплексировании ввода/вывода
INCLUDE
#include <sys/time.h>
ИСПОЛЬЗОВАНИЕ
int = selectfint numfds, fd_set *read_set fd_set *write_set fd,set *error_set, struct timeval *timeout); void FD_ZERO(fd_set *fdset) void FDJ5ET(int fd, fd_set *fdset) void FD*CLR(intfd, fd”set *fdset) void FDJSSET(int fd, fd.set *fdset)
АРГУМЕНТЫ
numfds - максимальное число fd для наблюдения + 1 read_set - ждать входных данных на этих дескрипторах write_set - ждать подтверждения успешности записи на этих fd errof set - ждать возникновения ситуации исключения на этих fd timeout - выход после истечения данного интервала времени
КОДЫ ВОЗВРАТА
-1 - при обнаружении ошибки 0 - при окончании таймаута num - число дескрипторов файла, удовлетворяющих критерию
select одновременно ведет наблюдение за несколькими файловыми дескрипторами. Выход
из вызова происходит по мере того, как на одном из контролируемых дескрипторов чтолибо произойдет. Более точно, вызов select контролирует события на трех наборах файло вых дескрипторов. С помощью одного набора select определяет - нет ли данных, готовых для чтения. С помощью другого набора select определяет - нет ли данных, готовых для записи. С помощью третьего набора select определяет - не возникла ли ситуация исключе ния. Каждый набор файловых дескрипторов представлен двоичным массивом. Значение аргумента numfds на единицу больше, чем число дескрипторов, за которыми будет вестись наблюдение. Выход из select происходит, когда возникнет любое событие, которое было определено с помощью аргументов при обращении к вызову. Или выход происходит, когда закончится интервал таймаута. Результатом выполнения select будет целое число, которое указывает сколько дескрипторов удовлетворили случившимся событиям. Нулевой указатель, который может быть задан при обращении к select, говорит о необходи мости игнорирования соответствующего условия.
15.3. Выбор соединения
535
15.2.3. select и talk В этой главе мы не будет писать программу talk. Локализация другого пользователя и уста новка соединения требует выполнения нескольких шагов. Например, для локализации другого пользователя необходимо выполнить поиск в файле utmp. Мы изучали все эти идеи и средства, которые требуется применить для выполнения последующих шагов. Каковы эти шаги? Какие требуется при этом выполнить системные вызовы?
15.2.4. select или poll Если вам не нравится использовать вызов select, то вы можете использовать вместо него вызов poll. Вызов select был разработан в Berkeley, а вызов poll был разработан в Bell Labs. Оба вызова выполняют аналогичные действия. В большинстве версий Unix до сих пор поддерживаются эти две версии вызова.
15.3. Выбор соединения Команда talk является хорошим примером системных программ в Unix: это синтез коопера ции и соединения процессов. При работе talk два процесса read и write будет работать с ин формацией точно так же как если бы данные находились в обычных дисковых файлах.'
Файловые дескрипторы в talk присоединены к клавиатуре, экрану и сокету (см. рисунок 15.3). Но они могут быть присоединены и к другим процессам или к другим устройствам. Пересылка данных между процессами - это важнейшая часть работы talk. Это одна из операций при работе с процессами. Выбор метода соединения является важ ным потому, что выбор предопределяет правильный алгоритм или структуру данных.
15.3.1. Одна проблема и три ее решения Проблема: передача данных от сервера клиенту. Каким же образом выбрать коммуни кационный метод, который можно было бы использовать? Рассмотрим time/date-сервер, который мы написали, используя технику потоковых сокетов. Один процесс может определить, сколько сейчас времени, а другой процесс хочет узнать сколько сейчас времени (см. рисунок 15.4). Каким образом можно передать информацию о времени от одного процесса другому процессу?
536
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
Три решения: файл, программный канал, разделяемая память. На рисунке 15.5 изобра жены три метода: один метод нам знаком, а два других являются новыми. Знакомым явля ется метод использования файла. Двумя новыми являются такие методы: именованные программные каналы и разделяемые сегменты памяти. В каждом из этих методов осуще ствляется передача данных соответственно через диск, через ядро, через пространство пользовательской памяти. Каковы детали, слабые и сильные стороны каждого из этих ме тодов?
15.3.2. Механизм IPC на основе использования файлов Процессы могут взаимодействовать, передавая информацию через файлы. Один процесс пишет в файл, а другие процессы будут читать данные из этого файла. Time/Date-cepeep использует файл. Не будем писать программу на С. Достаточно напи сать простой скрипт: #!/bin/sh # time-cepeep, файловая версия while true; do date > /tmp/current_date sleep 1 done
Сервер каждую секунду записывает в файл текущие дату и время. При выполнении опера ции перенаправления (символ >) происходит уничтожение содержимого файла, а потом про исходит запись даты и времени.
15.3. Выбор соединения
537
Time/Date-Knuenm использует файл. Клиент читает содержимое файла: #!/bin/sh # time-клиент, файловая версия cat /tmp/current_date
Замечания относительно решения с использованием файлов для IPC Доступ: Клиенты должны иметь возможность читать содержимое файла. Используя меха низм стандартных прав доступа к файлам, мы даем серверу право на запись в файл, а кли енты будут обладать только правами на чтение из файла. Множественность клиентов: Сразу несколько клиентов могут попытаться одновременно читать данные из файла. В Unix не устанавливается ограничений на количество процес сов, которые могут открыть один и тот же файл. Условия гонок: Сервер модифицирует содержимое файла. Он уничтожает старое содержи мое файла и заносит в файл новые значения даты и времени. Если клиент попытается чи тать данные на промежутке времени между уничтожением данных в файле сервером и записью в файл новых данных, то клиент получит либо пустые данные, либо частично правильный результат. Предотвращение условия гонок: Сервер и клиенты должны использовать определенный вариант мехнизма mutex. Далее мы рассмотрим методы файл-блокировок. Кроме того, если использовать lseek и write вместо creat, то файл никогда не будет пустым. Системный вызов write является атомарной операцией.
15.3.3. Именованные программные каналы Обычные программные каналы можно использовать только для соединения родственных процессов. Обычный программный канал создается процессом и уничтожается после того, как последний ni процессов закроет его.
Именованный программный канал, который также называют каналом FIFO, может быть использован для соединения неродственных процессов. Он может существовать независимо от существования процессов (Может существовать после его создания до своего явного уничтожения. - Примеч. пер.) - см. рисунок 15.6. Канал FIFO аналогичен неприсоединенному к водопроводу садовому шлангу, который лежит на газоне. Можно один конец этого шланга поднести к уху одного из экспериментаторов, а другой экспериментатор может чтолибо крикнуть на другом конце (Автор предлагает еще один необычный механизм общения между людьми: садовый шланг. - Примеч. пер.). Наши экспериментаторы, не являясь родст венниками, могут таким образом общаться через садовый шланг. При этом шланг будет су ществовать и в случае, когда никто не будет его таким образом использовать. Канал FIFO это программный канал, к которому можно обращаться по имени канала.
538
Средства межпроцессного взаимодействия (!РС). Как можно пообщаться?
Использование каналов FIFO 1. Как я могу создать канал FIFO? mkfifo(char *name, mode_t mode) С помощью библиотечной функции mkfifo будет создан канал FIFO, для которого будут установлены затребованные права доступа. Есть еще и команда mkfifo, которая при своем выполнении вызывает эту же функцию. 2. Как я могу удалить канал FIFO? При использовании unlink (fifoname) происходит удаление канала FIFO. Это делается точно так же, как происходит удаление обычного файла. 3. Как. я могу настроить канал FIFO, чтобы использовать его для приема сообщений, когда канал будет использован для соединения процессов? open (fifoname, 0_RD0NLY). Выполнение системного вызова open будет блокировано до тех пор, пока какой-либо процесс не откроет его на запись. 4. Как я могут настроить канал FIFO, чтобы иметь возможность посылать через него сообщения? open (fifoname, 0_WR0NLY). Выполнение системного вызова open будет блокировано до тех пор, пока какой-либо процесс не откроет канал FIFO на чтение. 5. Как два процесса будут общаться через канал FIFO? Процесс-отправитель должен будет использовать системный вызов write, а процесс-получатель должен будет использовать системный вызов read. Когда процесс-отправитель выполнит dose, то процесс-получатель обнаружит признак конца файла (При попытке процесса-получателя читать из канала FIFO. -Примеч. пер.). Time/Date сервер и клиент, использующие канал FIFO. Ниже приведены два скрипта. Первый - это сервер, а второй - клиент. Они взаимодействуют при передаче информации о времени с помощью канала FIFO. #!/bin/sh # time-cepeep while true; do rm -f Amp/time_fifo mkfifo Amp/timeJifo date > /tmpAimeJifo done #!/bin/sh # time-клиент cat /tmp/time_fifo
Замечания относительно решения с использованием каналов FIFO для IPC Доступ: Каналы FIFO используют механизм прав доступа точно так же, как это делается в отношении обычных файлов. Сервер должен иметь право на запись в канал. Клиенты должны иметь только права на чтение из канала. Множественность читателей: Поименованный программный канал по сути работает как очередь, а не как обычный файл. Процесс-писатель добавляет данные в очередь, а процессы-читатели удаляют данные из очереди. Каждый клиент при обращении к каналу FIFO извлекает оттуда строку с датой и временем. Поэтому сервер всякий раз должен повторно посылать в канал информацию о времени и дате.
15.3. Выбор соединения
539
Условие гонок: При построении сервера на основе использования канала не возникает проблемы состязаний (проблемы гонок). Системные вызовы read и write являются ато марными, если размер сообщения не превышает размер канала. При чтении сообщения изымаются из канала FIFO, а при записи информация заносится в канал. Ядро блокирует процессы до тех пор, пока процесс-писатель и процесс-читатель связаны через канал.. (При попытке чтения из пустого канала или при попытке записи в полный канал. Примеч. пер.) Нет необходимости использовать какие-либо дополнительные средства син хронизации. Серверы, которые читают из каналов FIFO. Time/date-cepeep написан с использовани ем канала FIF,0, где происходит блокировка до тех пор, пока клиент не откроет канал FIFO на чтение. В приложениях, где сервер читает из канала FIFO, происходит ожидание, пока клиент не запишет данные в канал. Что можно привести в качестве примера, где сервер ожидал бы клиентского ввода?
15.3.4. Разделяемая память По какому маршруту “путешествуют” данные, когда их передают между процессами через файл или канал FIFO? Системный вызов write копирует данные из памяти процесса в буфер ядра. Системный вызов read копирует данные из буфера ядра в память процесса.
Зачем процессам приходится копировать данные в/из ядра, если оба процесса развивают ся на одной машине, развиваются в различных частях пользовательского пространства па мяти? Два процесса на одной машине могут обменяться данными (или разделять данные), используя для этого механизм разделяемого сегмента памяти. Это часть пользователь ского пространства памяти, на которую два процесса имеют указатели (см. рисунок 15.7). В зависимости от установленных прав доступа оба процесса могут писать данные в это место памяти и читать оттуда. Информация разделяется, а не копируется, между процес сами. Разделяемую память можно рассматривать как глобальные переменные в отноше нии нитей. Свойства разделяемых сегментов памяти • Разделяемый сегмент памяти существует независимо от процесса. • Разделяемый сегмент памяти имеет имя, в качестве которого выступает ключ. • Ключ - это целое число. • Разделяемый сегмент памяти имеет собственника и права доступа к сегменту. • Процессы могут присоединить сегмент, после чего они получают указатель на сегмент.
540
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
Использование разделяемого сегмента памяти 1. Как я могу получить разделяемый сегмент? int segjd = shmget(key, size-of-segment, flags)
Если сегмент уже существует, то системный вызов shmget определяет его местораспо ложение. Если сегмент не существует, то вы можете с помощью аргумента flags задать требование на создание сегмента, указав при этом желаемые права доступа к сегменту. Это в некоторой степени напоминает работу системного вызова open. 2. Как я могу присоединить разделяемый сегмент памяти? void ptr = *shmat{segjd, NULL, flags) С помощью shmat разделяемый сегмент памяти становится частью адресного пространст
ва процесса. В результате получаем указатель на сегмент. При обращении можно исполь зовать флаги, если необходимо определить, чтобы сегмент был бы только читаемым. 3. Как я могу сделать разделяемый сегмент доступным на чтение и на запись данных? strcpy(ptr, "hello"); memcpyO, ptr[i] - это обычные операции с указателями.
Time/Date-cepeep, использующий механизм разделяемой памяти Г shm_ts.c: time-cepeep, в котором используется разделяемая память. * Будет выглядеть несколько эксцентричным приложением
7 tinclude <stdio.h> #include <sys/shm.h> tinclude tdefine TIME_MEM_KEY 99 tdefine SEG_SJZE ((size_t) 100) tdefine oops~(m,x) {perror(m); exit(x);} main()
{
/* это используется как имя для файла */ /* размер сегмента 7
int segjd; char *mem_ptr, *ctime(); long now; int n; Г создание разделяемого сегмента памяти */ segjd = shmget(TIME_MEM_KEY, SEG.SIZE, IPC_CREAT|0777); if (segjd ==-1) oops("shmget", 1); Г присоединить сегмент и получить указатель на место, к которому сегмент был присоединен
7
mem_ptr = shmat(segjd, NULL, 0); if (mem_ptr == (void *) -1) oopsfshmat", 2); /* запуск на исполнение в течение минуты 7 for(n=0; п<60; п++){ time(&now); /* получить значение текущего времени */ strcpy(mem_ptr, ctime(&now)); /* записать в mem */ sleep( 1); /* подождать 1 секунду */
)
/* теперь удалить сегмент */ shmctl(seg id, IPC RMID, NULL);
>
541
15.3. Выбор соединения
Time/Date-wiuenm, использующий разделяемую память
Г shmjc.c: time-клиент, в котором используется разделяемая память. *
Будет выглядеть несколько эксцентричным приложением
7
«include <stdio.h> «include <sys/shm.h> «include «define TIME_MEM_KEY 99 «define SEGjSIZE ((sizej) 100) «define oops(m,x) {perror(m); exit(x);} main()
{
/* это напоминает номер порта */ /* размер сегмента */
int segjd; char *mem_ptr, *ctime(); long now; f создать разделяемый сегмент памяти */ segjd = shmget(TIME_MEM_KEY, SEG_S!ZE, 0777); if (segjd ==-1) oops("shmget",1); /* присоединить сегмент и получить указатель на сегмент */ mem_ptr = shmat(seg_id, NULL, 0); if (mem_ptr == (void *) -1) oops("shmat",2); printf('The time, direct from memory:..%s", mem_ptr); shmdt(mem_ptr); [* отсоединить сегмент, если он уже не нужен */
} Замечания по средству IPC на основе использования разделяемой памяти Доступ: Клиенты при работе с сервером должны иметь возможность читать из разделяемого сегмента памяти. Разделяемые сегменты памяти используют механизм прав доступа, который работает аналогично механизму прав доступа в файловой системе. Разделяемый сегмент имеет собственника и имеет разряды доступа для пользователя, группы и всех остальных. Механизм разделяемой памяти можно контролировать для обеспечения защиты данных. Поэтому сервер имеет право на запись, а клиенты имеют только права на чтение. Множественность клиентов: из разделяемого сегмента могут одновременно читать дан ные сразу несколько клиентов. Условия гонок: Сервер модифицирует данные в разделяемом сегменте при вызове sircpy. Это библиотечная функция, которая исполняется в пользовательском пространстве. Если клиент читает данные из сегмента памяти в момент, когда сервер производит записи в сег мент новой строки, то клиент может прочитать комбинацию из новой и старой строк. Предотвращение условий гонок: Сервер и клиенты должны использовать некоторые систем ные средства синхронизации при доступе к разделяемым ресурсам. В ядре есть механизм синхронизации, который называется семафоры. Мы рассмотрим позже.
15.3.5. Сравнение методов коммуникации Исходной задачей была проблема передачи строки от одного процесса к другому. Все три метода могут быть использованы для решения этой задачи. Клиенты получают данные от сервера, когда они этого пожелают. Мы рассмотрели уже четыре версии клиент-серверных систем. Мы может даже написать версии, где использовался бы механизм дейтаграмм или механизм доменных адресов в Unix. Какой из методов кажется вам более предпочтитель ным? Какой критерий использовать при выборе?
542
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
Скорость При передаче данных через файлы и именованные каналы выполняется много действий. Ядро копирует данные в свое пространство, а затем передает эти данные обратно в поль зовательское пространство. При работе с файлами ядро копирует данные на диск, а затем копирует данные обратно в пользовательское пространство. На практике хранение данных в памяти оказывается более сложным, чем это может показаться. Система виртуальной па мяти производит свопирование страниц пользовательской памяти на диск. Поэтому разде ляемый сегмент памяти может также записываться на диск и читаться с диска. Наличие или отсутствие соединения Файлы и разделяемые сегменты памяти выступают в роли доски объявлений. Кто-либо вывешивает свое объявление на доску и уходит. Те, кто читает объявления, могут читать объявление в любое время. Причем объявление можно читать одновременно. Механизм программных каналов FIFO требует установления соединения между процессами. Процесс-писатель и процесс-читатель должны оба открыть канал FIFO до того, как ядро будет пересылать по каналу данные. При этом клиент может лишь только читать данные. В ме ханизме потоковых сокетов также требуется устанавливать соединение. А при использо вании дейтаграммных сокетов устанавливать соединение не требуется. Для некоторых приложений наличие или отсутствие соединения имеет достаточно важное значение. Область передачи Насколько далеко могут “путешествовать” ваши сообщения? Механизм разделяемой памяти и именованных программных каналов можно использовать для взаимодействия процессов, которые развиваются на одной и той же машине. Файл может храниться на файловом сервере и может быть использован для связи процессов, которые развиваются на различных машинах. Сокеты с Internet адресами могут соединять процессы на различных машинах, сокеты с Unix адресами не могут быть использованы для установления такой связи. Какую область пере дачи вы хотели бы использовать? Расширенную или ограниченную? Ограничения на доступ Вы желаете, чтобы каждый имел возможность работать с вашим сервером, или желаете установить ограничения на доступ к серверу и предоставить доступ только определенным пользователям? В механизмах, где используются файлы, каналы FIFO, разделяемая па мять, сокеты с Unix адресами, используются стандартные средства прав доступа, которые используются в файловой системе. В Internet-сокетах этого нет. Условия гонок Использование в программах механизма разделяемой памяти и разделяемых файлов дела ет программы более сложными, чем при использовании программных каналов и сокетов. Каналы и сокеты - это очереди, управление которыми производит ядро. Процесс-писатель помещает данные в один конец очереди, а процесс-читатель выбирает данные с другого конца очереди. Процессы при этом не заботятся о внутренней структуре очереди. Доступ к разделяемым файлам и разделяемой памяти ядром не контролируется. Если про цесс читает файл в тот момент, когда другой процесс пытается записать информацию в файл, то читающий процесс может получить либо неполные, либо искаженные данные. Далее мы рассмотрим блокировки (замки) файлов и семафоры.
15.4. Взаимодействие и координация процессов
543
15.4. Взаимодействие и координация процессов Что еще можно сказать об уже надоевших условиях гонок? Клиенты и серверы могут раз делять одни и те же файлы или одну и ту же памятьГКак мы можем предотвратить процес сы от получения данных каким-либо Другим путем? Как процессы могут координировать свои действия? Теперь мы рассмотрим средства, с помощью которых процессы могут пра вильно разделять ресурсы: блокировки файлов и семафоры.
15.4.1. Блокировки файлов Два вида блокировок Рассмотрим две проблемы. Во-первых, возникает вопрос - что случится, если сервер в те кущий момент занят переписыванием содержимого файла, а клиент попытается обратить ся к файлу на чтение? Клиент при обращении к файлу найдет его пустым или не полно стью сформированным. В нашем date/time-cepeepe мы не сталкивались с такой пробле мой, но в серверах, которые обрабатывают метеорологические данные, где используются более длинные сообщения, такая проблема, вероятно, возникнет. Поэтому, пока сервер занят переписыванием содержимого файла, клиенты должны ждать окончания этой про цедуры на сервере. Рассмотрим теперь противоположную ситуацию. Что произойдет, если клиент читает файл строку за строкой и неожиданно сервер отбирает у клиента файл, сбрасывает (транкатенирует) содержимое файла и начинает записывать в файл новые данные. Клиент убе дится, что файл изменился, увидев это собственными глазами. Поэтому, когда клиент читает файл, сервер должен ждать, когда клиент закончит процедуру чтения. Другим кли ентам нет необходимости ждать окончания чтения. Но если несколько процессов будут одновременно читать один и тот же файл, то в этом нет никакого риска. Для предотвращения указанных проблем нам нужно иметь два типа блокировок файла. Первый тип, блокировка по записи, означает: ”Я пишу в файл, и все остальные должны ждать, пока я не закончу это делать”. Второй тип, блокировка по чтению, означает: ”Я чи таю из файла. Процессы-писатели должны ждать, пока я не закончу это делать. Но другим процессом можно читать этот файл”. Программирование с использованием блокировок файлов В Unix есть три способа, которые позволяют блокировать открытые файлы: flock, lockf и fcntl. Наиболее гибким и переносимым из трех является метод с использованием fcntl. Использование fcntl для блокирования файлов 1. Как я могу установить блокировку по чтению для открытого файла? Для этого следует использовать fcntl (fd, F SETLKW, &lockinfo) Здесь первый аргумент-это файловый дескриптор, который должен быть блокирован по чтению. Второй аргумент, FJ5ETLKW, сообщает о вашем желании переводить текущий про цесс в состояние ожидания процесса, если блокировка была уже установлена. Нужно ждать, пока блокировка будет сброшена, если в этом есть необходимость. Третий аргу мент указывает на переменную типа struct flock. В следующем ниже коде устанавливается блокировка на чтение на файловый дескриптор: set read_lock(int fd)
{~ struct flock lockinfo; lockinfo.IJype = F_RDLCK;
/* блокировка на чтение раздела */
544
Средства межпроцессного взаимодействия (!РС). Как можно пообщаться?
lockinfo.l_pid = getpidO; lockinfo.l_start =0; lockinfo.l_whence = SEEK_SET; lockinfo.ljen =0; fcntl(fd, F SETLKW, SJockinfo);
/* для МЕНЯ*/ /* начинается со смещением 0 байт.. */ f* от начало файла */ /* до конца */
} 2. Как я могу установить блокировку на запись для открытого файла? Для этого следует использовать fcntl (fd, F_SEFLKW, &lockinfo) при установленном значении поля lockinfo.l_type = FJNRUCK; 3. Как я могу снять блокировку? Для этого следует использовать fcntl (fd, F_SETLKW, &lockinfo) при установленном значении поля lockinfo.IJype = FJJNLCK; 4. Как я могут блокировать только часть файла? Для этого следует использовать fcntl (fd, F_SETLKW, &lockinfo) при установленном значении поля iockinfo.I_start, равном значению смещения от начала, и при установленном значении поля lockinfo.ljen, равном длине области в файле. Код time-cepeepa, использующего технику блокирования файлов:
f file_ts.c - запись текущей date/time в файл Использование: file_ts имя_файла * Действие: записать текущие time/date в файл имя_файла * Замечание: используется метод блокировки на базе fcntlQ
*1 «include <stdio.h> «include <sys/Rle.h> «include «include «define oops(m,x) {perror(m); exit(x);} main(int ac, char *avQ)
{ int time_t now; char if (ac != 2){
fd; ‘message; fprintf(stderr,"usage: file_ts filename\n");
exit(1);
} if ((fd = 0pen(av[1],0_CREAT|0_TRUNC|0_WR0NLY,()644)) = -1) oops(av[1],2);
{ time(&now); message = ctime(&now); lockoperation(fd, F_WRLCK); if (lseek(fd, 0L, SEEK.SET) == -1) oops("lseek",3);
/* получить значение времени */ /* заблокировать файл по записи */
15.4. Взаимодействие и координация процессов
if (write(fd, message, strlen(message)) == -1) oopsfwrite", 4); lock_operation(fd, F_UNLCK); /* разблокировать файл */ sleep(1); /* ожидание нового момента времени
} } lock operation(int fd, int op)
{ struct flock lock; lock.l_whence = SEEK_SET; lock.i_start = lock.l_len = 0; lock.l_pid = getpid(); lock.l_type = op; if (fcntl(fd, F_SETLKW, &lock) == -1) oops("lock operation", 6);
} Код для time-клиента, использующего технику блокирования файлов:
Г fite_tc.c - чтение текущего значения date/time из файла * *
Использование: filejc имя_файла Замечание: на основе применения fcntl()
7 tinclude <stdio.h> tinclude <sys/file.h> tinclude tdefine oops(m,x) {perror(m); exit(x);} tdefine BUFLEN 10 main(intac, char*av[])
{ int char if (ac != 2){
fd, nread; buf [BUFLEN]; fprintf(stderr,"usage: file_tc filename\n"); exit(1);
} if ((fd= open(av[1 ] ,0_RD0NLY)) == -1) oops(av[1],3); lock operation(fd, F_RDLCK); while((nread = read(fd, buf, BUFLEN)) > 0) write(1, buf, nread); lock_operation(fd, F_UNLCK); close(fd);
} lockoperation(int fd, int op)
{ struct flock lock;
546
Средства межпроцессного взаимодействия (!РС). Как можно пообщаться?
lock.l_whence = SEEK_SET; lock.l_start = lock.ljen = 0; lock.l_pid = getpidO; lock.l type = op; if (fcntl(fd, F_SETLKW, &lock) = -1) oopsflock operation", 6);
} Блокирование файлов: итог При вызове fcntl с установленным F_SETLKV\^po устанавливает для процессу определенный вид блокировки. Клиенты могут установить блокировку на чтение перед тем, как читать данные. Если сервер установил блокировку' на запись, то клиенты будут задерживаться в моменты, когда с файлом работает сервер. Сервер должен установить блокировку на за пись до того, как он будет пытаться модифицировать содержимое файла. Если клиент установил блокировку на чтение, то сервер будет задерживаться до тех пор, пока все кли енты не сбросят свои блокировки на чтение. Важное замечание: процессы могут игнорировать блокировку В нашем обсуждении блокировок файлов мы предполагали, что программы сервера и кли ента будут при исполнении ждать установки и освобождения блокировок, когда они хотят читать или модифицировать разделяемые данные. Может ли процесс проигнорировать на личие блокировок и читать или изменять файл тогда, когда другой процесс пользуется блокировкой? Да. Unix предоставляет процессам возможность использовать блокировки при взаимодействии, но Unix не запрещает и другие формы взаимодействия.
15.4.2. Семафоры В версии time/date-клиента и сервера, которые были построены на основе использования метода разделяемой памяти, сегмент разделяемой памяти играл ту же роль, что и файл, в версии, где используется файла в качестве средства для передачи данных. Как при этом можно предотвратить взаимовлияние процессов? Устанавливаются ли блокировки при чтении и при записи в сегменты памяти? Нет, но процессы могут использовать весьма гиб кий механизм по обеспечению синхронизации: семафоры. Семафор - это переменная ядра, которая доступна для всех процессов в системе. Процесс может использовать такие переменные ядра для синхронизации доступа к разделяемой памяти и другим ресурсам. В главе, где рассматривались нити, было объяснено, как нити могут использовать условные переменные для того, чтоб оповестить другие нити, когда случается нечто интересное. Условные объекты являются глобальными в составе процес са. Семафоры же являются глобальными в составе системы. Каким образом можно использовать такие глобальные переменные для наших time-сервера и time-клиента? Счетчики процессов и операции над процессами и счетчиками Сервер производит запись в сегмент. Он должен ждать момента, когда ни один из клиен тов не читает из сегмента. Клиенты читают из сегмента. Они должны ждать, пока сервер не закончит запись в сегмент. Мы можем выразить эти правила работы с сегментами с по мощью значений переменных: • Клиент ждет, пока установится значение number_of_writers == 0 • Сервер ждет, пока установится значение numberjrfjeaders == 0
15.4. Взаимодействие и координация процессов
547
Семафоры представляют собой глобальные переменные в масштабе системы. Мы можем использовать один семафор для всех процессов-читателей, а другой семафор - для всех процессов-писателей. Для управления этими переменными необходимы две операции. Процесс-читатель, например, должен ждать, пока счетчик процессов-писателей не станет равным нулю, а затем немедленно инкрементировать счетчик процессов-читателей. Когда процесс-читатель закончит работу по чтению данных, то он должен будет декрементиро вать счетчик процессов-читателей. Процесс-писатель, аналогично, должен ждать, пока счетчик процессов-читателей не дос тигнет нуля, а затем инкрементировать счетчик процессов-писателей. Две операции ожидание, когда счетчик процессов-читателей достигнет нуля, и увеличение счетчика процессов-писателей, рассматриваются как единая неделимая операция. Другими слова ми, эта пара действий рассматривается как одна атомарная операция. Процесс, который использует семафоры для синхронизации своих действий, может использовать несколько переменных и выполнять в отношении них несколько атомарных операций.
Это еще не вся информация о семафорах. Процесс может выполять набор действий над семафорами, выполнять все сразу. >
Наборы семафоров, наборы действий Time-сервер использует два семафора (см. рисунок 15.8). ПроцессьГ-читатели и процессыписатели выполняют два набора действий. Прежде чем модифицировать разделяемую память, сервер должен выполнить такой набор действий: [0] ждать, пока numjeaders станет равным 0. [1] прибавить 1 к numj/vriters. Когда сервер закончит работу с разделяемой памятью, то он должен выполнить такой набор действий: [0] вычесть 1 из num_writers. Прежде чем читать из разделяемой* памяти, клиент должен выполнить такие действия: [0] ждать, пока значение num_writers достигнет 0. [1] прибавить 1 к numjeaders. Когда клиент закончит работу с памятью, то он должен выполнить такой набор действий: [0] вычесть 1 из numjeaders.
548
Средства межпроцессного взаимодействия (!РС). Как можно пообщаться?
Сервер shm_ts2.c К нашей предыдущей программе shm_ts.c добавим механизм семафоров и получим в резуль тате программу shm_ts2.c:
[* shm_ts2.c - time-сервер, который разделяет память, версия 2: * использование семафоров для реализации блокировки * Программа использует разделяемую память по ключу 99 * Программа использует семафорный набор по ключу 9900
7 tinclude <stdio.h> tinclude <sys/shm.h> tinclude ctime. h> tinclude <sys/types.h> tinclude <sys/sem.h> tinclude <signal.h> tdefine TIME_MEM_KEY 99 /* аналогично имени файла */ tdefine TIME’SEM JKEY 9900 tdefine SEG_SIZE ((size_t)100) /* размер сегмента */ tdefine oops(m,x) {perror(m); exit(x);} union semun {int val; struct semid_ds *buf; ushort ‘array;}; int segjd, semsetjd; /* глобальные переменные для cleanup!) */ void cleanup(int); main()
{ char *mem_ptr, *ctime(); time_t now; intn; /* создание разделяемого сегмента памяти */ segjd = shmget(TIМЕ_МEM_KEY, SEG.SIZE, IPC_CREAT|0777); if (segjd == -1) oops("shmget", 1); Г присоединение сегмента и получение указателя на сегмент */ mem_ptr = shmat(segjd, NULL, 0); if (mem_ptr == (void *) -1) oopsf’shmat", 2); Г создание набора семафоров semset: ключ 9900,2 семафора и права доступа: * rw-rw-rw
7 semsetjd = semget(TIME_SEM_KEY, 2, (0666|IPC_CREAT|IPC_EXCL)); if (semsetjd ==-1) oopsfsemget", 3); set_sem_value(semsetjd, 0,0); /* установить счетчики 7 set_sem_value(semsetjd, 1,0); /* в 0 7 signal(SIGINT, cleanup); /* запустить на исполнение в течение минуты 7 for(n=0; п<60; п++){ time(&now); /* поручить значение текущего времени */
,4. Взаимодействие и координация процессов
549
printf("\tshm_ts2 waiting for lock\n”); wait_and_lock(semsetjd); /* блокировка памяти */ printf("\tshm_ts2 updating memory\n"); strcpy(mem_ptr, ctime(&now)); /* запись в память */ sleep(5); release_lock(semset_id); /* снятие блокировки */ printf("\tshm_ts2 released lock\n“); sleep( 1); /* ожидать 1 секунду 7 cleanup(O);
)
) void cleanup(int n)
{ shmctl(seg_id, IPC_RMID, NULL); /* удалить разделяемую память */ semctl(semset id, 0, IPC_RMID, NULL); /* удалить семафорный набор */
} Л * инициализация семафора
7 set sem value(int semset id, intsemnum, intvai)
{ union semun initval; initval.val = val; if (semctl(semsetjd, semnum, SETVAL, initval) == -1) oopsfsemctl", 4);
} /* * построение и выполнение набора действий над элементами: * ожидать достижение 0 на n readers и инкремент счетчика n writers
7 wait and lock(int semset id)
{ struct sembuf actions[2]; /* набор действий */ actions[0].sem_num = 0; /* sem[01 - это счетчик n_readers 7 actions[0].sem_flg = SEM_UNDO; /* автоматическое восстановление 7 f* ожидать,когда счетчик читателей достигнет нуля 7 actions[0].sem_op = 0; actions[1].sem_num = 1; Г sem[1] - это счетчик n_writers 7 actions[1].sem_flg = SEM_UNDO; Г автоматическое восстановление 7 /* инкремент счетчика писателей 7 actions[1].sem_op = +1 ; if (semop(?emset_id, actions, 2) == -1) oops("semop: locking", 10);
}
Г *построение и выполнение набора действий из одного элемента: декремент счетчика num_writers
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
550
release_lock(int semsetjd)
{ struct sembuf actions[ 1 ]; /* набор действий 7 actions[0].sem_num = 1; /* sem[0] - это счетчик n_writers 7 actions[0].sem_flg = SEM_UNDO; /* автоматическое восстановление 7 actions[0].sem_op = • 1 ; /* декремент счетчика писателей 7 if (semop(semsetJd, actions, 1) == -1) oopsf'semop: unlocking", 10);
} Сервер выполняет пять действий в отношении семафорного набора: 1. Создает семафорный набор: semsetjd = semget (key_t key, int numsems, int flags) С помощью semget создается семафорный набор, в котором будет находиться numsems семафоров. В программе shm_ts2 создается набор из двух семафоров. Набор имеет права
доступа 0666. После выполнения semget в качестве результата возвращается идентифи катор (ID) семафорного набора. 2. Устанавливает значения двух семафоров в 0: semctl (int semsetjd, int semnum, int cmd, union semun arg)
Мы используем semctl, чтобы управлять семафорным набором. Первый аргумент-это идентификатор набора. Второй аргумент - это количество составных семафоров в на боре. Третий аргумент - это команда управления. Если для команды управления также необходим аргумент, то для этого при обращении к semctl будет использован четвертый аргумент. В программе shmjs2 мы использовали команду управления SETVAL, с помо щью которой была произведена инициализация каждого составного семафора - значе ние каждого семафора было установлено в 0. 3.
Ждет, когда счетчик читателей станет равным нулю, затем инкрементирует счетчик писателей num_writers: semop (int semid, struct sembuf ^actions, size_t numactions)
С помощью semop выполняется набор действий над семафорным набором. Первый аргумент идентифицирует семафорный набор. Второй аргумент - это массив дейст вий. С помощью последнего аргумента задается размер массива действий. Каждое дей ствие в наборе действий - это структура, с помощью которой задается такое требование:”Выполнить операцию sem_op над семафором под номером semjium с использова нием опций sernflg”. Весь набор действий выполняется как групповое действие. Это главная особенность. Функция waitjindjock предназначена для выполнения двух дейст вий: ожидание, когда счетчик читателей достигнет нуля, и потом инкрементирование счетчика писателей. Мы создали массив из двух действий. Нулевое действие выражает такое требование: “Ожидать, когда значение семафора станет равным 0”. Первое дей ствие выражает такое требование: ’’Прибавить 1 к семафору 1”. Процесс будет блокирован до тех пор, пока не будут успешно выполнены оба этих действия. Когда счетчик читателей достигнет 0, будет увеличен на 1 счетчик писателей. После этого производится выход из semop. С помощью флага SEMJJND0 для ядра устанавливается требование на выполнение дей ствия отката (undo), если процесс будет закончен (Имеется в виду аномальное оконча ние процесса. -Примеч. пер.). В данном приложении посредством инкремента счетчи ка писателей достигается эффективная блокировка сегмента памяти. Если процесс
15.4. Взаимодействие и координация процессов
551
“погибнет” до момента выполнения декремента счетчика, то другие процессы уже не смогут использовать разделяемый сегмент памяти (Тем самым автор описывает потен циальную опасность возникновения тупиковой ситуации, если не будет использована возможность отката. - Примеч. пер.). 4. Декремент счетчика писателей num_writers: В release_IOCk мы выполняем только одно действие: декремент счетчика писателей. Мы обращаемся к semop, подготовив предварительно массив действий, где будет содержаться одно действие. Если клиент ждет, когда счетчик писателей достигнет нуля, то такому клиенту будет предоставлена возможность возобновить свое исполнение. 5. Удаление семафора: semctl(semset_id, О, IPC_RMID, 0)
Когда сервер сделает все необходимое, то он вызывает опять semctt с тем, чтобы уда лить семафор. Клиент: shm_tc2.c Программа клиента проще - shm_tc2. В программе shm_tc2 нет действий по инициализации семафоров, а также нет и удаления семафоров.
Г shm_tc2.c - time-клиент, разделяющий память Версия2: * использование семафоров для блокировки процессов * Программа использует разделяемую память по ключу 99 * Программа использует семафорный набор по ключу 9900
7 #include <stdio.h> tinclude <sys/shm.h> tinclude tinclude <sys/types.h> tinclude <sys/ipc.h> tinclude <sys/sem.h> [* ключ напоминает номер порта 7 tdefine TIME_MEM_KEY 99 tdefine TIME_SEM_KEY 9900 /* ключ напоминает имя файла 7 tdefine SEG_SIZE ((size_t)100) Г размер сегмента 7 tdefine oops(m,x) {perror(m); exit(x); union semun {int val; struct semid_ds *buf; ushort *array;}; main()
{
int segjd; char *mem_ptr, *ctime(); long now; int semsetjd; /* id для семафорного набора */ Г создание разделяемого сегмента памяти */ segjd = shmget(TIME_MEM_KEY, SEG_SIZE, 0777); if (segjd ==-1) oops("shmget",1); /* присоединение сегмента и получение указателя на сегмент */ mem_ptr = shmat(segjd, NULL, 0); if (mem_ptr == (void *) -1) oops("shmat",2); .> Г установление связи с семафорным набором 9900 с двумя составными * семасЬсюами
2
Средства межпроцессного взаимодействия (!РС). Как можно пообща
7 semsetjd = semget(TIME_SEM_KEY, 2,0); wait.andJock(semsetjd); ” printffThe time, direct from memory:..%s", memjjtr); release_lock(semsetjd); shmdt(fnem ptr); /* отсоединение сегмента */
} Г
* построение и использование набора действий, состоящего из двух элементов: * ожидание, когда счетчик писателей станет равным 0 И инкремент счетчика * читателей
7
wait and lock(int semset id)
{
union semun semjnfo; /* свойства */ struct sembuf actions[2]; f* набор действий */ actions[0].sem_num = 1; Г sem[1] - это счетчик писателей n_writers */ actions[0].sem_flg = SEMJJNDO; Г автоматическое восстановление */ actions[0j.sem_op = 0; /* ожидать, когда достигнет 0 */ actions[1].sem_num = 0; /* sem[0] - это счетчик читателей njeaders */ actions! 1 j. sem jig = SEM_UNDO; Г автоматическое восстановление*/ Г инкремент счетчика читателей пег njeaders */ actions[1].sem_op = +1 f if (semop(semsetjd, actions, 2) == -1) oopsf'semop: locking", 10);
} Г
* построение и использование одноэлементного набора действий: * декремент счетчика читателей num readers
7
release lock(int semset id)
{
union semun sem jnfo; /* свойства 7 struct sembuf actions! 1 ]; /* набор действий */ actions[0] .sem_num = 0; /* sem[0] - это счетчик читателей njeaders 7 actionstO] .sem_flg = SEMJJNDO; /* автоматическое восстановление*/ . actions[0j .sem'op = -1; /* декремент счетчика читателей */ if (semop(semset_id, actions, 1) == -1) oops("semop: unlocking”, 10);
} Компиляция и проверка работы данных программ:
$ сс shm_ts2.c -о shmserv $ сс shm_tc2 .с -о shmclnt $ ./shmierv & [1] 15533 shm_ts2 waiting for lock shm’ts2 updating memory $ shm_ts2 released lock shm~ts2 waiting for lock shm_ts2 updating memory
$ ./shmcint
shm_ts2 released lock The time, direct from memory:..Sat Oct 27 17:36:34 2001 $ shm_ts2 waiting for lock shm"ts2 updating memory
15.4. Взаимодействие и координация процессов
553
$ ./shmclnt shm_ts2 released lock The time, direct from memory:..Sat Oct 27 17:36:40 2001 $ shm_ts2 waiting for lock ipcs
-------Shared Memory Segments -. key shmid owner 0x00000063 30670854 bruce
perms 777
bytes 100
nattch 1
perms 666
nsems 2
status
perms
used-bytes messages
-------Semaphore Arrays -..key semid owner 0x000026ac 262146 bruce
-------Message Queues -..... key msqid owner $ shmjs2 released lock shm_ts2 waiting for lock $ kill -INT 15533 $ semop: unlocking: Invalid argument
Тестовый запуск программ на исполнение показывает, что клиент ждет, когда сервер сни мет блокировку. Одновременно могут работать несколько клиентов. Каждый клиент будет ждать, когда счетчик сервера достигнет 0, и затем клиент инкрементирует счетчик клиен тов. Если одновременно три клиента читают из разделяемой памяти, то счетчик читателей будет равен трем. Сервер будет ждать, когда эти три клиента декрементируют счетчик чи тателей. Конечно, программа не претендует на отработку всех возможных ситуаций. Например, как предотвратить одновременное исполнение сразу двух серверов? Ведь сервер ждет, только когда счетчик читателей достигнет 0, и не проверяет значение счетчика писателей. Ожидание на семафоре - положительное решение проблемы Наш клиент ждет, когда значение семафора “число_писателей” станет равным нулю. А сервер ждет, когда значение семафора “число_читателей” станет равным нулю. В нашей программе у нас можем возникнуть желание ждать на семафоре до того момента, когда его значение станет равным некоторому положительному числу. Например, нам может понадоиться дождаться, когда значение семафора станет равным 2. Как это можно выпол нить в программе? Используем для этого не совсем очевидный метод: обратимся к ядру с требованием вычесть 2 из значения семафора. Семафоры не могут принимать отрицательных значений. Поэтому ядро блокирует вызов до момента, когда значение семафора станет равным 2 или большим. Заметим, как все просто. Когда значение семафора станет равным 2, наш про цесс вычтет свою 2. При этом любой другой процесс, который хотел бы вычесть 2 из значения семафора, будет блокирован. Член sem_op в операции работает следующим образом: semj)p - положительное число
Действие: Инкремент значения семафора на величину sem op. sem_op - нуль
Действие: Блокировка процесса до момента, когда значение семафора станет равным нулю. sem_op - отрицательное число
Действие: Блокировка процесса до момента, когда после прибавления величины sem_op к значению семафора полученное значение не будет отрицательным.
554
Средства межпроцессного взаимодействия (!РС). Как можно пообщаться?
15.4.3. Сравнение сокетов и каналов FIFO с разделяемой памятью Мы написали четыре версии time/date-cepBepa и клиента. Версии с использованием соке тов и каналов FIFO были весьма простыми. Клиент соединялся с сервером, сервер посы лал некоторые данные, и процессы разъединялись. Версии с разделяемой памятью и раз деляемыми файлами на первый взгляд выглядят просто. Но они требуют использования средств блокировки или семафоров для защиты данных от неправильного использования. При добавлении средств блокировки или семафоров такие версии будут более предпочти тельны в работе. При использовании файлов и разделяемой памяти сразу несколько клиентов могут читать информацию от одного сервера. При этом клиенты и серверы с данными работают не од новременно. Данные будут сохранены, если произойдет аварийное окончание процессов. Но даже при использовании программных каналов и сокетов могут возникнуть некоторые виды блокировок. Каналы и сокеты при реализации з свою очередь представляют собой сегменты памяти, через которые данные передаются от источника данных к потребителю данных. Но уже ядро, а не сам процесс будет управлять блокировками и семафорами, с тем чтобы защитить данные в таких сегментах от искажений.
15.5. Спулер печати Один time/date-cepBep посылает данные нескольким клиентам. В некоторых приложениях работа происходит в обратном порядке: несколько клиентов посылают данные одному серверу. Так, например, организована работа с спулером печати. Какие вопросы проек тирования могут здесь возникнуть?
15.5.1. Несколько писателей, один читатель Несколько пользователей разделяют один принтер (см. рисунок 15.9). Как можно исполь зовать модель клиент/сервер при разработке программы, которая позволяла бы разделять принтер? Одновременно сразу несколько пользователей могут послать требование на печать, но принтер в каждый момент времени может печатать только один файл. Програм ма организации печати должна воспринимать множественный ввод данных и вырабаты вать один поток на вывод, предназначенный для выдачи на принтер. Что должен делать сервер? Что должны выполнять клиенты? Как они должны взаимодействовать?
15.5. Спулер печати
555
Какие должны быть использованы функциональные устройства? Какие данные и сообще ния необходимо использовать для взаимодействия этих компонентов?
Наиболее простой способ напечатать файл в системе-Unix - выполнить команду: cat filename > /dev/lp 1 или ср filename /dev/lp1
Здесь /dev/lp1 - это имя файла устройства для принтера. В вашей системе файл устройства для принтера может иметь другое имя. Но при этом все равно будет возможность исполь зовать метод посылки данных на принтер или другое устройство, если открыть файл устройства с помощью open, а потом выполнить write.
Можем ли мы использовать блокировки по записи? Мы знаем о наличии блокировок по записи и о семафорах. А почему нельзя написать специ альную версию программ cat или ср, которые были бы ориентированы на печать и которые ис пользовали бы при своей работе блокировки по записи на устройстве для предотвращения од новременного доступа со стороны процессов? Пусть имеется работающая программа копиро вания, которая использует механизм блокировок файла. Если одна программа установит бло кировку на принтер, то другие источники информации для программы копирования будут блокированы до тех пор, пока первое из заданий не сбросит блокировку. Какой процесс будет далее обслужен принтером? Ядро может разблокировать один из процессов, но при этом не будет учтена очередность поступления запросов на печать. Решить проблему выбора среди запросов совсем не просто. Вторая проблема, связанная с управлением печати на принтере, заключается в. том, что некоторые пользователи могут попытаться схитрить и поэтому не бу дут использовать для доступа к принтеру специальную программу. Третья проблема заключа ется в том, что некоторые файлы требуют специальной обработки. Например, файл с изображением нужно будет конвертировать в графические команды, которые будут понятны принтеру. Многие пользователи не знают, какие программы следует использовать для конвертирова ния данных в печатный формат. Поэтому без использования этих программ их ждет раз очарование, когда они увият результат распечатки. Решение этих проблем достигается за счет централизации.
556
Средства межпроцессного взаимодействия (IPC). Как можно пообщаться?
15.5.2. Модель клиент/сервер Использование в программе модели клиент/сервер решает те проблемы печати, которые мы рассмотрели. Программа сервер, которая будет называться демоном печати, имеет возможность выполнять печать на принтере, а другие пользователи лишены такой воз можности (см. рисунок 15.11). Каждый пользователь должен запустить клиентскую про грамму 1рг тогда, когда появится необходимость напечатать файл. Программа 1рг копирует файл и затем помещает эту копию в очередь заданий на печать. Пользователь может уда лить или отредактировать файл по мере того, как будет печататься копия. Демон печати может использовать программы преобразования для правильной печати изображений и шрифтов.
Как взаимодействуют клиент и сервер? Какими данными они обмениваются? Передает ли клиент серверу весь файл или клиент только посылает серверу имя файла? Что будет, если клиент развивается на одной машине, а сервер - на другой? Как выбрать метод связи меж ду процессами? Для Unix было разработано много различных систем печати. В некоторых используются сокеты, в других используются программные каналы. В некоторых исполь зуют только fork и файлы. А как обстоят дела в части обеспечения кооперации, использования блокирования файлов, взаимного исключения? Разработайте компонентную модель, коммуникационную модель и модель кооперации для поддержки печати так, чтобы все это работало на одной машине. Затем выполните проект таким образом, чтобы все это работало под управлением Internet. Сравните идеи, которые будут использованы вами, с идеями, принятыми в различных сис темах печати Unix.
557
15.6. Обзор средств IPC
15.6. Обзор средств IPC Нами были изучены различные формы для организации межпроцессного взаимодействия. В обобщенном виде эти результаты можно представить в виде таблицы. Метод exec/wait
Тип М
environ
м
pipe
S
kill-signal
м
inet сокет
S
inet сокет
м
Unix сокет
S м
Unix сокет
Различные машины
★ *
Р/С
Sib
Unrel
Различные нити
★ ★ *
*
*
*
★
*
? ? ? ?
? ? ? ?
? ? * ★
? ? ? ?
?
?
★
?
*
Именованный канал
S
Разделяемая память
R
★
★
*
?
Очередь сооб щений
М
★
*
*
★
Файлы
R М
N
*
*
*
Блокировки файлой
С
N
*
*
■к
? * *
Семафоры
с с с
*
*
*у
?
★
*
Переменные
mutexes link
•
★
*
Сокращения: Р/С - отношение типа клиент/сервер (parent/child) Sib - отношения на уровне братьев (sibling) Unrel ~ неродственные (unrelated) процессы М - посылака сообщений среднего размера (messages) S - поток (stream) данных с использованием read и write R ~ случайный (random) доступ к данным С - используется для задач синхронизации/координации * - соответствующее приложение ? - несоответствующее приложение N - соответствующее с сетевой файловой системой В таблицу не включены TLI и их производные - сетевые средства Bell Labs.
?
558
Средства межпроцессного взаимодействия (!РС). Как можно пообщаться?
Пояснения fork-execv-argv, exit-wait
Эти системные вызовы используются для обращения к программе с передачей ей при вы зове списка аргументов, а также для того, чтобы передать вызывающей программе целое число при окончании вызываемой программы. Процесс-отец использует fork для создания нового процесса. Программа в новом процессе вызывает execv для запуска на исполнение новой программы и для передачи ей списка параметров. Дочерний процесс с помощью exit перадает процессу-отцу целое значение, которое отец принимает с помощью wait. Метод ориентирован на сообщения, предполагает только родственные процессы, про цессы развиваются только на одной машине. environ
Системный вызов ехес автоматически копирует в новую программу массив строк, на который выставлен указатель через глобальную переменную environ. С помощью этого метода программа может передавать требуемые значения дочернему процессу и последующим потомкам, которые будут потом запускать программы. Среда копиру ется. Поэтому дочерний процесс не может изменить среду процесса-отца. Свойства метода: ориентирован на сообщения, передача информации в одном направ лении, только для родственных процессов, процессы только на одной машине. pipe
Программный канал (pipe) - это однонаправленный поток данных, который создается процессом. С ним связаны два файловых дескриптора, которые содержатся в ядре. Данные, записанные через один файловый дескриптор, могут быть считаны через дру гой файловый дескриптор. Если после создания программного канала процесс вызыва ет fork, то новый процесс может писать или читать из этого же самого канала, что обес печивает передачу данных от одного процесса к другому. Свойства метода: потоковая ориентация, передача данных обычно в одном направле нии, процессы только на одной машине. kill-signal
Сигнал - это одно целочисленное сообщение, которое передается от одного процесса (с использованием системного вызова kill) к другому. Принимающий процесс может ус тановить функцию обработки сигнала, используя для этого системный вызов signal. Сигнал, когда поступит процессу, будет перехвачен. Свойства метода: ориентирован на сообщения, однократная передача в одном направле нии, передача только между процессами с одним UID, процессы только на одной машине. Internet-coKembi Internet-сокеты являются соединением между конечными точками, установленными на машине с определенным номером порта. Данные передаются от одного процесса к другому посредством побайтной передачи данных через один сокет и приема их на другом сокете. Это аналогично тому, как если бы один человек говорил бы по телефону в Бостоне, а другой человек слушал бы его в Токио. Internet-сокеты разделяются на два типа: потоковые сокеты и дейтаграммные сокеты. Оба типа - двунаправленные. Пото ковые сокеты работают аналогично файловым дескрипторам. Программист использу ет вызовы write и read для передачи и приема данных. Дейтаграммные сокеты работают по принципу пересылки почтовых открыток. Процесс-писатель посылает процессучитателю небольшой буфер с данными. Все транзакции производятся с буферами дан ных, а не с потоками данных.
15.6. Обзор средств !РС
559
Свойства метода: есть поточные версии и есть версии, ориентированные на сообще ния, двунаправленная передача данных, процессы не должны быть родственными, процессы могут быть разнесены по разным машинам. Именованные сокеты Именованные сокеты, которые также называют Unix-доменные сокеты, используют в качестве адреса имя файла, а не адресную пару: имя_хоста-номер_порта. Именован ные сокеты используют в потоковых и дейтаграммных версиях. Поскольку в качестве адреса используются имена файлов, а не “хост-порт”, то они могут соединять процес сы только на одной машине. Свойства метода: есть поточные версии и версии, ориентированные на сообщения, двунаправленная передача данных, процессы не должны быт*> родственными, процес сы могут развиваться только на одной машине. Именованные программные каналы (FIFO) Именованный программный канал работает аналогично обычному (неименованному) программному каналу. Но он может связывать не родственные процессы. Именован ные каналы идентифицируются с помощью имен файлов. Процесс-писатель открывает файл для записи, используя вызов open. Процесс-читатель открывает файл на чтение, используя вызов Open. Такие каналы используются аналогично именованным сокетам, но они позволяют передавать данные только в одном направлении. Свойства метода: потоковая ориентация, передача данных в одном направлении, пере дача между произвольными (не родственными) процессами, процессы расположены на одной машине. Блокировки файлов Unix предоставляет возможность процессам устанавливать блокировки на секции фай лов. Один процесс может блокировать секцию файла. После чего он может потом мо дифицировать эту секцию. Другой процесс, который попытается обратиться к бло кированной секции, будет приостановлен или будет извещен о блокировке секции файла. Такие блокировки позволяют одному процессу взаимодействовать с другим процессом, независимо от того, кто из них будет писать, а кто читать из файла. Для ус тановки и проверки совещательной (advisory) блокировки могут быть использованы системные вызовы flock, lockf и fcntl. На некоторых системах доступны для использова ния принудительные (compulsory) блокировки. Свойства метода: ориентированный на сообщения, произвольные направления пере дачи, передача между произвольными (не родственными) процессами, процессы рас положены на одной машине. Разделяемая память , Каждый процесс имеет собственное пространство данных. Любые переменные, которые определены в программе или появляются при работе программы, будут доступны только этому процессу. Процесс может с помощью вызовов shmget и shmat создать участок памяти, который может разделяться с другими процессами. Данные, которые один процесс запи сывает в такой разделяемый участок памяти, могут быть прочитаны любым другим про цессом, который имеет доступ к этому участку памяти. Это наиболее эффективная форма для IPC, поскольку для ее реализации не требуется пересылки данных. Свойства метода: асинхронный доступ, произвольные направления передачи, пере дача между произвольными (не родственными) процессами, процессы расположены на одной машине.
560
Сродства межпроцессного взаимодействия (/PC). Как можно пообщаться?
Семафоры Семафоры - переменные с системным масштабом использования, которые программы могут использовать для коордйнации своей работы. Процесс может инкрементировать и декрементировать значение семафора, может ожидать на семафоре, когда его значе ние достигнет требуемой величины. Семафоры работают как билеты (tickets) при рабо те с лицензионным сервером. Когда процесс ждет использования ресурса, то он декре.» мёнтирует значение семафора. Если в текущий момент нет ни одного билета , то про цесс будет заблокирован до тех пор, пока другой процесс не инкрементирует значение семафора. Семафоры могут быть использованы самым разным образом. Свойства метода: доступ, ориентированный на сообщения, произвольные направления передачи, передача между произвольными (не родственными) процессами, процессы расположены на одной машине. Очереди сообщений Очереди сообщений напоминают программные каналы FIFO. Но они не используют име на файлов. Процессы могут добавлять сообщения в очередь, а также могут выбирать сообщения из очереди. Множество процессов могут разделять множество очередей. Свойства метода: доступ, ориентированный на сообщения, передача данных, только водном направлении, передача между произвольными (не родственными) процесса ми, процессы расположены на одной машине. Файлы Файл одновременно может быть открыт более чем одним процессом. Если один про цесс записывает данные в файл, то другой процесс может прочитать эти данные. Важно, что один и тот же файл могут открыть одновременно несколько процессов. При правильно спланированном протоколе можно реализовать сложные коммуникации между процессами на основе использования обыкновенных файлов. Свойства метода: случайный доступ, произвольные направления передачи, передача между произвольными (не родственными) процессами, при использовании NFS можно устанавливать межмашинные соединения.
15.7. Соединения и игры В этой главе были рассмотрены варианты передачи данных между процессами. Ядро в Unix управляет процессами, файлами, устройствами. Оно выполняет определенные действия с программными каналами, сокетами, файлами, разделяемой памятью и сигналами при оранизации передачи данных с одного места в другое. Для некоторых программ создание систе мы передачи данных и организация передачи данных является основной целью. Кен Томпсон, один из авторов Unix, писал в 1978 году:
Ядро UNIX представляет собой скорее мультиплексор ввода/вывода, нежели собст венно операционную систему. Но ведь так и должно быть. В соответствии с такой точкой зрения, в большинстве других операционных систем можно обнаружить ряд свойств, которые были удалены из ядра UNIX. ...Многие из возможностей реали зуются в прикладном ПО, которое использует ядро в качестве инструментального средства1. В первой главе мы рассматривали утилиту be, Web-cepeep и игру в бридж через Internet. Далее мы написали версию Ьс и два Web-сервера. А почему бы не написать сетевой вари ант игры в бридж? Вы можете использовать curses для разработки пользовательского ин 1. “Unix Implementation,” Bell System Technical Journal, vol. 56, no. 6, 1978.
Заключение
561
терфейса, а также механизм сокетов для установления связей. Кто будет выступать в каче стве серверов? Кто будет в качестве клиентов? Какое средство может быть использовано для организации блокирования? Все необходимые средства, которые могут вам понадо биться при разработке так или иначе были рассмотрены в предшествующих главах. Про граммирование в Unix не такое трудное, как это представляется с первого взгляда. Но оно и не так просто, как это может показаться вначале. При упоминании об играх и сетях следует обратиться к высказыванию Денниса Ричи, который так описывал игру Космическое путешествие (SpaceTravel), которая привела в результате к Unix: Написанная вначале для Multics, игра не была средством моделирования перемещения крупных тел в Солнечной системе. Это была игра с игроком, управляющим в космосе
космическим кораблем, который наблюдал пейзаж и пытался посадить корабль на различных планетах и лунах. Управление космическим кораблем, наблюдение за пейзажем и попытка посадить корабль на неких планетах и лунах - это своего рода Web-серфинг. Может быть, упоминание о серфинге не является хорошей метафорой. Но пользователи действительно с помощью своего Web броузера перемещаются куда угодно. Web-серверы передают по запросам “пейзажи” удаленных мест. Люди используют telnet, ssh и ftp, чтобы “высадиться” на других машинах. Возможно, что Internet оказался непреднамеренной реализацией экспансивного простран ства Ричи и Томпсона, которое они смоделировали еще в 1969 году?
Заключение Основные идеи •
•
•
• •
Многие программы при своем исполнении состоят из двух или более процессов, которые при работе составляют систему, где производится разделение данных или происходит передача данных между процессами. Два человека, которые используют, например, команду talk, запускают два процесса. Эти процессы передают данные от клавиатур и сокетов на экраны и сокеты. Для некоторых процессов необходимо принимать данные от множества источников и посылать данные, опять же, множеству потребителей. Использование системных вызовов select и poll предоставляет процессу возможность ожидать ввода от множества файловых дескрипторов. В Unix поддерживается несколько методов для организации передачи данных между процессами. Именованные программные каналы и разделяемая память - это два меха низма которые могут быть использованы для передачи данных между процессами на одной машине. Коммуникационные методы сравниваются по разным характеристи кам: скорость передачи данных, тип передаваемых сообщений, диапазон передачи, поддержка ограничений на доступ, защита от возможных искажений данных. Блокировки файлов - это средство, которое процессы могут использовать для предотвращения искажений данных в файлах. Семафоры - это переменные системного масштаба доступа, которые процессы могут использовать для синхронизации своих действий. Один процесс может ожидать воз можности изменить семафор, а другой процесс может в этот момент его изменять.
562
Срщства межпроцессного взаимодействия (!РС). Как можно пообщаться?
Чтодальше? Хороший Способ еще более узнать о возможностях программирования в Unix - это про должить изучать имеющиеся программы и писать собственные программы. Очень много информации можно получить через Интернет, во многих книгах можно найти материал, который поможет изучить детали внутренней структуры Unix и детали программного ин терфейса. Рассматривайте программы, которые вы используете ежедневно, обращайтесь к новым программам, которые вызывают у вас интерес. В процессе использования и изучения программ, в процессе написания ваших собственных версий программ вы обо гатите и расширите ваше понимание Unix.
Исследования 15.1Humu вместо select. А почему бы не использовать нити для чтения из двух файловых дескрипторов в программе talk? Одна нить будет читать данные с клавиатуры, а другая нить будет читать данные от сокета. Какие новые трудности добавятся в программе, где будет реализовано многонитьевое решение? 15.2 TCP или UDP? Программа talk читает и записывает в большей части отдельные симво лы, но при этом использует для передачи данных потоковые (stream) сокеты. Каковы могут быть преимущества и каковы недостатки, если для программы использовать дейтаграммные сокеты? 15.3 Time-cepeep, построенный на основе использования каналов FIFO, блокируется при вы полнении date >ytmp/Hme_fifo до тех пор, пока клиент не откроет канал FIFO на чтение. Если сервер будет блокирован надолго, то клиент получит значение времени, когда сервер был блокирован или когда сервер был разблокирован? Почему? 15.4 Разделяемая память и файлы. Ознакомьтесь с документацией по вызову mmap. Вызов mmap дает возможность представить секцию файла в виде массива в памяти. Тем самым программе предоставляется возможность произвольного доступа к данным в файле, не используя для этого вызов lseek. Насколько сравнима возможность работы с данными с помощью mmap с возможностью работы с данными с помощью средств на основе использования файлов или разделяемой памяти при решении задачи передачи данных между процессами? Какие преимущества и какие недостатки можно отметить у метода mmap по сравнению с другими методами? 15.5 Сервер talk. Объясните работу talk который визуализирует сообщения у двух связан ных процессов. Поэкспериментируйте с talk, чтобы понять, как реализовано соедине ние между процессами. Какие еще программы могут быть подключены для проведе ния исследования?
Программные упражнения 15.6 Обратитесь к справочнику и проверьте - поддерживаются ли на вашей системе систем ные вызовы select и pQll? На некоторых системах может быть один системный вызов, а дру гой эмулируется с помощью другого. Перепишите программу selectdemo.c и используйте в ней вызов poll. 15.7 Напишите версию time/date-cepBepa и клиента, в которых используются : (a) Дейтаграммные сокеты с Inernet-адресами. (b) Потоковые сокеты с Unix-доменовыми адресами. 15.8 Напишите С-версию date/time-cepeepa и клиента, где используются каналы FIFO.
Заключение
563
15.9 Множество серверов с механизмом разделения памяти (a) Можно ли запустить одновременно два сервера, которые используют механизм разделения памяти? Почему да или почему нет? Попытайтесь это сделать. (b) Модифицируете в сервере текст wait_andJock, чтобы сервер ждал, пока счетчик серверов не стал бы равным нулю. 15.10 Семафоры выполняют функции блокировок файлов. Мы использовали механизм блокирования файлов, чтобы в файловой версии сервера правильно разделять файл. Перепишите эту программу и используйте вместо механизма блокировок файлов меха низм семафоров. 15.11 Механизм файловых блокировок выполняет функции семафоров. Мы использовали семафоры в версии сервера для защиты данных в разделяемой памяти. Перепишите эту программу, где вместо семафоров нужно использовать механизм блокировок фай лов. Вам понадобится для этого файл. 15.12 Слишком много читателей. Решение на основе использования семафоров в сервере, где использован механизм разделяемой памяти, не будет обеспечивать правильный по каз времени, если такое решение будет использованы достаточно большим количест вом клиентов. Рассмотрим ситуацию: читатель А инкрементировал на 1 счетчик чита телей. Далее читатель В инкрементировал счетчик читателей до 2. После чего за кончил работу читатель А и уменьшил счетчик читателей на 1, но читатель С опять увеличил его до 2. Затем закончил работу читатель В, но опять стартовал читатель А, а читатель С закончился. После чего опять стартовал читатель В и т. д. Все время к раз деляемой памяти обращались на чтение. Объясните, почему это не даст возможность писателю записывать в память новое значение времени. Модифицируйте систему так, чтобы писатель мог бы предотвратить блокирование новыми читателями сегмента раз деляемой памяти. 15.13 Напишите специальную версию команды ср для печати, которая использовала бы блокировку, чтобы предотвратить одновременный доступ к выходному файлу. Исполь зуйте ее на вашей машине, чтобы распечатать два файла при таком обращении: printcp file1/dev/lp1 & printcp file2 /dev/lp1 &
Предметный указатель $$ 281 $? 355 SHOME 320 . 135 .. 135 /32 /dev 166 /dev/null 195 /dev/tty 45-46,91,130, 382,526,533 /dev/zero 195 /etc/group 114 /etc/passwd 112-113 /etc/services 402 _exit системный вызов 312-313 <ermo.h>,заголовочный файл 88 <signal.h> заголовочный файл 217 <sys/stat.h> заголовочный файл 116,121 > 357-358 » 381
А accept системный вызов 404,406 AFJNET 409,414 AFUNIX 418 aio_read системный вызов 275-276 aio_retum системный вызов 275 aiocb структура 275 alarm системный вызов 238-243,471 Algol 60 31 API 396 AT&T 50 atexit библиотечная функция 312 atm.sh программа 204
В be команда 38-41, 386 be команда, разработка 388 Bell Laboratories 50 biff команда 193 bind системный вызов 403-405,413-414,422 bounce_aio.c программа 275 bounce_async.c программа 272 bounce Id.c программа 262 bounce2d.c программа, 2D анимация 266
Bourne Shell 320 BRKINT 188 BSD 50 builtin.C программа 340
С c_cc 184,213 c_iflag, c_oflag, c__cflag, cjflag 184 cat команда 34 cat команда в отношении /dev/mouse 193 cat команда для мыши 193 cd команда 33, 134, 152 cfgetospeed библиотечная функция 188 changeenv.c программа 344 chdir системный вызов 152, 355 chmod команда 123, 136, 319 chmod системный вызов 124 chown системный вызов 123 close и сокеты 407 close системный вызов 64 closedir библиотечная функция 100,101 close-then-open (закрыть, а затем открыть) 363 стр команда 76 сотт команда 358 connect системный вызов 386,408-410, 412,423 connect_to_server функция 423, 425 cooked режим 203 ср команда 35, 73-76 cpl.c программа 75 creat системный вызов 73,177 crmode режим 203 ctime библиотечная функция 69, 427 Ctrl-C 200,214, 250,310 curses 231-238, 278 curses и нити 520-533
D dc команда 40-41У 387 dc команда, как сопрограмма 391 defunct, метка окончания процесса 313 dgram.c программа 455 dgrecv.c программа 453 dgrecv2.c программа 458
565
Предметный указатель
dgsend.c программа 454 diff команда 159, 329 dirent структура 100,101 du команда 136 dup системный вызов 366-369 dup2 системный вызов 367, 379, 395
Е EACCESS 89 ECHO 186-187, 189, 209 echo команда 339 echo, опция stty 181 ECHOE 189 ECHOK 189 echostate.c программа 186 EINTR 88, 240 ENOENT 88 env команда 344 environ, глобальная переменная 346-349,557 EPERM 143 EPIPE 380 erase, ключ стирания 182, 189, 203,224, 235 errno, переменная 88,89, 90 exec и файловые дескрипторы 368 ехес и pipe, системные вызовы 378 ехес и нити 504 ехес 1 .с программа 292 execl библиотечная функция 315, 396 execlp библиотечная функция 314 execute.c программа 324 execv библиотечная функция 314, 558 execve системный вызов 345 execvp библиотечная функция 292-296, 300,308 exit библиотечная функция 303, 304, 308, 309,315,550 exit и нити 504 exit команда 329 export команда 348 export функция 350
F FJ3ETFL 174 F SETLKW 544
FJJNLCK, FJtDLCK, F_WRLCK 544, 545 fcntl и блокировки файлов 543-546 fcntl системный вызов 174,210,212,274 fdopen библиотечная функция 390,392, 394-396,414,434 FIFO и системный вызов open 538 FIFO и системный вызов read 538 FIFO и системный вызов unlink 538 FIFO и системный вызов write 538 FIFO, именованные программные каналы 537-539, 560 FIFO, создание 538 file_ts.c программа 544 fileinfo.c программа 107 filesize.c программа 106 find команда 137, 328 flock структура 545 fork и pipe 375-378 fork и нити 504 fork и серверы 427 fork и файловые дескрипторы 316, 369 fork системный вызов 297-301, 308,309, 311,314,330, 376,389, 397, 556 fork, возвращаемое значение 300 forkdemol.с программа 297 forkdemo2.c программа 298 forkdemo3.c программа 299 fstat системный вызов 383
G get_ticket функция 463 getdents системный вызов 101, 129 getenv библиотечная функция 344 getgrgid библиотечная функция 114, 118 gethostbyname библиотечная функция 405 gethostname системный вызов 404,408 getitimer системный вызов 243,246 getpid системный вызов 297, 545, 546 getpwuid библиотечная функция 113,114 GID 114 GNU/Linux 51 grep команда 61, 68, 100, 105, 199,210,290, 309, 319, 328, 329, 334, 335, 352
566
Н hello_multi.c программа 491 hello__single.c программа 489 hellol.с программа 232 hello2.c программа 233 hello3.c программа 235 hello4.c программа 235 hello5.c программа 236 НОМЕ переменная 321 HTTP, протокол 41,433-436 I ICANON 187-188, 207 icanon, опция stty 202 icrnl, опция stty 181 if-then-else команда 321, 328-331 IGNBRK 188 IGNCR 188 IGNPAR 188 incprint.c программа 494 init программа 313 INLCR 188 inode, файл устройства 170 INPCK 188 Internet адрес 453 intr, опция stty 181 iocti системный вызов 190 IPC 29 IPC, доступ 543 IPC, именованные каналы FIFO 536-538 IPC, именованный программный канал 536-538 IPC, область передачи 543 IPC, программный канал 375-378 IPC, разделяемая память 539-541, 546-553 IPC, сигналы 261 IPC, файлы 535-536 IPC: сравнение методов 535-542 IPCCREAT 540, 548 isatty библиотечная функция 383 ISIG 188,215 ISTRIP 188 ITIMERJPROF 242, 245-246 ITIMERJREAL 242, 245-246
Предметный указателе
ITIMERJVIRTUAL 242, 245-246 itimerval структура 243, 244,246 IXOFF 188 IXON 188
К Kernighan, Brian 229 kill команда 261, 305, 408,477,486, 553 kill системный вызов 261-262,472, 557
L lclnt_funcsl.c программа461 Iclntl.с программа461 less команда 34 link системный вызов 150 listargs.c программа 361 listchars.с программа 179 listen системный вызов 386,403,406,406, 414,415,423 In команда 135, 150, 159-160 logfilec.c программа 482 logfiled.c программа 481 login команда 30, 130 login, наблюдение за пользователями 357 logout 86-87 logout команда 30 logout, запись 87 logout_tty.c программа 87 lpd программа 554 lpr команда 35, 120, 556 Is команда 33, 97-99, 137 Is команда, удаленная 413-417 Is удаленная команда 411-415 lsl.c программа 102 ls2.c программа 115 lseek системный вызов 88,90 lseek системный вызов, присоединение • данных 176 Iservl .с программа 465 lsrvjiincsl.c программа 465 Istat системный вызов 160
М main, аргументы функции 294 make__server_socket функция 424,425
Предметный указатель
man команда 56 mkdir команда 35, 137, 149, 164 mkdir системный вызов 133 mkfifo библиотечная функция 538 mkfifo команда 537, 538 mmap системный вызов 562
more команда 34 moreOI .с программа 42 more02.c программа 46 mount команда 158 mutex - mutual exclusion lock (замок взаимного исключения) 498-501, 507, 508 mutex, динамическая инициализация 523 mv команда.35, 135, 150, 152
N newline, символ 179-180 noecho режим, установка 209 О 0_APPEND 176, 177, 483 0_ASYNC 272, 274 0_CREAT 177 0_EXCL 177 OJNDELAY 210, 212, 214 0_N0NBL0CK 210 OJRDONLY 365, 366, 367 О_RDONLY, 0_WRONLY, 0_RDWR 63 0_SYNC 174, 177 OJTRUNC 177 on_exit библиотечная функция 313 onlcr, опция stty 181 open .. dup2 .. close 369 open системный вызов 62, 63, 171, 173, 177, 178, 192,374,383 open., close., dup.. close 365 opendir библиотечная функция 62, 101
P PARMRK 188 passwd команда 120 passwd структура 113 passwd файл 112-113 pause системный вызов 238-241 pclose библиотечная функция 393, 394 perror библиотечная функция 90
567
PFJLOCAL 482 PFJJNIX 482 pg команда 34 PID 299, 300,479 pipe и exec 379 pipe и fork 377-380 pipe системный вызов 374,375,388,389,557 pipe.c программа 378 pipedemo.c программа 374 pipedemo2.c программа 376 play_again0.c программа 205 play_againl.c программа 206 play__again3.c программа 210 poll системный вызов 535, 561 рореп библиотечная функция 394-395 рореп библиотечная функция, риск использования 417, 418 рореп.с программа 395 popendemo.c программа 393 PPID 286 ps команда 285-287 pshl .с программа 294 psh2.c программа 307 pthread_attr_init библиотечная функция 512 pthread_attr_setdetached библиотечная функция 512 PTHREAD_CONDJNITIALIZER 508 pthread_cond_signal библиотечная функция 508,510 pthread__cond_t тип данных 508 pthread_cond_wait библиотечная функция 507, 508-510 pthread_create библиотечная функция 492, 493,495,497, 500, 501,508 PTHREAD_CREATEJDETACHED 512 pthreadjoin библиотечная функция 491,492,493 pthreadjnutexjnit библиотечная функция 523 PTHREAD_MUTEX_INITIALIZER 499,509 pthread_mutex_Jock библиотечная функция 499,500, 509 pthread_mutex_t тип данных 498 pthreadjmitex_unlock библиотечная функция 499,500,507 pwd команда 33, 1343 pwd команда, алгоритм 153-154 pwd команда, создание 152-156
568
Q quit сигнал 216 quit, опция stty 181 R raw режим 203 read и программные каналы 379 read команда 321, 328, 336 read системный вызов 61, 64, 375, 386 readdir библиотечная функция 100,101,102 readlink системный вызов 160 recvfrom системный вызов 452,453,457 release_ticket функция 461,463, 464,465 rename системный вызов 127, 150-152 Ritchie, Dennis 228, 561 rls.c программа 412 rlsd.c программа 413 rm команда 35, 149 rmdir команда 33, 133, 149 rmdir системный вызов 149 rotate.c программа 200 RPN41
S S_ISDIR макрос 111 SA_NODEFER 256 SA_RESETHAND 256 SA JtESTART 256 SA SIGINFO 256 sanitize функция 415-416 script2 программа 320 SEEK_CUR 86 SEEKJEND 86 SEEK_SET 86 select системный вызов 213, 532-534 selectdemo.c программа 532 sem_op 552 sembuf структура 549 semctl системный вызов 549, 550 semget системный вызов 548, 550 semop системный вызов 550 send системный вызов 485 sendmsg системный вызов 485 sendto системный вызов 457,483,485 set group id бит 121
Предметный указать
set user id бит 120-121 set команда 337 setecho.c программа 186 setitimer системный вызов 243,246 SGID 121 shell 31, 229 shell скрипт, исполнение 319 shell, встроенные команды 339-342 shell, исполнение программ 306 shell, основной цикл 290 , 323 shell, основные функции 289-290 shell, переменные 289-290,337-340 shell, поток управления 328-331 shell, пример small-shell 323-328 shell, программирование 289-290 shell, простой пример 294 shell, разработка 307 shell, скрипт 137,204, 319-321, 331, 343, 355,354-355, 536, 538 shell, цикл while 355,357 shinjc.c программа 541 shm_tc2.c программа 551 shmj;s.c программа 540 shm_ts2.c программа 548 shmat системный вызов 540 shmget системный вызов 540 showenv.c программа 344 showtty.c программа 187 SIG_BLOCK 259 SIGJDFL217-218 SIGJGN 217-218 SIG_SET 259 SIGJUNBLOCK 259 sigactdemo.c программа 256 sigaction системный вызов 255-58 sigaction структура 256 sigaddset библиотечная функция 259 SIGALRM 238-239, 472 SIGCHLD 313, 317,428-430 sigdelset библиотечная функция 260 sigdemo 1 .с программа 218 sigdemo2.c программа 220 sigemptyset библиотечная функция 259 sigfillset библиотечная функция 259
Предметный указатель
SIGINT 214, 216, 217,218 SIGIO 272, 273-277 SIGKILL 216, 281 signal системный вызов 217-218,248, 323, 548, 557 SIGPIPE 380 sigprocmask системный вызов 259 SIGQUIT 216 SIGSEGV 216 SIGUSR1 262 SIGUSR2 262 SIGWINCH 227 sleep библиотечная функция 218,219,220, 235-238 sleep 1 .с программа 239 smshl.с программа 321 smsh2.c программа 331 SOCK__STREAM 403, 405, 408, 412 sockaddr_in структура 453 socket системный вызов 386,403-405,412, 422,423 socketpair системный вызов 418 socklen_t тип данных 459 socklib.c программа 424 sort команда, как сопрограмма 391 space travel 229 splitline.c 325 spwd.c program 154 st_mode, файл устройства 171 stat системный вызов 106 stat структура 105, 106, 107 stdinredirl.c программа 364 stdinredir2.c программа 366 sticky bit, разряд 108, 121 stty команда 165, 166, 181-182, 202 stty, разработка 187 SUID 120, 121 symlink системный вызов 160 System V 50
T talk команда 529 tanimate.c программа 520 tbounceld.c, программа 518
SSL
tcgetattr системный вызов 183 TCP 451 TCSANOW, TCSADRAIN, TCSAFLUSH 183,184
tcsetattr системный вызов 184 tee команда 383 termios структура 183 test программа 354 Thompson, Ken 228, 560 time системный вызов 404 time_t тип данных 69 timeclnt.c программа 408 timeserv.c программа 403 timeval структура 245, 246 tinybc.c программа 389 TIOCGWINSZ 190 touch команда 127 tty драйвер 181-190 tty команда 168 twebserv.c программа 512 twordcountl.с программа 496 twordcount2.c программа 498
U UCB 50 UDP 451 UID 112,113 umask системный вызов 124 Unix сокеты доменов 480-483 Unix, история 50 unlink системный вызов 150, 193 usleep библиотечная функция 241 utime системный вызов 126 utmp структура 58 utmp файл 57-60, 84, 357 utmp функции буферизации 79 utmplib.c программа 81
V varlib.c программа 348 VERASE 185, 188 VINTR215-216 VKILL 188 VMIN 207 VTIME 213, 225
570
W wait системный вызов 301-309, 312,321, 322,324, 557 waitdemol.c программа 301 waitdemo2.c программа 303 waitpid системный вызов 313, 429 watch.sh программа 359 Web броузер 41 Web сервер 41 Web сервер, алгоритм 434 Web сервер, нити 510-516 Web сервер, протокол 431-432 Web сервер, разработка 430-440 webserv.c программа 436 while цикл 357 who команда 52-56 who 1 .с программа 65 who2.c программа 67 whotofile.c программа 371 winsize структура 190 WNOHANG 429 write и программные каналы 379-380 write и сокеты 407 write команда 169 write системный вызов 74, 377, 386,485 writeO.c программа 170 wtmp файл 175
Z zero, устройство 195
А
Предметный указатель
анимация, управляемая 263-277 аргументы для main 291 асинхронные сигналы 215 асинхронный ввод 271-277 атомарная операция 176, 190, 547
Б билет 448 билеты, восстановление 470-472 билеты, легализация 474-476 блок двойной косвенной адресации 144 блокирование сигналов 252,255,258 блокировка по записи 543 блокировка по чтению 543 блокировки файлов 543-546, 559 блокировки файлов 543-546, 559 блокировки файлов, запись 543 блокировки файлов, использование fcntl 544 блокировки файлов, использование link. 195 блокировки файлов, использование ссылок 195 блокировки файлов, чтение 543 блокируемый ввод 209 буфер экрана 234 буферирование 77-85, 93 буферирование в ядре 82, 171 буферирование в ядре, управление 172 буферирование, терминал 199-201, 213 буферирование, ядро 83
В
ввод/вывод 28 взаимодействие нитей 494-503 аварии, на стороне клиента 470 взаимодействие нитей 494-503 аварии, на стороне сервера 473 возврат каретки, символ 179 автомат состояний 330,331 восстановление билетов 471-473 адрес 400 время доступа к файлу 106, 126 адрес Unix 481-484 время модификации файла 36, 96, 126-127, адрес вызывателя 407 197,280 адрес вызывателя 407. время последней модификации адрес для сокета 405 файла 96, 196, 280 адрес, Интернет 454 время, представление 69 анимация и нити 516-526 анимация на основе использования нитей 517 встроенные команды 339-342 вывод значений установок драйвера 185 анимация, 2D 268-273 выполнение программы 292 анимация, множественная 519
57)
Предметный указатель
группа пользователей при управлении доступом к файлам 105 группа цилиндров 145 групповой идентификатор GID 107, ИЗ групповой идентификатор, имя группы 115 групповой идентификатор, назначение GID 114 групповой идентификатор, эффективный 120 группы 115
запуск программ на исполнение 289-296 значение кода возврата, передаваемое родительскому процессу 305 зомби 313, 427-429
И
игнорирование сигнала 217,220,249,250, 254, 258, 278, 323,324 идентификатор пользователя 108, 112 идентификатор пользователя и пользовательское имя 112-113 идентификатор пользователя, д эффективный 121 дамп образа процесса 304 имена файлов 149 данные, блоки данных 139, 140 имена файлов с начальной точкой 96 данные, искажение данных 257 имена файлов, длина 34 данные, область данных 138 именованный программный данные, реассемблирование данных 452 канал 537-539, 559 данные, фрагментация данных 451 имя “точка” 98 двунаправленные коммуникации 387 имя хоста 400 двунаправленный программный канал 418 имя”точка_точка” 98 дейтаграммы, ответ 458 индексные дескрипторы 139-142 дейтаграммы, посылка 454 инструментальное средство, дейтаграммы, прием 453 программное 199 демон 416 инструментальные программные дисковые блоки 138 средства 198, 359 дисковые соединения, атрибуты 172-177 интервальный таймер 241-248, 304 . < дисковый блок 138 интерфейс автомата для получения длина имени файла 34 напитка 385, 386 доступ 88 интерфейс, данные 386 дочерний процесс 296-306, 322, 336, 341, информация о файле, отображение 346, 394, 427, 433-434 информации 115 дочерний процесс и программный канал 375,377 искажение данных 257 дочерний процесс и файловые исполнимый файл 320 дескрипторы 368-371 исполняемая программа 283 дочерний процесс, ожидание окончания 300-306 драйвер терминала 180-190 история, Unix 50 драйвер устройства 170 драйвер устройства, изменение установок 172-173 канонический режим 202 драйвер, tty 181-190 каталоги 32 драйвер, терминал 180-190 каталоги, действия над каталогами, устройство 145-149 каталоги, дерево каталогов 32, 97, 134 задержка, программирование 237 каталоги, закрытие 101 записи о входах в сессии 56-60, 175 каталоги, объединение деревьев запись на терминалы 170 каталогов 156
572
каталоги, определение 98 каталоги, открытие 100 каталоги, переименование 135 каталоги, перемещение по дереву 134 каталоги, проверка свойств 110 каталоги, путь к каталогу 134 каталоги, смена каталога 33 каталоги, создание 33, 134 каталоги, стандартная система 53 каталоги, структура 141 каталоги, удаление 33, 135 каталоги, чтение 97-100 каталоги, чтение записей 101,129 клавиши инициализации функций редактирования 224 клиент 385, 387 клиент, аварии на стороне клиента 470 клиент, использование разделяемой памяти 54J8 клиент, типичный 425 клиент, установка 399-400 клиент/сервер 384-386,400 клиент/сервер, взаимодействие 425 клиент/сервер, установка 424 ключ kill 200,203,226 ключи для удаления символов 200,225 ключи управления процессом 224 ключи, для редактирования 224 ключи, для управления процессом 224 код возврата в скриптах 303, 304 коды символов 179 копирование файла 73, 75 корневая файловая система 157 корневой каталог 32, 135 косвенный блок 144 критическая секция 258
Л легализация, билеты 474-476 лицензионный клиент, версия 1 461 лицензионный сервер 444,447 М маска 122, 123,124, 185, 186 маска на создание файлов 123
Предметный указатель
маскирование 108-110 медленные устройства 279 межпроцессные коммуникации 29 метеорологический сервер 402 младший номер 169 множество процессов 299 мультинитьевая программа 491
Н надежность коммуникаций 451 наименьший по значению доступный файловый дескриптор 362,364,365,373,375 неблокируемый ввод 210 неканонический режим 202-203 неканонический режим, установка 207 ненадежные коммуникации 451 несколько аргументов для нитей 500 нити и curses 523-534 нити и exit 504 нити и fork 504 нити и ехес 504 нити и память 504 нити и процессы 503-505 нити и сигналы 504 нити исполнения 489-490 нити отсоединенные 511 нити, аргументы 500 О обработка символов 48, 179-181,277 обработчик сигналов 218, 261,282 обработчик сигналов 217-219, 281, 317 опции командной строки 36, 55 открытие файла на запись 63 открытие файла на чтение 63 открытие файла на чтение и запись 63 отсоединенные нити 511 отсутствие ожидания 210 . очереди сообщений 560 ошибки системных вызовов 88
П память 288-289 память ядра 287-288 память ядра 288-289
Предметный указатель
переменные состояния 262, 331 переменные, shell 337-340 переменные, разделение между нитями 494 перенаправление ввода 363- 370 перенаправление ввода 363- 368 перенаправление ввода/вывода 289-290,355, 356,372 перенаправление ввода/вывода, принцип 362-3 63
перенаправление вывода 357-358, 369-372 перенаправление вывода 357-358, 368-371 перенаправление, ввод/вывод 372 платы 137-138, 170’ повторно входная функция 260 подкаталоги, структура 148 подстановка переменных 341, 354 поле листинга для указания режима работы с файлом 104, 108-111, получение листинга содержимого каталога 102 пользователи, регистрация входов 357 пользователи, список текущих пользователей 65, 67 пользовательская программа 199 пользовательские имена 112 пользовательский режим 78, 79 пользовательское имя и UID 112 пользовательское пространство 27,49, 284 помывка автомобиля 191 поправка к приоритету 285 порт 400 порты, широко известные 402 порция спагетти 159 потоки 190 права доступа , файл устройства 166, 169 права доступа к файлу 36-37, 73, 106-108, 123, 136, 143, 149, 192 права доступа, файлы 105 прерываемый системный вызов 241 приглашение 31 программа 285 программные лицензии 444 программный канал 374, 373-381, 385-387,558 программный канал двунаправленный 418
573
программный канал, запись данных 375,379 программный канал, разделение 375-378 программный канал, создание 373 программный канал, средство IPC 376-379 программный канал, чтение данных 375,379 программы, запуск на исполнение 289-295 прокси сервер 419 протокол 400, 412, 449,450 протокол лицензионного сервера 460 протокол, лицензионный сервер 460 процесс 284-288 процесс дочерний 296-305,322,337,341,346, 369-372,428 процесс родительский 296-305, 313, 314, 346, 369-372, 389,428, 557 процесс фоновый 316, 328 процесс, идентификатор 285 процесс, окончание 312 процесс, определение существования 472-473 процесс,.приоритет 285 процесс, размер 285 процесс, создание 296-300 процесс, уничтожение 312 процесс, управление 28 процесс, управление через терминал 310 процессор 28,285 процессы в системе 286-287 процессы и аргументы 311 процессы и нити 503-504 процессы и файлы 392 процессы и функции 311 процессы, чтение данных 392, 396 путь, каталоги 134 i ,<
Р рабочий каталог 32-33 раздел диска 138 разделы диска 138 разделяемая память 559 разделяемая память и семафоры 548 разделяемая память, IPC 539-541, 546-554 разделяемая память, time сервер 548 разделяемая память, клиент 551 разделяемые переменные для нитей 494 размер окна 191,226
574
размер файла 105 размер файла 105 размер файла, файл устройства 168 разряды прав доступа, декодирование 111 распределение дисковой памяти 142 распределенные сервера 478-480 реассемблирование, данные 451 регистрация окончания сессии 87 редактирование при работе драйвера терминала 200-^202 режим auto-append 174-176, 381 режим эхоотображения 172 режим ядра 90 режим, raw 203 режим, й отношении файла 73, 104 режим, канонический 202 режим, неканонический 202 режимы работы драйвера терминала 200-201 режимы работы терминала 202-205 рекурсивные сигналы 252,255 родительский каталог 135 родительский каталог, структура 148 родительский процесс 285, 295-305, 312, 313, 345, 388, 394, 427, 428, 557 родительский процесс и файловые дескрипторы 369-372 родительский процесс и программный канал 374, 378
С свойства файла, отображение свойств 106 свойства файла, установка свойств 123-126 свойства файла, чтение свойств 105-107 свопинг 121 связи между устройствами 157 связи между устройствами 158 сектор диска 138 сектор диска 138 семафоры 545-553, 560 семафоры и разделяемая память 548 семафоры, действия над семаформами 546 семафоры, операции 553 семафоры, установки 547 семейство адресов 400 сервер Web, протокол 431,432
Предметный указатель
сервер, Web 41,430-432 сервер, аварийные ситуации 473 сервер, использующий разделяемую память 548 сервер, общий 426 сервер, прокси 419 сервер, разработка 426-430 сервер, установка 397-398 сервер, установка сокета 422-423 сервера, распределенные 478-480 сервис 385 сети 29 сигнал прерывания 217 сигнал, игнорирование 217,220,248,251,322,323 сигналы 215-223, 304, 310 сигналы и нити 504 сигналы, IPC 262 сигналы, блокирование 252, 256,259 сигналы, имена 216 сигналы, множество 249-252,429 сигналы, надежные 254-257 сигналы, ненадежные 249-252 сигналы, посылка 261 сигналы, потеря 429 сигналы, при вводе 271-277 сигналы, реакция 217 сигналы, рекурсии 252, 256 сигналы, терминал 215, 310 сигнальные наборы 260 символическая ссылка 159-161 синхронные сигналы 215 системное пространство 25, 49 системные вызовы, ошибки 88 системные вызовы, прерываемые 241 системные вызовы, сокращение 78 системные сервисы 26-27 скорость передачи данных 171 скрипт, shell 137,204, 319-321, 354, 357-358, 536 скрытый файл 97 служба времени 397 собственник файла 105,106, 121-123,125— 126,143,169 создание процесса 296-300 создание файла 73, 177
Предметный указатель
сокет, установка сокета на сервере 422-423 сокеты 386, 397-410,559 сокеты дейтаграмм 450-456 сокеты потока 405,450 сокеты, Unix сокеты доменов 481-484 сопрограмма 388, 391 список текущих пользователей 65, 70 список файлов и информация 116 справочник, поиск 57 спулер печати 554-556 среда 318, 322, 343-354 среда и дочерние процессы 346 среда и системный вызов ехес 347-349 среда, изменения среды 346, 348-349 ссылки 106, 124, 141-142 ссылки, использование для блокировок 195 ссылки, символические 159, 160 ссылки, твердые 150, 160 стандартные файловые дескрипторы 359 стандартный ввод 45, 199, 356, 359 стандартный вывод 198, 199, 359 стандартный вывод сообщений об ошибках 198, 359 старший номер 169, 171 статус файла 105 статус, файл 104 страница документации 55 страницы памяти 122, 288-289 страницы, память 288-289 суперблок 139, 145, 160 супервизорный режим 79 Супермен lb счетчик ссылок 104, 148
Т таблица inode 139, 141 таймаут 198,200,207,213,225,281,532-534 таймаут на входе 200, 209,213, 225,281, 532-534 таймаут, без задержек 210 таймер, интервальный 241-248, 304 таймер, программирование 243-248 таймеры 28 твердая ссылка 148, 160, 161 текущий каталог 152
575
текущий указатель в файле 86-87 терминал, буферирование 200-202,213 терминал, получение атрибутов 182 терминал, редактирование 200-202 терминал, установка атрибутов 182 терминал, эхоотображение 200-202 терминалы, как файлы 166-}68 терминалы, разработка программы write 169 терминалы, режимы 200-203,224 терминалы, сигналы 214, 310 терминальные соединения, атрибуты 178-190 тип файла 106, 108 тип файла, установка 123 тип, файл 106 точка монтирования 157, 163 транкатенация файла 73 трансплантация мозга 293 трансплантация, мозг 293, 345 трек диска 138 трек диска 138 тройной косвенный блок 145
У уведомление для нитей 505-510 уведомление для нитей 505-510 удаление мозга 293 управление ненадежными сигналами 249 управление процессами через терминал 310 управление событиями 263 управляющие символы 181-185 условие гонок 174-177,497, 542 условная переменная 507 успех 329 установка текущей позиции в файле 86-87 установки драйвера терминала, изменения 187 установки драйвера терминала, просмотр 186 устройства 28,49 устройства, отличия от файлов 171-172 устройства, права доступа к устройствам 168 устройства, схожесть с файлами 166-171 устройства, управление 187, 198,221 устройство /dev/null 195 устройство для свопинга 121
576
Предметный указатель
файловые дескрипторы и системный вызов ехес 370 файл устройства 166, 386 файловые дескрипторы и системный вызов файл устройства, st_mode 171 файл устройства, индексный дескриптор inode fork 170 316,369 файловые дескрипторы, множественное файл устройства, права доступа 168 чтение 530-534 файл устройства, размер файла 168 файл, блочное использование дисковой памятифайловые 140 дескрипторы, стандартное файл, буферирование при чтении 77 использование 360 файл, время последнего доступа 106, 126 файловый дескриптор 61, 62, 63,363 файл, время последней модификации 36,126-127 файловый дескриптор и системный файл, группа 125-126 вызов socket 409 файл, запись в файл 63, 73 файлы, IPC 536-537 файл, копирование 73, 75 файлы, получение листингов со свойствами 32,96 файл, перезапись 85 файлы, сравнение 76 файл, переименование 35, 135, 150-152 фоновый процесс 328 файл, перемещение 135 фрагментация, данные 451 файл, печать содержимого 35 фрагментация, файл 121,145 файл, поблочное чтение 143 Ч файл, просмотр 34 чтение каталогов 98-101 файл, режим присоединения данных 174-176 чтение многих входов 530-534 файл, скрытый 97 файл, собственник 105,106, 121, 122, 123, Ш 125, 126., 143, 169 широко известные порты 402 файл, создание 73, 140, 177 файл, текущий указатель 85-86, 316 Э файл, транкатенация 73, 177 экран, буфер экрана 234 файл, удаление 35 экран, виртуальный 234 файл, устройство 167,386 экран, размер 190 файл, чтение и запись 64 экран, управление 231-238 файл, чтение содержимого 62, 63, 64 эпоха 69 файловая система 161 эффективный идентификатор группы файловая система, варианты 145 (EGID) 121 файловая система, корневая файловая эффективный идентификатор пользователя система 157 (EUID) 120,125 файловая система, представления с позиций Я пользователя 134-137 файловая система, структура 137-141 ядро 28 файловые дескрипторы и программные каналы 374
Ф