Книга
Книга
СолитерСолитер
Солитер
Солитер
А. В. Гордеев А. Ю. Молчанов
СИСТЕМНОЕ ПРОГРАММНОЕОБЕСПЕЧЕНИЕ
«®й&«®й&• теоретический ма1ериал в сочетании с примерамии упражнениями• теоретический ма1ериал в сочетании с примерамии упражнениями
«®й&
«®й&
• теоретический ма1ериал в сочетании с примерами
и упражнениями
• теоретический ма1ериал в сочетании с примерами
и упражнениями
о для студентов и преподавателей высших учебных заведений направления «Информатика и вычислительная техника*
УЧЕБНИК / ДЛЯ ВУЗ О В
А. В. Гордеев
А. Ю. Молчанов
СИСТЕМНОЕ ПРОГРАММНОЕОБЕСПЕЧЕНИЕ
Допущено Министерством образования Российской Федерациив качестве учебника для студентов высших учебных заведений,обучающихся по специальностям «Вычислительные машины, комплексы,системы и сети» и «Автоматизированные системы обработкиинформации и управления» направления подготовки дипломированныхспециалистов «Информатика и вычислительная техника»
£ ПИТЕР
Москва • Санкт Петербург Нижний Новгород ВоронежРостов на Дону Екатеринбург СамараКиев Харьков ■ Минск
2003
ББК32.973-018я7
УДК 681.3.06(075)
Г68
Рецензенты
Еркович С. П„ доктор физико-математических наук, Московский государственныйтехнический Университет (МГТУ) им. Баумана, профессор
Ревунков Г И., кандидат технических наук, Московский государственныйтехнический Университет (МГТУ) им Баумана доцент
Г68 Системное программное обеспечение / А. В. Гордеев, А. Ю. Молчанов. — СПб.: Питер, 2003 — 736 с.: ил.
ISBN 5-272-00341-1
Учебник создам в соответствии с Государственным образовательным стандартом в дисциплине «Системное программное обеспечение» Его основой стал учебный материал. в течение нескольких лет преподаваемый студентам Санкт-Пстербуртского государственного университета аэрокосмического приборостроснпя. Помимо общетеоретических вопросов описаны конкретные реализации различных системных программ. поэтому учебник может быть полезен не только студен гам, детально изучающим системное программное обеспечение, по и тем, кто собирается само- стоягелыю разработать отдельные компоненты тш создать комплекс параллельно исполняющихся взаимодействующих программ.
Допущено Мшшстерсгвом образования Российской Федерации в качестве учебника для сту- лепгов высших учебных заведений, обучающихся по специальностям «Вычислительные машины, комплексы, системы и сети» и «Автоматизированные системы обработки информации и управче- штя» направления подготовки дипломированных специалистов «Информатика и вычислительная техника».
ББК 32.973-018я7
УДК 681.3.06(075)
ISBN 5-272-00341-1ISBN 5-272-00341-1© Издательский дом «Питер», 2003
ISBN 5-272-00341-1
ISBN 5-272-00341-1
Краткое содержание
Часть I. Операционные системы и среды
Глава 1. Основные понятия22
Глава 2. Управление задачами и памятью в операционных системах. . 51
Глава 3. Особенности архитектуры микропроцессоров i80x86 . .. . . . 100
Глава 4. Управление вводом/выводом и файловые системы . .129
Глава 5. Архитектура операционных систем и интерфейсы прикладного программирования191
Глава 6. Проектирование параллельных взаимодействующих вычислительных процессов 221
Глава 7. Проблема тупиков и методы борьбы с ними269
Глава 8. Современные операционные системы301
Часть II. Трансляторы формальные языки и грамматики
Глава 9. Формальные языки и грамматики 347
Глава 10. Регулярные языки387
Глава 11. Контекстно-свободные языки418
Глава 12 Классы КС языков и грамматик . . . ...463
Глава 13. Основные принципы построения трансляторов529
Глава 14. Генерация и оптимизация кода 588
Глава 15. Современные системы программирования 655
Приложение А Тексты про раммы параллельных взаимодействующих задач700
Приложение Б. Тексты программ комплекса параллельных взаимодействующих приложений. . . . 709
Список литературы . . . . . .,719
Алфавитный указатель725
Содержание
Предисловие 13
От издательства.16
Часть I. Операционные системы и среды
Глава 1. Основные понятия . . .22
Понятие операционной среды . 22
Понятия вычислительного процесса и ресурса 24
Диаграмма состояний процесса ,. 28
Реализация понятия последовательного процесса вОС . 31
Процессы и треды33
Прерывания 37
Основные виды ресурсов 44
Классификация операционных систем ,48
Контрольные вопросы и задачи.,49
Вопросы для проверки 49
Глава 2. Управление задачами и памятью в операционных системах. , 51
Планирование и диспетчеризация процессов и задач.53
Стратегии планирования . , . 53
Дисциплины диспетчеризации54
Качество диспетчеризации и гарантии обслуживания60
Диспетчеризация задач с использованием динамических приоритетов. ... 62
Память и отображения, виртуальное адресное пространство67
Простое непрерывное распределение и распределение с перекрытием (оверлейные структуры) . , .... 70
Распределение статическими и динамическими разделами72
Разделы с фиксированными границами 73
Разделы с подвижными границами 75
Сегментная страничная и сегментно-страничная организация памяти76
Сегментный способ организации виртуальной памяти77
Страничный способ организации виртуальной памяти82
Сегментно-страничный способ организации виртуальной памяти86
Распределение оперативной памяти в современных ОС для ПК... 88
Распределение оперативной памяти в MS-DOS89
Распределение оперативной памяти в Microsoft Windows 95/98.. 92
Распределение оперативной памяти в Microsoft Windows NT95
Контрольные вопросы и задачи, 99
Вопросы для проверки 99
Глава 3. Особенности архитектуры микропроцессоров i80x86 100
Реальный и защищенный режимы работы процессора ,, 100
Новые системные регистры микропроцессоров i80x86 .. .102
Адресация в 32-разрядных микропроцессорах i80x86 при работе
в защищенном режиме 104
Поддержка сегментного способа организации виртуальной памяти. . . . . 104
Поддержка страничного способа организации виртуальнойпамяти108
Режим виртуальных машин для исполнения приложений
реального режима 111
Защита адресного пространства задач112
Уровни привилегий для защиты адресного пространства задач 113
Механизм шлюзов для передачи управления на сегменты кода
с другими уровнями привилегий 115
Система прерываний 32-разрядных микропроцессоров 180x86 . 119
Работа системы прерываний в реальном режиме работы процессора 119
Работа системы прерываний в защищенном режиме работы процессора. . 123
Контрольные вопросы и задачи127
Вопросы для проверки 127
Глава 4. Управление вводом/выводом и файловые системы129
Основные понятия и концепции организации ввода/вывода в ОС130
Режимы управления вводом/выводом 133
Закрепление устройств, общие устройства ввода/вывода 135
Основные системные таблицы ввода/вывода 136
Синхронный и асинхронный ввод/вывод .. .. . . .140
Кэширование операций ввода/вывода при работе с накопителями
на магнитных дисках .. .. . . .. .. .. . . 142
Функции файловой системы ОС и иерархия данных. .. 146
Структура магнитного диска (разбиение дисков на разделы). .147
Файловая система FAT156
Таблица размещения файлов 156
Структура загрузочной записи DOS 160
Файловые системы VFAT и FAT32162
Файловая система HPFS 167
Файловая система NTFS (New Technology File System)178
Основные возможности файловой системы NTFS 178
Структура тома с файловой системой NTFS 180
Возможности файловой системы NTFS по ограничению доступа
к файлам и каталогам .185
Основные отличия FAT и NTFS187
Контрольные вопросы и задачи . . . . 188
Вопросы для проверки ................188
Задания189
Глава 5. Архитектура операционных систем и интерфейсы прикладного программирования-191
Основные принципы построения операционных систем191
Принцип модульности ... . . .191
Принцип функциональной избирательности ....192
Принцип генерируемости ОС ....192
Принцип функциональной избыточности193
Принцип виртуализации 193
Принцип независимости программ от внешних устройств195
Принцип совместимости . 195
Принцип открытой и наращиваемой ОС196
Принцип мобильности (переносимости) .. .... 197
Принцип обеспечения безопасности вычислений197
Микроядерные операционные системы . . 199
Монолитные операционные системы . . .... ....... . . . . 201
Требования, предъявляемые к ОС реального времени202
Мультипрограммность и многозадачность 203
Приоритеты задач (потоков) 203
Наследование приоритетов ... 204
Синхронизация процессов и задач. . . . . . 204
Предсказуемость205
Принципы построения интерфейсов операционных систем . . .205
Интерфейс прикладного программирования207
Реализация функций API на уровне ОС 208
Реализация функций API на уровне системы программирования209
Реализация функций API с помощью внешних библиотек . . . 211
Платформенно-независимый интерфейс POSIX. . . . . 213
Пример программирования в различных API ОС .... 216
Текст программы для Windows (WinAPI) 216
Текст программы для Linux (POSIX API) 218
Контрольные вопросы и задачи 219
Вопросы для проверки 219
Глава 6. Проектирование параллельных взаимодействующих вычислительных процессов221
Независимые и взаимодействующие вычислительнае процессы221
Средства синхронизации и связи при проектировании взаимодействующих вычислительных процессов 227
Использование блокировки памяти при синхронизации параллельных
процессов 227
Синхронизация процессов посредством операции «ПРОВЕРКА И УСТАНОВКА» ...233
Семафорные примитивы Дейкстры236
Использование семафоров при проектировании взаимодейс |ующих вычислительных процессов 242
Мониторы Хоара 250
Почтовые ящики . . . 253
Конвейеры и очереди сообщений . 255
Конвейеры (программные каналы). . . . . . . 255
Очереди сообщений .. .257
Примеры создания параллельных взаим действующих вычислительных процессов . 258
Пример создания многозадачного приложения с помощью системы программирования Borland Delphi,. . , 259
Пример создания комплекса параллельных взаимодействующих программ выступающих как самостоятельные вычислительные процессы262
Контрольные вопросы и задачи267
Вопросы для проверки . 267
Глава 7. Проблема тупиков и методы борьбы с ними . . .. .. 269
Понятие тупиковой ситуации при выполнении параллельных вычислительных процессов.269
Примеры тупиковых ситуаций и причины их возникновения .. . . .271
Пример тупика на ресурсах типа CR . . . •271
Пр мер тупика на ресурсах типа CR и SR .. . .. . . .273
Пример тупика на ресурсах типа SR 274
Формальные модели для изучения проблемы тупиковых ситуации276
Сети Петри . .. . . 276
Вычислительные схемы281
Модель пространства состояний системы 283
Методы борьбы с тупиками 287
Предотвращение тупиков287
Обход тупиков . . . . 288
Обнаружение тупика 291
Контрольные вопросы и задачи300
Вопросы для проверки300
Глава 8. Современные операционные системы301
Семейство операционных систем UNIX301
Общая характеристика семейства операционных систем UNIX особенности архитектуры семейства ОС UNIX301
Основные понятия системы UNIX 303
Функционирование системы UNIX. ,. . . 308
Файловая система 311
Межпроцессные коммуникации в UNIX .... 316
Операционная система Unux .. . 323
Семейство операционных систем OS/2 Warp компании IBM325
Особенности архитектуры и основные возможности OS/2 Warp. 328
Особенности интерфейса OS/2 Warp331
Серверная операционная система OS/2 Warp 4.5 . . ► . 332
Сетевая ОС реального времени QNX 334
Архитектура системы QNX ... . . . 335
Основные механизмы QNX для организации распределенных вычислений339
Контрольные вопросы и задачи 344
Вопросы для проверки 344
Часть II. Трансляторы, формальные языки и грамматики
Глава 9. Формальные языки и грамматики 347
Языки и цепочки символов. Способы задания языков347
Цепочки символов Операции над цепочками символов347
Понятие языка. Формальное определение языка348
Способы задания языков349
Синтаксис и семантика языка 350
Особенности языков программирования 351
Определение грамматики. Форма Бэкуса—Наура353
Понятие о грамматике языка 353
Формальное определение грамматики Форма Бэкуса—Наура354
Принцип рекурсии в правилах грамматики. . . 356
Другие способы задания грамматик357
Классификация языков и грамматик.^360
Классификация грамматик. Четыре типа грамматик по Хомскому361
Классификация языков . 363
Примеры классификации языков и грамматик . . . 365
Цепочки вывода Сентенциальная форма .. . 367
Вывод. Цепочки вывода367
Сентенциальная форма грамматики. Язык, заданный грамматикой369
Левосторонний и правосторонний выводы 370
Дерево вывода. Методы построения дерева вывода370
Проблемы однозначности и эквивалентности грамматик372
Однозначные и неоднозначные грамматики 372
Эквивалентность и преобразование грамматик373
Правила, задающие неоднозначность в грамматиках .. . . 376
Распознаватели. Задача разбора1376
Общая схема распознавателя ....................... 376
Виды распознавателей .. 378
Классификация распознавателей по типам языков380
Задача разбора (постановка задачи)382
Контрольные вопросы и задачи 383
Вопросы383
Задачи.384
Глава 10. Регулярные языки 387
Регулярные языки и грамматики 387
Леволинейные и праволинейные грамматики. Автоматные грамматики . .387
Алгоритм преобразования регулярной грамматики к автоматному виду. . . 388
Пример преобразования регулярной грамматики к автоматному виду . . 390
Конечные автоматы 391
Определение конечного автомата. .391
Детерминированные и недетерминированные конечные автоматы392
Преобразование конечного автомата к детерминированному виду393
Минимизация конечных автоматов .395
Регулярные множества и регулярные выражения397
Определение регулярного множества397
Регулярные выражения. Свойства регулярных выражений ........398
Уравнения с регулярными коэффициентами 399
Способы задания регулярных языков 403
Три способа задания регулярных языков403
Связь регулярных выражений и регулярных грамматик404
Связь регулярных выражений и конечных автоматов407
Связь регулярных грамматик и конечных автоматов408
Пример построения конечного автомата на основе заданной грамматики . 410
Свойства регулярных языков413
Свойства регулярных языков 413
Лемма о разрастании для регулярных языков 414
Контрольные вопросы и задачи 415
Вопросы 415
Задачи . . . . 416
Глава 11. Контекстно-свободные языки . . . 418
Распознаватели КС-языков, Автоматы с магазинной памятью 418
Определение МП-автомата 418
Эквивалентность языков МП-автоматов и КС-грамматик420
Детерминированные МП-автоматы423
Свойства КС-языков .. . 423
Свойства произвольных КС-языков . . . . . 423
Свойства детерминированных КС-языков 424
Лемма о разрастании КС-языков 425
Преобразование КС-грамматик. Приведенные грамматики . ,426
Преобразование грамматик. Цель преобразования426
Приведенные грамматики427
Удаление недостижимых символов427
Удаление бесплодных символов427
Устранение 1-правил429
Устранение цепных правил .. 431
КС-грамматики в нормальной форме . . 433
Грамматики в нормальной форме Хомского433
Устранение левой рекурсии. Грамматики в нормальной форме Грейбах . . 437
Распознаватели КС-языков с возвратом. 441
Принципы работы распознавателей с возвратом441
Нисходящий распознаватель с возвратом .. 443
Распознаватель на основе алгоритма «сдвиг-свертка». . . . . .449
Табличные распознаватели для КС-языков ...... 455
Общие принципы работы табличных распознавателей455
Алгоритм Кока—Янгера—Касами456
Алгоритм Эрли (основные принципы)457
Принципы построения распознавателей КС-языков без возвратов458
Контрольные вопросы и задачи . . . 460
Вопросы460
Задачи461
Глава 12: Классы КС-языков и грамматик . . 463
Нисходящие распознаватели КС-языков без возвратов463
Левосторонний разбор по методу рекурсивного спуска463
Определение Щк)-грамматики 471
Принципы построения распознавателей .для Щк)-грамматик474
Алгоритм разбора для Щ1)-грамматик. . . 475
Восходящие распознаватели КС-языков без возвратов...486
Определение 1П(к)-грамматики 486
Принципы построения распознавателей для кП(к)-грамматик. 489
Грамматики предшествования (основные принципы)497
Грамматики простого предшествования 498
Грамматики операторного предшествования508
Соотношение классов КС-языков и КС-грамматик519
Особенности восходящих и нисходящих распознавателей.519
Отношения между классами КС-грамматик . . . .521
Отношения между классами КС-языков ...... 524
Контрольные вопросы и задачи . . 525
Вопросы 525
Задачи. 527
Глава 13. Основные принципы построения трансляторов529
Трансляторы, компиляторы и интерпретаторы — общая схема работы . .. . 529
Определение транслятора, компилятора, интерпретатора529
Этапы трансляции. Общая схема работы транслятора. . . ■ . 535
Понятие прохода. Многопроходные и однопроходные компиляторы538
Интерпретаторы. Особенности построения интерпретаторов,. 540
Трансляторы с языка ассемблера («ассемблеры»)542
Таблицы идентификаторов. Организация таблиц идентификаторов548
Назначение и особенности построения таблиц идентификаторов548
Простейшие методы построения таблиц идентификаторов550
Построение таблиц идентификаторов по методу бинарного дерева551
Хэш-функции и хэш-адресация554
Комбинированные способы построения таблиц идентификаторов .. .563
^Лексические анализаторы (сканеры) Принципы построения сканеров . . .564
Назначение лексического анализатора . 564
Принципы построения лексических анализаторов . . 568
Построение лексических анализаторов. . 570
Автоматизация построения лексических анализаторов (программа LEX)576
Синтаксические анализаторы. Синтаксически управляемый перевод577
Основные принципы работы синтаксического анализатора577
Дерево разбора. Преобразование дерева разбора в дерево операций . . . 579
Автоматизация построения синтаксических анализаторов (программа YACC)582
Контрольные вопросы и задачи 583
Вопросы 583
Задачи . 585
Глава 14 Генерация и оптимизация кода . . . 588
Семантический анализ и подготовка к генерации кода588
Назначение семантического анализа588
Этапы семантического анализа 589
Идентификация лексических единиц языков программирования . . . .594
Распределение памяти Принципы распределения памяти597
Дисплей памяти процедуры (функции). Стековая организация дисплея памяти . 603
Память для типов данных (RTTI-информация)610
Генерация кода. Методы генерации кода .... 612
Общие принципы генерации кода. Синтаксически управляемый перевод 612
Способы внутреннего представления программ ..615
Обратная польская запись операций.. .619
Схемы СУ-перевода .. . 622
Оптимизация кода. Основные методы оптимизации . . . . 630
Общие принципы оптимизации кода................... 630
Оптимизация линейных участков программы634
Другие методы оптимизации программ641
Машинно-зависимые методы оптимизации648
Контрольные вопросы и задачи650
Вопросы650
Задачи 652
Глава 15. Современные системы программирования 655
Понятие и структура системы программирования . 655
История возникновения систем программирования 655
Структура современной системы программирования658
Принципы функционирования систем программирования.660
Функции текстовых редакторов в системах программирования660
Компилятор как составная часть системы программирования .661
Компоновщик Назначение и функции компоновщика662
Загрузчики и отладчики. Функции загрузчика663
Библиотеки подпрограмм как составная часть систем программирования 665
Дополнительные возможности систем программирования668
Лексический анализ «на лету». Система подсказок и справок 668
Разработка программ в архитектуре «клиент—сервер»669
Разработка программ в трехуровневой архитектуре. Серверы приложений670
Примеры современных систем программирования672
Системы программирования компании Borland/lnprise672
Системы программирования фирмы Microsoft677
Системы программирования под ОС Linux и UNIX .. . 682
Разработка программного обеспечения для сети Интернет687
Контрольные вопросы и задачи 696
Вопросы696
Задачи 698
Приложение А. Тексты программы параллельных взаимодействующих задач... 700
Приложение Б. Тексты программ комплекса параллельных взаимодействующих приложений . . . 709
Список литературь 719
Алфавитный указатель 725
Предисловие
Настоящий учебник предназначается в первую очередь студентам технических вузов, хотя может быть востребован и обычными подготовленными пользователями, желающими углубить свои познания в области системного программного обеспечения.
Согласно Государственному образовательному стандарту, ио которому ведется обучение студентов, поступивших в вузы до 2000 г., в рамках дисциплины «Системное программное обеспечение», относящейся к обязательным специальным дисциплинам учебного плана по направлению «Информатика и вычислительная техника», студент должен изучить следующие обязательные разделы: «...назначение, функции и структура операционной системы (ОС); понятие процесса; управление процессами, способы диспетчеризации процессов; понятие ресурса, виды ресурсов, управление ресурсами; управление памятью; устройства, виды устройств, драйверы устройств, устройства в MS-DOS; файловая система на диске, структура логического диска в MS-DOS; синхронизация процессов, семафоры, сообщения, использование семафоров для решения задач взаимоисключения и синхронизации; тупики, способы борьбы с тупиками; загрузка и настройка ОС, файлы конфигурирования MS-DOS, основные команды MS-DOS; обзор современных ОС; трансляторы; формальные языки и грамматики, типы грамматик; вывод цепочек; конечный и магазинный автоматы, распознаватели и преобразователи, построение автомата по заданной грамматике; структура компиляторов и интерпретаторов, лексический, синтаксический и семантический анализаторы, генератор кода; распределение памяти, виды переменных; статическое и динамическое связывание; загрузчики; функции загрузчика; настраивающий и динамический загрузчики; подключение библиотек».
В новой редакции Государственного образовательного стандарта, который вступил в силу в 2000 г. и относится к поколению студентов, поступивших в вузы осенью 2000 г. и позже, несколько изменено основное содержание этой дисциплины. В частности, в стандарте записано, что в рамках дисциплины «Системное программное обеспечение» должны изучаться следующие обязательные разделы: «...пользовательский интерфейс операционной среды; управление задачами; управление, памятью; управление вводом/выводом; управление файлами; пример современной операционной системы, программирование в операционной среде; ассемблеры; мобильность программного обеспечения; макроязыки; трансляторы;
формальные языки и грамматики, типы грамматик; вывод цепочек; конечный и магазинный автоматы, распознаватели и преобразователи, построение автомата по заданной грамматике; структура компиляторов и UHwqmpemamopoe, лексический, синтаксический и семантический анализаторы, генератор кода; распределение памяти, виды переменных; статическое и динамическое связывание; загрузчики; функции загрузчика; настраивающий и динамический загрузчики, подключение библиотек».
Таким образом, мы можем констатировать, что в основном в дисциплине «Системное программное обеспечение» должно уделяться внимание операционным системам, средам и системам программирования. Именно в гаком ключе в основном и строится настоящий учебник, поскольку предполагается, что его читателями, прежде всего, будут студенты, обучающиеся по специальностям, относящимся к направлению «Информатика и вычислительная техника». Учебный материал, ставший основой для настоящего учебника, уже в течение нескольких лет изучается студентами специальности 22.01.00 в Санкт-Петербургском государственном университете аэрокосмического приборостроения. Другими словами, ио су- щсству, в основу учебника лег расширенны и конспект лекций ио дисциплине «Системное программное обеспечение». Эта дисциплина изучается в течение двух семестров. В нервом семестре рассматриваются операционные системы (принципы их построения и функционирования, вопросы создания параллельных взаимодействующих задач, выполняющихся в мультизадачных операционных системах), а во втором - формальные грамматики, трансляторы и системы программирования. Поэтому книга разбита на две крупные части. Эти две части связаны между собой не потому, что так построен план изучения дисциплины. Материал, рассматриваем ый в каждой из частей учебника, тесно связан с вопросами, изучаемыми в другой ее части. Таким образом, детальное изучение материала любой части книги требует по крайней мере знакомства с основными понятиями всего учебника. Так, например, изучение структуры и технических аспектов работы компиляторов невозможно без знания принципов распределения памяти, которые относятся к вопросам построения операционных систем. Именно поэтому эти два крупных раздела объединены авторами в одну книгу и совместно представляются вниманию читателей.
Помимо общетеоретических вопросов в учебнике рассмотрены и отдельные практические вопросы, описаны конкретные реализации различных системных программ.
В первой части учебника, прежде всего, излагаются основные понятия ОС, принципы их построения и функционирования В последние годы практически повсеместно ПК работают под управлением современных 32-битовых ОС, использующих аппаратные возможности микропроцессоров для создания и организации эффективных и защищенных вычислений. Мы посчитали необходимым рассмотреть в первой части учебника эти вопросы.
Наиболее популярными ОС являются системы Windows 95/98, Windows NT 4.0, начинается переход к Windows ME и семейству ОС Windows 2000 компании Microsoft. По этим ОС имеется огромное количество самых разнообразных публикаций, в том числе и учебных материалов, объем которых порой очень велик. В то же время по остальным ОС публикаций существенно меньше. Поэтому
в первой части настоящего учебного пособия мы в качестве примеров операционных систем и сред кратко рассматриваем такие ОС, как OS/2 Warp, UNIX и Linux, QNX. Естественно, что отдельные вопросы иллюстрируются и на примере популярных ОС Windows 95/98 и Windows NT 4.0.
Во второй части учебника рассматриваются как общие вопросы, связанные с построением трансляторов, так и методы их практической реализации от примитивных распознавателей текста до законченных систем программирования. Практическая реализация компиляторов и интерпретаторов рассматривается с точки зрения современных широко распространенных языков программирования высокого уровня, таких как Pascal, С и C++.
Кратко рассматриваются также практические вопросы построения прикладных программ на основе архитектуры «клиент—сервер» и трехзвенной архитектуры, ориентированной па работу с серверами баз данных и серверами приложений. Эти вопросы затрагиваются не с точки зрения технологии их реализации (такие сведения можно найти в появившейся сейчас специализированной литературе) в той или иной ОС, а со стороны методов разработки соответствующих прикладных программ с помощью той пли иной системы программирования. Для более детального знакомства с конкретными реализациями читатели могут воспользоваться приводимыми авторами ссылками на литературные источники или на соответствующие технические публикации в глобальной сети Интернет.
Таким образом, данный учебник должен быть полезен не только тем, кто хочет детально изучить системное программное обеспечение, но и тем, кто собирается сам разработать отдельные компоненты, в том числе отдельные системные утилиты, распознаватели и интерпретаторы команд, компоновщик или транслятор с некоторого языка, создать комплекс параллельно исполняющихся взаимодействующих программ. В учебнике рассматриваются вопросы, которые полезно знать в любом случае, если разработчик прикладной программы имеет дело с некоторым входным языком (которым может быть далеко не только язык программирования, но и любой другой язык команд, в том числе заданный пользователем или определенный в некоторой прикладной области).
Примеры систем программирования, которые рассматриваются в этом учебнике, предназначены для работы в среде перечисленных выше операционных систем. Фрагменты программ, приведенные в книге, написаны на языках программирования высокого уровня Pascal и С.
В этом коротком предисловии мы, как авторы этой книги, хотим еще высказать самые теплые слова благодарности своим родным и близким за их долгое терпение, доброжелательность и сердечную заботу в течение всего времени подборки материалов, написания книги и ес «бесконечного» улучшения, благодаря которым только и мог появиться на свет этот труд, вынужденно оторвавший нас на некоторое время от домашних забот и хлопот. Хочется поблагодарить и Гордеева В. А. за конструктивную критику и помощь в подготовке отдельных программ. Авторы признательны также сотрудникам издательства «Питер» Васильеву А В и Ваулиной Е. IO. за их терпеливое и внимательное отношение в процессе подготовки текста книги, его обработки и корректуры. Наше взаимное уважение и сотрудничество позволили довести учебный материал книги «до ума», хотя это и нс всегда удавалось сделать в заранее оговоренные сроки.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Подробную информацию о наших книгах вы найдете па Web-сайте издательства
http://www.piter.com.
ЧАСТЬ IЧАСТЬ I
ЧАСТЬ I
ЧАСТЬ I
Операционные системы и среды
В англоязычной технической литературе термин System Software (системное программное обеспечение) означает программы и комплексы программ, являющиеся общими для всех, кто совместно использует технические средства компьютера, и применяемые как для автоматизации разработки (создания) новых программ, так и для организации выполнения программ существующих. С этих, позиций системное программное обеспечение может быть разделено па следующие пять групп:
Операционные системы.
Системы управления файлами.
Интерфейсные оболочки для взаимодействия пользователя с ОС и программные среды.
Системы программирования.
Утилиты.
Рассмотрим вкратце эти группы системных программ.
Под операционной системой (ОС) обычно понимают комплекс управляющих и обрабатывающих программ, который, с одной стороны, выступает как интерфейс между аппаратурой компьютера и пользователем с его задачами, а с другой — предназначен для наиболее эффективного использования ресурсов вычислительной системы и организации надежных вычислений. Любой из компонентов прикладного программного обеспечения обязательно работает под управлением ОС. На рис. 1 изображена обобщенная структура программного обеспечения вычисли тельной системы. Видно, что ни один из компонентов программного обеспечения, за исключением самой ОС, не имеет непосредственного доступа к аппаратуре компьютера. Даже пользователи
взаимодействуют со своими программами через интерфейс ОС. Любые их команды, прежде чем попасть в прикладную программу, сначала проходят через ОС.
Рис.1. Обобщенная структура программного обеспечения вычислительной системы
Основными функциями, которые выполняет ОС, являются следующие:
О прием or пользователя (или от оператора системы) заданий или команд, сформулированных на соответствующем языке — в виде директив (команд) оператора или в виде указаний (своеобразных команд) с помощью соответствующего манипулятора (например, с помощью мыши), — и их обработка;
О прием и исполнение программных запросов на запуск, приостановку, остановку других программ;
О забрузка в оперативную память подлежащих исполнению программ,
О инициация программы (передача ей управления, в результате чего процессор исполняет программу);
О идентификация всех программ и данных;
О обеспечение работы систем управлении файлами (СУФ) и/или систем управления базами данных (СУБД), что позволяет резко увеличить эффективность всего программного обеспечения;
О обеспечение режима мультипрограммирования, то есть выполнение двух или более программ на одном процессоре, создающее видимость их одновременного иснол ней ня;
О обеспечение функций по организации и управлению всеми операциями ввода/вывода;
О удовлетворение жестким ограничениям на время отпета в режиме реального времени (характерно для соответствующих ОС);
О распределение памяти, а в большинстве современных систем и организация виртуальной памяти;
О планирование и диспетчеризация задач в соответствии с заданными стратегией и дисциплинами обслуживания;
О организация механизмов обмена сообщениями и данными между выполняющимися программами;
О защита одной программы от влияния другой; обеспечение сохранности данных;
О предоставление услуг на случай частичного сбоя системы;
О обеспечение работы систем программирования, с помощью которых пользователи готовят свои программы.
Назначение системы управления файлами — организация более удобного доступа к данным, организованным как файлы. Именно благодаря системе управления файлами вместо низкоуровневого доступа к данным с указанием конкретных физических адресов нужной нам записи используется логический доступ с указанием имени файла и записи в нем. Как правило, все современные ОС имеют соответствующие системы управления файлами. Однако выделение этого вида системного программного обеспечения в отдельную категорию представляется целесообразным, поскольку ряд ОС позволяет работать с несколькими файловыми системами (либо с одной из нескольких, либо сразу с несколькими одновременно). В этом случае говорят о монтируемых файловых системах (дополнительную систему управления файлами можно установить), и в этом смысле они самостоятельны. Более того, можно назвать примеры простейших ОС, которые могут работать и без файловых систем, а значит, им необязательно иметь систему управления файлами, либо они могут работать с одной из выбранных файловых систем. Надо, однако, понимать, что любая система управления файлами нс существует сама по себе — она разработана для работы в конкретной ОС и с конкретной файловой системой. Можно сказать, что всем известная файловая система FAT (file allocation table)* имеет множество реализаций как система управления файлами, например FAT-16 для самой MS-DOS, supcr-FAT для OS/2, FAT для Windows NT
1 Здесь и далее без указания на источник заимствования приводятся английские эквиваленты слов и словосочетаний.
и г. л. Другими словами, для работы с файлами, организованными в соответствии с некоторой файловой системой, для каждой ОС должна быть разработана соответствующая система управления файлами; и эта система управления файлами будет работать только в той ОС, для которой она и создана.
Для удобства взаимодействия с ОС могут использоваться дополнительные интерфейсные оболочки. Их m лонное назначение — либо расширить возможности по управлению ОС, либо изменить встроенные в систему возможности. В качестве классических примеров интерфейсных оболочек и соответствующих операционных сред выполнения программ можно назвать различные вариан гы графического интерфейса X Window в системах семейства UNIX (например, К Desktop Environment в Linux), PM Shell или Object Desktop в OS/2 с iрафнческим интерфейсом Presentation Manager; наконец, можно указать разнообразные варианты интерфейсов для семейства ОС Windows компании Microsoft, которые заменяют Explorer и могут напоминать либо UNIX с его графическим интерфейсом. либо OS/2, либо MAC OS. Следует отмстить, что о семействе ОС компании Microsoft с общим интерфейсом реализуемым программными модулями с названием Explorer (в файле system.ini, который находится в каталоге Windows, имеется строка SHELL=EXPLORER.EXE), все же можно сказать, что заменяемой в этих системах является только ин- терфейсная оболочка, в то время как сама операционная среда остается неизменной; опа интегрирована в ОС. Другими словами, операционная среда определяется программными интерфейсами, то есть API (application program interface). Интерфейс прикладного программирования (API) включает в себя управление процессами, памятью и вводом/вы волом.
Ряд операционных систем могут организовывать выполнение программ, созданных для других ОС. Например, в OS/2 можно выполнять как программы, созданные для самой OS/2, так и программы, предназначенные для выполнения в среде MS-DOS и Windows 3.x. Соответствующая операционная среда организуется в операционной системе в рамках отдельной виртуальной ма- шпны. Аналогично, в системе Linux можно создать условия для выполнения некоторых программ, написанных для Windows 95/98. Определенными возможностями исполнения программ, сосанных для иной операционной среды, обладает и Windows NT Эта система позволяет выполнять некоторые программы. созданные для MS DOS, OS/2 Lx, Windows 3.x Правда, в своем последнем семействе ОС Windows ХР разработчики решили отказаться от поддержки возможности выполнения DOS-программ.
Наконец, к этому классу системного пртраммного обеспечения следует отнести и эмуляторы, позволяющие смоделировать в одной операционной системе какую-либо другую машину пли операционную систему. Так, известна система эмуляции WMWARE, которая позволяет запустить в среде Linux тюбую другую ОС, например Windows. Можно, наоборот, создать эмулятор, работающий в среде Windows, который позволит смоделировать компьютер, работающий под управлением любой ОС, в том числе н под I innx.
Таким образом, термин операционная среда означает соответствующий ни терфойс, необходимый программам для обращения к ОС с целью получить
определенный сервис — выполнигь операцию ввода/вывода, получить или освободить участок памяти и т. д.
Система программирования па рис. 1 представлена прежде всего такими компонентами, как транслятор с соответствующего языка, библиотеки подпрограмм, редакторы, компоновщики и отладчики. Не бывает самостоятельных (оторванных от ОС) систем программирования. Любая система программирования может работать только в соответствующей ОС, пом которую она и создана, однако при этом опа может позволять разрабатывать программное обеспечение и под другие ОС. Например, одна из популярных систем программирования на языке C/C++ от фирмы Watcom для OS/2 позволяет получать программы и для самой OS/2, и для DOS, и для Windows.
В том случае, когда создаваемые программы должны работать совсем па другой annapai пой базе, говорят о кросс-системах. Так, для ПК на базе микропроцессоров семейства 180x86 имеется большое количество кросс-систем, позволяющих создавать программное обеспечение для различных микропроцессоров и микроконтроллеров.
Наконец, под утилитами понимают специальные системные программы, с по мощью которых можно как обслуживать саму операционную систему, так и подготавливать для работы носители данных, выполнять перекодирование данных, осуществлять оптимизацию размещения данных па носителе и производить некоторые другие работы, связанные с обслуживанием вычислительной системы. К утилитам следует отнести и программу разбиения накопителя на магнитных дисках на разделы, и программу форматирования, и программу переноса основных системных файлов самой ОС. Также к утилитам относятся и небезызвестные комплексы программ от фирмы Symantec, посящп имя Питера Нортона (создателя этой фирмы и соавтора популярного набора уги- 1иг для первых IBM PC). Естественно, что утилиты могут работать только в соответствующей операционной среде.
ГЛАВА 1 Основные понятия
Понятие операционной среды
Операционная система выполняет функции управления вычислительными процессами в вычислительной системе, распределяет ресурсы вычислительной системы между различными вычислительными процессами и образует программную среду, в которой выполняются прикладные программы пользователей. Такая среда называется операционной.
Любая iipoi рамма имеет дело с некоторыми исходными данными, которые она обрабатывает, и порождает в конечном итоге некоторые выходные данные, результаты вычислений. Очевидно, что в абсолютном большинстве случаев исходные данные попадают в оперативную намять (с которой непосредственно работает процессор, выполняя вычисления по программе) с внешних (периферийных) устройств Аналогично и результаты вычислений, в конце концов, должны быть выведены на внешние устройства. Следует заметит!., что программирование опе- рацнй ввода/вывода относится, пожалуй, к наиболее сложным и трудоемким задачам. Дело в том, ч то при создании таких программ без использования современных систем программирования, как говорится, «по старинке», нужно знать не только архи тектуру процессора (его состав, назначение основных регистров, систему команд процессора, форматы данных и т. п ), по и архитектуру подсистемы ввода/вывода (соответствующие интерфейсы, протоколы обмена данными, алгоритм работы контроллера устройства ввода/вывода и г. д.). Именно поэтому развитие системного программирования и самого системного программного обеспечения пошло по пути выделения наиболее часто встречающихся операций и создания для них соответствующих программных модулей, которые можно в дальнейшем использовать в большинстве вновь создаваемых программ.
Например, в далекие пятидесятые годы, на заре развития вычислительных систем, при разработке первых систем программирования прежде всего создавали программные модули для подсистемы ввода/вывода, а уже затем — вычисления часто встречающихся математических операций и функций. Благодаря этому при создании прикладных программ программисты могли просто обращаться к соответствующим функциям ввода/вывода и иным функциям и процедурам, что избавляло их от необходимости каждый раз создавать все программные компонен-
ты «с нуля» и от необходимости знать во всех подробностях особенности работы контроллеров ввода/вывода и соответствующих интерфейсов.
Следующий шаг в автоматизации создания гиговых к выполнению машинных двоичных программ заключался в том, что транслятор с алгоритмического языка более высокой) уровня, нежели первые ассемблеры, уже сам мог подставить вместо высокоуровневого оператора типа READ или WRITE все необходимые вызовы к готовым библиотечным программным модулям. Состав и количество библиотек систем программирования постоянно увеличивались. В конечном итоге возникла ситуация, когда при создании двоичных машинных программ программисты могут вообще не знать многих деталей управления конкретными ресурсами вычислительной системы, а должны только обращаться к некоторой программной подсистеме с соответствующими вызовами и получать от нес необходимые функции и сервисы. Эта программная подсистема и есть операционная система (ОС), а набор ее функций, сервисов и правила обращения к ним как раз и образуют то базовое понятие, которое мы называем операционной средой. Таким образом, можно сказать, что термин операционная среда означает, прежде всего, соответствующие интерфейсы, необходимые программам и пользователям для обращения к ОС с целью получить определенные сервисы.
Можно спросить: а чем отличаются системные программные модули, реализующие основные системные функции, от тех программных модулей, что пишутся прикладными программистами? Ответ простой: тем, что эти модули, как правило, используются всеми прикладными программами. Поэтому пет особого смысла на этапе создания машинной двоичной программы (которую и исполняет процессор) присоединять соответствующие системные программные модули к телу программы. Выгоднее просто обращаться к этим программным модулям, указывая их адреса и передавая им необходимые параметры, поскольку они уже и гак находятся в основной памяти, ибо нужны всем. Другими словами, эти основные системные программные модули входят в состав самой ОС.
Параллельное существование терминов «операционная система» и «операционная среда» вызвано тем, что ОС в общем случае может поддерживать несколько операционных сред. Например, операционная система OS/2 Warp может выполнять следующие программы:
так называемые «натиеные» программы, созданные с учетам соответствующего «родного» 32-битового программного интерфейса этой ОС;
16-битовые программы, созданные для систем OS/2 первого поколения;
16 битовые приложения, разработанные для выполнения в операционной среде MS-DOS или PC DOS
16-битовые приложения, созданные для операционной среды Windows 3.x;
О саму операционную оболочку Windows 3.x и уже в ней — созданные для нее программы.
Операционная среда может включать несколько пнтерфейсов: по ьзовательские и программные. Если говорить о пользовательских, то, например, система Linux
имеет для пользователя как интерфейсы командной строки (можно использовать различные «оболочки» — shell), интерфейс наподобие Norton Commander Midnight Commander, гак и графические интерфейсы — X-Window с различными менеджерами окон — KDE, Gnome и т. д. Если же говорить о программных интерфейсах, го в гой же ОС Linux программы могут обращаться как к операционной системе за соответствующими сервисами и функциями, гак и к графической подсистеме (если опа используется). С точки зрения архитектуры процессора (н всего ПК в целом) двоичная программа, созданная для работы в среде Linux, использует те же команды и форматы данных, что и программа, созданная для работы в среде Windows NT. Однако в первом случае мы имеем обращение к одной операционной среде, а во втором — к другой. И программа, созданная для Windows непосредственно, не будет выполняться в Linux; однако если в ОС Linux организовать полноценную операционную среду Windows, то наша Windows-программа сможет быть выполнена. Можно сказать, что операционная среда — это то системное программное окружение, в котором могут выполняться программы, созданные по правилам работы этой среды.
Понятия вычислительного
процесса и ресурса
Понятие «вычислительный процесс» (или просто — «процесс») является одним из основных при рассмотрении операционных систем. Как понятие, процесс яв лястся определенным видом абстракции, и мы будем придерживаться следующе го неформального определения, приведенного в работе |37]. Последовательный процесс (иногда называемый «.задачей»') — это выполнение отдельной программы с ее данными на последовательном процессоре. Концептуально процессор рассматривается в двух аспектах: во-первых, он является носителем данных и, во- вторых, он (одновременно) выполняет операции, связанные с их обработкой.
В качестве примеров можно назвать следующие процессы (задачи): выполнение прикладных программ пользователей, ути шт и других системных обрабатывающих программ. Процессами могут быть редактирование какого-либо текста, трансляция исходной программы, ее компоновка, исполнение. Причем трансляция какой-нибудь исходной программы является одним процессом, а трансляция следующей исходной программы — другим процессом, поскольку, хотя транслятор как объ-едипсние программных модулей здесь выступает как одна и та же программа, но данные, которые он обрабатывает, являются разными.
Опредслопио концепции процесса преследует цель выработать механизмы распре деления и управления ресурсами. Понятие ресурса, так же как и понятие процесса,
В концепции которая получила наибольшее распространение в 70-е годы, задача (ia.sk) ) о совокупность связанных между собой и образующих единое целое программных модулей и данных, требующая рссу|)сов вычислительной слегемы для своей реализации В последующие годы задачей спин называть единицу работы, для выполнения которой предоставляется центральный процессор. Вычислительный процесс может включать в себя несколько задач.
является, пожалуй, основным при рассмотрении операционных систем. Термин ресурс обычно применяется по отношению к повторно используемым, относительно стабильным и часто недостающим объектам, которые запрашиваются, используются и освобождаются процессами в период их активности. Другими словами, ресурсом называется всякий объект, который может распределяться внутри системы.
Ресурсы могут быть разделяемыми, когда несколько процессов могут их использовать одновременно (в один и тот же момент времени) или параллельно (в течение некоторого интервала времени процессы используют ресурс попеременно), а могут быть и неделимыми (рис. 1.1).
Рис. 11. Классификация ресурсов
При разработке первых систем ресурсами считались процессорное время, память, каналы ввода/вывода и периферийные устройства (50, 89]. Однако очень скоро понятие ресурса стало гораздо более универсальным и общим. Различного рода иро1"раммные л информационные ресурсы также могут быть определены для системы как объекты, которые могут разделяться и распределяться, и доступ к которым необходимо соответствующим образом контролировать В настоящее время понятно ресурса превратилось в абстрактную структуру с целым рядом атрпбу тов, характеризующих способы доступа к этой структуре и се физическое пред ставление в системе. Более того, помимо системных ресурсов, о которых мы сейчас говорили, как ресурс стали толковать и такие объекты, как сообщения и синхросигналы, которыми обмениваются задачи.
В первых вычислительных системах любая программа могла вы пол пяться только после полного завершения предыдущей. Поскольку эти первые вычислительные системы были построены в соответствии с принципами, изложенными в известной работе Яноша Джон фон Неймана, все подсистемы и устройства компьютера управлялись исключительно центральны^ процессором. Центральный процессор осуществлял и выполнение вычислений, и управление операциями ввода/вывода данных. Соответственно, пока осуществлялся обмен данными между оперативной памятью и внешними устройствами, процессор не мог выполнять вычисления. Введение в состав вычислительной машины специальных коитрочлеров позволило совместить во времени (распараллелить) операции вывода получен пых данных и последующие вычисления на центральном процессоре. Однако все равно процессор продолжал часто и долго простаивать, дожидаясь завершения
очередное! операции ввода/вывода. Поэтому было предложено организовать так называемый мультипрограммный (мультизадачный) режим работы вычислительной системы. Суть его заключается в том, что пока одна программа (один вычислительный процесс или задача, как мы теперь говорим) ожидает завершения очередной операции ввода/вывода, другая программа (а точнее, другая задача) может быть поставлена на решение (рис. 1.2).
Рис. 1.2. Пример выполнения двух программ: а — однопрограммный режим; б — мультипрограммный режим
Из рис. 1.2, на котором в качестве примера изображена такая гипотетическая си- туация, видно, что благодаря совмещению во времени выполнения двух программ общее время выполнения двух задач получается меньше, чем если бы мы выполняли их по очереди (запуск одной только после полного завершения другой). Из этого же рисунка видно, что время выполнения каждой задачи в общем случае становится больше, чем если бы мы выполняли каждую из них как единственную.
При мультипрограммировании повышается пропускная способность системы, но отдельный процесс никогда не может быть выполнен быстрее, чем если бы он выполнялся в однопрограммном режиме (всякое разделение ресурсов замедляет работу одного из участников за счет дополнительных затрат времени на ожидание освобождения ресурса).
Как мы уже отмечали, операционная система поддерживает мультипрограммирование (мпогопроцессность) и старается эффективно использовать ресурсы путем организации к ним очередей запросов, составляемых тем или иным способом. Это требование достигается поддерживанием в памяти, более одного процесса, ожидающего процессор, и более одного процесса, готового использовать другие ресурсы, как только последние станут доступными. Общая схема выделения ресурсов такова. При необходимости использовать какой-либо ресурс (оперативную память, устройство ввода/вывода, массив данных и т. п.) задача обращается к супервизору операционной системы — ее центральному управляющему модулю, который может состоять из нескольких модулей, например: супервизор ввода/вывода, супервизор прерываний, супервизор программ, диспетчер задач и т. д. — посредством специальных вызовов (команд, директив) и сообщает о своем требовании. При этом указывается вид ресурса и, если надо, его объем (например, количество адресуемых ячеек оперативной памяти, количество дорожек или секторов на системном диске, устройство печати и объем выводимых данных и т. и.).
Директива обращения к операционной системе передает ей управление, переводя процессор в привилегированный режим работы (см. раздел «Прерывания», глава 1), если такой существует. Не все вычислительные комплексы имеют два (и более) режима работы: привилегированный (режим супервизора), пользовательский, режим эмуляции какого-нибудь другого компьютера и т. д.
Ресурс может быть выделен задаче, обратившейся к супервизору с соответствующим запросом, если
он свободен и в системе нет запросов от задач более высокого приоритета к этому же ресурсу;
текущий запрос и ранее выданные запросы допускают совместное использование ресурсов;
ресурс используется задачей низшего приоритета и может быть временно отобран (разделяемый ресурс).
Получив запрос, операционная система либо удовлетворяет его и возвращает управление задаче, выдавшей данный запрос, либо, если ресурс занят, ставит задачу в очередь к ресурсу, переводя ее в состояние ожидания (блокируя). Очередь к ресурсу может быть организована несколькими способами, но чаще всего это осуществляется с помощью списковой структуры.
После окончания работы с ресурсом задача опять с помощью специального вызова супервизора (посредством соответствующей директивы) сообщает операци онной системе об отказе от ресурса, или операционная система забирает ресурс сама, если управление возвращается супервизору после выполнения какой-либо системной функции. Супервизор операционной системы, получив управление по этому обращению, освобождает ресурс и проверяет, имеется ли очередь к ос-
нободившемуся ресурсу. Если очередь сеть — в зависимости от принятой дисциплины обслуживания (правила обслуживания) и приоритетов заявок он выводит из состояния ожидания задачу, ждущую ресурс, и переводит ее в состояние готовности к выполнению. После этого управление либо передается данной задаче, либо возвращается той, которая только что освободила ресурс.
При выдаче запроса на ресурс задача может указать, хочет ли она владеть ресурсом монопольно или допускает совместное использование с другими задачами Например, с файлом можно работать монопольно, а можно и совместно с другими задачами.
Если в системе имеется некоторая совокупность ресурсов, то управлять их пс- иользованием можно па основе определенной стратегии. Стратегия подразумевает четкую формулировку целей, следуя которым можно добиться эффективного распределения ресурсов.
При организации управления ресурсами всегда требуется принять решение о том, что в данной ситуации выгоднее: быстро обслуживать отдельные наиболее важные запросы, предоставлять всем процессам равные возможности, либо обслуживать максимально возможное количество процессов и наиболее полно использовать ресурсы [37].
Диаграмма состояний процесса
Необходимо различать системные управляющие процессы, представляющие работу супервизора операционной системы и занимающиеся распределен нем и управлением ресурсов, от всех других процессов: системных обрабатывающих процессов, которые не входят в ядро операционной системы, и процессов пользователя. Для системных управляющих процессов в большинстве операционных систем ресурсы распределяются изначально и однозначно. Эти процессы управляют ресурсами системы, за использование которых существует конкуренция между всеми остальными процессами. Поэтому исполнение системных управляющих программ не принято называть процессами. Термин задача можно употреблять только по отношению к процессам пользователей и к системным обрабатывающим процессам. Однако это справедливо не для всех ОС. Например, в так называемых «мик- роядерпых» (см. главу 5 «Архитектура операционных систем и интерфейсы прикладного npoi раммирования») ОС (в качестве примера можно привести ОС реального времени QNX фирмы Quantum Software systems) большинство управляющих программных модулей самой ОС и даже драйверы имеют статус высокоприоритетных процессов, для выполнения которых необходимо выделить соответствующие ресурсы. Аналогично и в UNIX-системах выполнение системных программных модулей тоже имеет статус системных процессов, которые получают ресурсы для своего исполнения.
Если обобщать и рассматривать не только обычные ОС общего назначения, но и, например, ОС реального времени, то можно сказать, что процесс может находиться в активном и пассивном (не активном) состоянии. 13 активном состоянии
процесс может участвовать в конкуренции за использование ресурсов вычислительной системы, а в пассивном — он только известен системе, но в конкуренции не участвует (хотя его существование в системе и сопряжено с предоставлением ему оперативной и/нли внешней памяти). В свою очередь, активный процесс может быть в одном из следующих состояний:
выполнения — все затребованные процессом ресурсы выделены. В этом состоянии в каждый момент времени может находиться только один процесс, если речь идет об однопроцессорной вычисли тельной системе;
готовности к выполнению — ресурсы могут быть предоставлены, тогда процесс перейдет в состояние выполнения;
О блокирования или ожидания — затребованные ресурсы не могут быть предоставлены, или не завершена операция ввода/вывода.
В большинстве операционных систем последнее состояние в свою очередь, подразделяется па множество состояний ожидания, соответствующих определенному виду jiCcypca, из-за отсутствия которого процесс переходит в заблокированное состояние.
В обычных ОС, как правило, процесс появляется при запуске какой-нибудь программы. ОС организует (порождает пли выделяет) для нового процесса соответствующий дескриптор (см. об этом дальше) процесса, и процесс (задача) начинает развиваться (выполняться). Поэтому пассивного состояния не существует. В ОС реального времени (ОСРВ) ситуация иная. Обычно при проектировании системы реального времени уже заранее бывает известен состав программ (задач), которые должны будут выполняться. Известны и многие их параметры, которые необходимо учитывать при распределении ресурсов (например, объем памяти, приоритет, средняя длительность выполнения, открываемые смайлы, используемые устройства и т. п.). Поэтому для них заранее заводят дескрипторы задач с тем, чтобы впоследствии не тратить драгоценное время па организацию дескриптора и поиск для него необходимых ресурсов. Таким образом, в ОСРВ многие процессы (задачи) могут находиться в состоянии бездействия, что мы и отобразили па рис. 1.3, отделив это состояние от остальных состояний пунктиром.
За время своего существования процесс может неоднократно совершать переходы из одного состояния в другое. Это обусловлено обращениями к операционной системе с запросами ресурсов и выполнения системных функций, которые предоставляет операционная система, взаимодействием с другими процессами, появлением сигналов прерывания от таймера, каналов и устройств ввода/вывода, а также других устройств. Возможные переходы процесса па одного состояния в другое отображены в виде графа состояний на рис. 1.3. Рассмотрим эти переходы из одного состояния в другое более подробно.
Процесс из состояния бездействия может перейт и в состояние готовности в следующих случаях:
но команде оператора (пользователя). Имеет место в тех диалоговых операционных системах, где программа может иметь статус задачи (и при этом являться пассивной), а не просто быть исполняемым файлом и только на время
исполнения получать статус задачи (как это происходит в большинстве современных ОС для НК);
при выборе из очереди планировщиком (характерно для операционных систем, работающих в пакетном режиме);
по вызову из другой задачи (посредством обращения к супервизору один процесс может, создать, инициировать, приостановить, остановить, уничтожить другой процесс);
по прерыванию от внешнего инициативного устройства (сигнал о свершении некоторого события может запускать соответствующую задачу);
при наступлении запланированного времени запуска программы.
Рис. 1.3. Граф состояний процесса
Последние два способа запуска задачи, при которых процесс из состояния бездействия переходит в состояние готовности, характерны для операционных систем реального времени.
Процесс, который может исполняться, как только ему будет предоставлен процессор, а для дпск-резидснтпых задач в некоторых системах — и оперативная память, находится в состоянии готовности. Считается, что такому процессу уже выделены все необходимые ресурсы за исключением процессора.
Из состояния выполнения процесс может выйти по одной из следующих причин:
процесс завершается, при этом он посредством обращения к супервизору передает управление операционной системе и сообщает о своем завершении. В результате этих действий супервизор либо переводит его в список бездействующих процессов (процесс переходит в пассивное состояние), либо уничтожает (уничтожается, естественно, нс сама программа, а именно задача, которая соответствовала исполнению некоторой программы). В состояние бездействия процесс может быть переведен принудительно: по команде оператора (дейст-
вие этой и других команд оператора реализуется системным процессом, который «транслирует» команду в запрос к супервизору с требованием перевести указанный процесс в состояние бездействия), или путем обращения к супер визору операционной системы из другой задачи с требованием остановить данный процесс;
процесс переводится супервизором операционной системы в состояние готовности к исполнению в связи с появлением ботее приоритетной задачи тшт в связи с окончанием выделенного ему кванта времени;
процесс блокируется (переводится в состояние ожидания) либо вследствие запроса операции ввода/вывода (которая должна быть выполнена пр жде, чем он сможет продолжить исполнение), либо в силу невозможности предоставить ему ресурс запрошенный в настоящий момент (причиной перевода в состояние ожидания может быть и отсутствие сегмента или страницы в случае организации механизмов виртуальной памяти, см. раздел «Сегментная, страничная и сегментно-страничная организация памяти» в главе 2), а также по команде оператора на приостановку задачи или по требованию через супервизор от другой задачи.
При наступлении соответствующего события (завершилась операция ввода/вывода, освободился затребованный ресурс, в оперативную память загружена необходимая страница виртуальной памяти и т д ) процесс деблокируется и переводится в состояние готовности к исполнению.
Таким образом, движущей силой, меняющей состояния процессов, являются события. Один из основных видов событий — это прерыванги
Реализация понятия последовательного процесса в ОС
Для того чтобы операционная система могла управлять процессами, она должна располагать всей необходимой для этого информацией С этой целью на каждый процесс заводится специальная информационная структура, называемая дескриптором процесса (описателем {задачи, блоком управления задачей) В общем случае дескриптор процесса содержит следующую информацию
идентификатор процесса (так называемый PID ■ process identifieator)
тип (или класс) процесса, который определяет для супервизора некоторые правила предоставления ресурсов;
приоритет процесса, в соответствии с которым супервизор предоставляет ресурсы В рамках одного класса процессов в первую очередь обслуживаю гея более приоритетные процессы;
переменную состояния, которая определяет, в каком состоянии находится процесс (готов к работе, в состоянии выполнения, ожидание устройства ввода/вывода и т. д.);
защищенную область памяти (или адрес такой зоны), в шпорой хранятся текущие значения регистров процессора, если процесс прерывается, не закончив работы Эта информация называется контекстом задачи
информацию о ресурсах, которыми процесс владеет и/илн имеет право пользоваться (указатели на открытые файлы, информация о незавершенных операциях ввода/вывода и т. н.);
место (или его адрес) для организации общения с другими процессами;
параметры времени запуска (момент времени, когда процесс должен активизироваться, и периодичность этой процедуры);
в случае отсутствия системы управления файлами — адрес задачи на диске в ее исходном состоянии и адрес на диске, куда она выгружается из оперативной памяти, если ее вытесняет другая (для диск-резидентных задач, которые постоянно находятся во внешней памяти на системном магнитном диске и загружаются в оперативную память только па время выполнения).
Описатели задач, как правило, постоянно располагаются в оперативной памяти с целью ускорить работу супервизора, который организует их в списки (очереди) и отображает изменение состояния процесса псромешепнем соответствующего описателя из одною списка в другой. Для каждого состояния (на исключением состояния выполнения для однопроцессорной системы) операционная система ведет соответствующий список задач, находящихся в этом состоянии. Однако для состояния ожидания может быть нс одни список, а столько, сколько различных видов ресурсов могут вызывать состояние ожидания. Например, состояний ожидания завершения операции ввода/вывода может быть столько, сколько устройств ввода/вывода имеется в системе.
В некоторых операционных системах количество описателей определяется жестко и заранее (на этапе генерации варианта операционной системы или в конфигурационном файле, который используется при загрузке ОС), в других — по мере необходимости система может выделять участки памяти под новые описатели. Например, в OS/2 максимально возможное количество описателен задач определяется в конфигурационном файле CONFIG.SYS, а в Windows NT оно в явном виде не задается. Справедливости ради стоит заметить, что в упомянутом файле указывается количество не процессов, а именно задач, и под задачей в данном случае понимается как процесс, так и поток этого же процесса, называемый потоком или тредом (см. следующий раздел). Например, й рока в файле CONFIG.SYS
THREADS=1024
указывает, что всего в системе может параллельно существовать и выполняться до 1024 задач, включая вычислительные процессы и их потоки.
В ОС реального времени чаще всего количество процессов фиксируется и, следовательно, целесообразно заранее определять (па этапе генерации или конфигурирования ОС) количество дескрипторов. Для использования таких ОС в качестве систем общего назначений (что сейчас встречается редко, а в недалеком прошлом достаточно часто в качестве вычислительных систем общего назначения приобретали мини-ЭВМ и устанавливали на них ОС реального времени) обычно количество дсскршгтщюв берется с некоторым запасом, и появление новой задачи связывается с заполнением этой информационной структуры. Поскольку дескрипторы процессов постоянно располагаются в оперативной памяти (с целью ускорить работу диспетчера), то их количество не должно быть очень большим При необходимости иметь большое количество задач один и тот же деск-
риптор может в разное время предоставляться для разных задач, но это сильно снижает скорость реагирования системы.
Для более аффективной обработки данных в системах реального времени целесообразно иметь постоянные задачи, полностью или частично всегда существующие в системе независимо от того, поступило па них требование или нет. Каждая постоянная задача обладает некоторой собственной областью оперативной памяти (ОЗУ-ревидентные задачи) независимо от того, выполняется задача в данный момент или нет. Эта область, в частности, может использоваться для хранения данных, полученных задачей ранее. Данные могут храниться в иен и тогда, когда задача находится в состоянии ожидания или даже в состоянии бездействия.
Для аппаратной поддержки работы операционных систем с этими информационными структурами (дескрипторами задач) в процессорах могут быть реализованы соответствующие механизмы. Так, например, в микропроцессорах Intel 80x86 (см. главу 3 «Особенности архитектуры микропроцессоров i80x8G для организации мультипрограммных операционных систем»), начиная с 80286, имеется специальный регистр TR (task register), указывающий местонахождение TSS (сегмента состояния задачи, см. раздел «Новые системные регистры микропроцессоров 180x86», глава 3), в котором при переключении с задачи на задачу автоматически сохраняется содержимое регистров процессора [2, 22, 84]. Как правило, в современных ОС для этих микропроцессоров дескриптор задачи включает в себя TSS. Другими словами, дескриптор задачи больше по размеру, чем TSS, и включает в себя такие традиционные поля, как идентификатор задачи, се имя, тип, приоритет и т. и.
Процессы и треды
Понятие процесса было введено для реализации идей мультипрограммирования. Напомним, в свое время различали термины «мультизадачность» и «мультипрограммирование». Таким образом, для реализации «мультизадачное™» в ее исходном толковании необходимо было тоже ввести соответствуюп1ую сущность. Такой сущностью и стали так называемые «легковесные» процессы, или, как их теперь преимущественно называют, — потоки или треды (инти). Рассмотрим эти понятия подробнее.
Когда говорят о процессах (process), то тем самым хотят отметить, что операционная система поддерживает их обособленность: у каждого процесса имеется свое виртуальное адресное пространство, каждому процессу назначаются своп ресурсы — файлы, окна, семафоры и т. д. Такая обособленность нужна для того, чтобы защитить один процесс от другого, поскольку они, совместно используя все ресурсы вычислительной системы, конкурируют друг с другом. В общем случае процессы просто никак не связаны между собой и могут принадлежать даже разным пользователям, разделяющим одну вычислительную систему. Другими словами, в случае процессов ОС считает их совершенно несвязанными и независи-
мыми. При этом именно ОС берет на себя роль арбитра в конкуренции между процессами по поводу ресурсов.
Однако желательно иметь еще и возможность задействовать внутренний параллелизм, который может быть в самих процессах. Такой внутренний параллелизм встречается достаточно часто и его использование позволяет ускорить их решение. Например, некоторые операции, выполняемые приложением, могут требовать для своего исполнения достаточно длительного использования центрального процессора. В этом случае при интерактивной работе с приложением пользователь вынужден долго ожидать завершения заказанной операции и не может управлять приложением до тех пор, пока операция не выполнится до самого конца. Такие ситуации встречаются достаточно часто, например, при обработке больших изображений в графических редакторах. Если же программные модули, исполняющие такие длительные операции, оформлять в виде самостоятельных «подпроцессов» (легковесных или облегченных процессов — потоков, можно также воспользоваться термином задача'), которые будут выполняться параллельно с другими «подпроцессами» (потоками, задачами), то у пользователя появляется возможность параллельно выполнять несколько операций в рамках одного приложения (процесса). Легковесными эти задачи называют потому, что операционная система не должна для них организовывать полноценную виртуальную машину. Эти задачи не имеют своих собственных ресурсов, они развиваются в том же виртуальном адресном пространстве, могут пользоваться теми же файлами, виртуальными устройствами и иными ресурсами, что и данный процесс. Единственное, что им необходимо иметь, — это процессорный ресурс. В однопроцессорной системе треды (задачи) разделяют между собой процессорное время так же, как это делают обычные процессы, а в мультипроцессорной системе могут выполняться одновременно, если не встречают конкуренции из-за обращения к иным ресурсам.
Главное, что обеспечивает многопоточность, — это возможность параллельно выполнять несколько видов операций в одной прикладной программе. Параллельные вычисления (а следовательно, и более эффективное использование ресурсов центрального процессора, и меньшее суммарное время выполнения задач) теперь уже часто реализуется на уровне тредов, и программа, оформленная в виде нескольких тредов в рамках одного процесса, может быть выполнена быстрее за счет параллельного выполнения ее отдельных частей. Например, если электронная таблица или текстовый процессор были разработаны с учетом возможностей многопоточной обработки, то пользователь может запросить пересчет своего рабочего листа или слияние нескольких документов и одновременно продолжать заполнять таблицу или открывать для редактирования следующий документ.
Особенно эффективно можно использовать многопоточность для выполнения распределенных приложений; например, многопоточный сервер может параллельно выполнять запросы сразу нескольких клиентов. Как известно, операционная система OS/2 одной из первых среди ОС, используемых на ПК, ввела многопоточность. В середине девяностых годов для этой ОС было создано очень большое количество приложений, в которых использование механизмов многопоточной обработки реально приводило к существенно большей скорости выполнения вычислений.
Итак, сущность «поток» была введена для того, чтобы именно с помощью этих единиц распределять процессорное время между возможными работами. Сущность «процесс» предполагает, что при диспетчеризации нужно учитывать все ресурсы, закрепленные за ним. А при манипулировании тредами можно менять только контекст задачи, если мы переключаемся с одной задачи на другую в рамках одного процесса. Все остальные вычислительные ресурсы при этом не затрагиваются. Каждый процесс всегда состоит по крайней мере из одного потока, и только если имеется внутренний параллелизм, программист может «расщепить» один тред па несколько параллельных.
Потребность в потоках (threads) возникла еще па однопроцессорных вычислительных системах, поскольку они позволяют организовать вычисления более эффективно. Для использования достоинств многопроцессорных систем с общей памятью треды уже просто необходимы, так как позволяют не только реально ускорить выполнение тех задач, которые допускают их естественное распараллеливание, но и загрузить процессорные элементы работой, чтобы они не простаивали. Заметим, однако, что желательно, чтобы можно было уменьшить взаимодействие тредов между собой, ибо ускорение от одновременною выполнения параллельных потоков может быть сведено к минимуму из-за задержек синхронизации и обмена данными.
Каждый тред выполняется строго последовательно и имеет свой собственный программный счетчик и стек. Треды, как и процессы, могут порождать треды-потомки, поскольку любой процесс состоит по крайней мере из одного треда. Подобно традиционным процессам (то есть процессам, состоящим из одного треда), каждый тред может находится в одном из активных состояний. Пока один тред заблокирован (или просто находится в очереди готовых к исполнению задач), другой тред того же процесса может выполняться. Треды разделяют процессорное время так же, как это делают обычные процессы, в соотвешвии с различными вариантами диспетчеризации
Как мы уже знаем, все треды имеют одно и то же виртуальное адресное пространство своего процесса. Это означает, что они разделяют одни и те же глобальные переменные. Поскольку каждый тред может иметь доступ к каждому виртуальному адресу, один тред может использовать стек другого треда. Между потоками нет полной защиты, так как это, во-первых, невозможно, а во-вторых, не нужно. Все потоки одного процесса всегда решают общую задачу одного пользователя, и механизм потоков используется здесь для более быстрого решения задачи путем ее распараллеливания. При этом программисту очень важно получить в слое распоряжение удобные средства организации взаимодействия частей одной программы. Повторим, что кроме разделения адресного пространства, все треды разделяют также набор открытых файлов, используют общие устройства, выделенные процессу, имеют одни и те же наборы сигналов, семафоры и т. п. Л что у тредов будет их собственным? Собственными являются программный счетчик, стек, рабочие регистры процессора, потоки-потомки, состояние.
Вследствие того, что треды, относящиеся к одному процессу, выполняются в одном и том же виртуальном адресном пространстве, между ними легко организовать тесное взаимодействие, в отличие от процессов, для которых нужны специальные механизмы обмена сообщениями и данными. Более того, программист,
создающий многопоточное нриюжение, може! заранее продумать работу множества тредов процесса таким образом, чтобы они могли в ганмодействовать наиболее выгодным способом, а не участвовать в конкуренции за предосгавлепие ресурсов тогда, когда этою можно избежать.
Для того чтобы можно было эффективно организовать параллельное выполнение рассмотренных сущностей (процессов и тредов), в архитектуру современных процессоров включена возможность работать со специальной информационной структурой, описывающей ту или иную сущность. Для этого уже на уровне архитектуры микропроцессора используется понятие «задача» (task). Ойо как бы объединяет в себе обычный и «легковесный» процессы. Это понятие и поддерживаемая для него на уровне аппаратуры информационная структура позволяют в дальнейшем при разработке операционной системы построить соответствующие дескрипторы как для процесса, гак и для треда. Отличаться эти дескрипторы будут прежде всего тем, что дескриптор треда может хранить только контекст приостановленного вычислительного процесса, тогда как ЛескрЯИТор процесса (process) должен уже содержать поля, описывающие тем или иным способом ресурсы, выде теплые этому процессу. Другими словами, тот же task stale segment (сегмент состояния задачи), подробно рассматриваемый в разделе «Адресация в 32-разрядных микропроцессорах 180x86 при работе в защищенном режиме» главы 3, используется как основа для дескриптора процесса. Каждый тред (в случае использования так называемой «плоской» модули памяти — см. раздел «Поддержка страничного способа организации виртуальной памяти», глава 3 — может быть оформлен в виде самостоятельного сегмента, что приводит к тому, что простая (не многопоточная) программа будет иметь всего один ссчмент кода в виртуальном адресном пространстве.
В завершение можно привести несколько советов по использованию потоков при создании приложений, заимствованных из работы [55|.
В случае использования однопроцессорной системы множество параллельных потоков часто не ускоряет работу приложения, поскольку' в каждый отдельно взятый промежуток времени возможно выполнение только одною потока. Кроме того, чем больше у вас потоков, тем больше нагрузка на систему, потраченная па переключение между ними. Если ваш проект имеет более двух постоянно работающих потоков, то такая мучьтизадачиость не сделает программу быстрее, если каждый из потоков не будет требовать частого ввода/ вывода.
Вначале нужно попять, для чего необходим ноток Поток, осуществляющий обработку, может помешать системе быстро реагировать па запросы ввода/ вывода. Потоки позволяют программе отзываться па просьбы пользователя и устройств, ио при этом сильно загружать процессор. Потоки позволяют компьютеру одновременно обслуживать множество устройств, и созданный вамп ноток, отвечающий за обработку аТацифичетасого устройства в качестве минимума может потребовать столько времени, сколько системе необходимо для обработки запросов всех устройств
Потокам можно назначить Определенный приоритет для тою, чтобы наименее значимые процессы выполнялись в фоновом рйжиме. Эго путь честного
разделения ресурсов CPU*. Однако необходимо осознать тот факт, что процессор один на всех, а потоков много. Если в вашей программе главная процедура передает нечто для обработки в низкоприоритетный поток, то сама программа становится просто неуправляемой.
Потоки хороню работают, когда они независимы. Но они начинают работать непродуктивно, если вынуждены часто синхронизироваться для доступа к общим ресурсам. Ь юкировка и критические секции отнюдь нс увеличивают скорость работы системы, хотя без использования этих механизмов взаимо действующие вычисления организовывать нельзя.
Помните, что память виртуальна. Механизм виртуальной памяти (см. раздел «Память и отображения, виртуальное адресное пространство», глава 2) еле дит за тем, какая часть виртуального адресного пространства должна нахо диться в оперативной памяти, а какая должна быть сброшена в файл подкачки. Потоки усложняют ситуацию, если они обращаются в одно и го же время к разным адресам виртуального адресного пространства приложения. Это значительно увеличивает нагрузку на систему, особенно при небольшом объеме кэш-памяти. Помните, что реально память не всегда «свободна», как это пи шут в информационных «окошках» «О системе». Всегда отождествляйте дос туп к памяти с уступом к файлу на диске и создавайте приложение с учетом вышесказанного.
Всякий раз, когда какой-либо из ваших потоков пытается воспользоваться общим ресурсом вычислительного процесса, которому он принадлежит, вы обязаны тем пли иным образом легализовать и защитить свою деятельность Хорошим средством для этого являются критические секции, семафоры и очереди сообщений. Если вы протестировали свое приложение и нс обнаружили ошибок синхронизации, то это еще не значит, что их там нет. Пользователь может создать самые непредсказуемые ситуации Это очень ответственный момент в разработке многопоточных приложений.
Не возлагайте на поток несколько функций. Сложные функциональные отношения затрудняют понимание обшей структуры приложения, его алгоритм. Чем проще и менее многозначна каждая из рассматриваемых ситуаций, тем больше вероятность, что ошибок удастся избежать
Прерывания
Прерывания представляют собой механизм позволяющий координировать па- раллельпое функционирование отдельных устройств вычислительной системы и реагировать на особые состояния, возникающие при работе процессора. Таким образом, прерывание — это принудительная передача управления от выполняемой программы к системе (а через нее — к соответствующей программе обработки прерывания), происходящая при возникновении определенного события.
CPU (central processing unit) — центральное обрабатывающее устройство или просто про
цессор.
Идея прерываний была предложена в середине 50-х годов и можно без преувеличения сказать, что она внесла наиболее весомый вклад в развитие вычислительной техники. Основная цель введения прерываний — реализация асинхронного режима работы и распараллеливание работы отдельных устройств вычислительного комплекса.
Механизм прерываний реализуется аппаратно-программными средствами. Структуры систем прерывания (в зависимости от аппаратной архитектуры) могут быть самыми разными, но все они имеют одну общую особенность — прерывание непременно влечет за собой изменение порядка выполнения команд процессором. Механизм обработки прерываний независимо от архитектуры вычислительной системы включает следующие элементы:
Установление факта прерывания (прием сигнала на прерывание) и идентификация прерывания (в операционных системах иногда осуществляется повторно, па шаге 4).
Запоминание состояния прерванного процесса. Состояние процесса определяется прежде всего значением счетчика команд (адресом следующей команды, который, например, в i80x86 определяется регистрами CS и IP — указателем команды [2, 22, 84)), содержимым регистров процессора и может включать также спецификацию режима (например, режим пользовательский или привилегированный) и другую информацию.
Управление аппаратно передастся подпрограмме обработки прерывания В простейшем случае в счетчик команд заносится начальный адрес подпрограммы обработки прерываний, а в соответствующие регистры — информация из слова состояния. В более развитых процессорах, например в том же i80286 и последующих 32-битовых микропроцессорах, начиная с i80386, осуществляется достаточно сложная процедура определения начального адреса соответствующей подпрограммы обработки прерывания и не менее сложная процедура инициализации рабочих регистров процессора (см. раздел «Система прерываний 32-разрядных микропроцессоров i80x86», глава 3).
Сохранение информации о прерванной программе, которую не удалось спасти на шаге 2 с помощью действий аппаратуры. В некоторых вычислительных системах предусматривается запоминание довольно большого объема информации о состоянии прерванного процесса.
Обработка прерывания. Эта работа может быть выполнена той же подпрограммой, которой было передано управление на шаге 3, но в ОС чаще всего она реализуется путем последующего вызова соответствующей подпрограммы.
Восстановление информации, относящейся к прерванному процессу (этап, обратный шагу 4).
Возврат в прерванную программу.
Шаги 1-3 реализуются аппаратно, а шаги 4-7 — программно.
Па рис. 1.4 показано, что при возникновении запроса на прерывание естествен ный ход вычислений нарушается и управление передается программе обработки возникшего прерывания. При этом средствами аппаратуры сохраняется (как правило, с помощью механизмов стековой памяти) адрес той команды, с которой
следует продолжить выполнение прерванной программы. После выполнения программы обработки прерывания управление возвращается прерванной ранее программе посредством занесения в указатель команд сохраненного адреса команды Однако такая схема используется только в самых простых программных средах. В мультипрограммных операционных системах обработка прерываний происходит по более сложным схемам, о чем будет ботее подробно написано ниже.
Под рограмма обработки прерывания
Отключе иепрерываний, сохранениеконтекста прерваннойпрограммы, установкарежима работы системыпрерываний
Собственно телопрограммь обработкипрерывания
Восстановлениеконтекста прерваннойранее программы,установка прежнегорежима работысистемы прерываний
Рис. 1.4. Обработка прерывания
Итак, главные функции механизма прерываний:
распознавание или классификация прерываний;
передача управления соответственно обработчику прерываний;
корректное возвращение к прерванной программе.
Переход от прерываемой программы к обрабогчику и обратно должен выполняться как можно быстрей. Одним из быстрых методов является использование таблицы, содержащей перечень всех допустимых для компьютера прерываний и адреса соответствующих обработчиков. Для корректного возвращения к прерванной программе перед передачей управления обработчику прерываний содержимое регистров процессора запоминается либо в памяти с прямым доступом, либо в системном стеке — system stack.
Прерывания, возникающие при работе вычислительной системы, можно разделить на два основных класса: внешние (их иногда называют асинхронными) и внутренние (синхронные).
Внешние прерывания вызываются асинхронными событиями, которые происходят вис прерываемого процесса, например:
прерывания от таймера
прерывания от внешних устройств (прерывания по вводу/выводу).
прерывания по нарушению питания;
прерывания с пульта оператора вычислительной системы;
прерывания от другого процессора или другой вычислительной системы.
Внутренние прерывания вызываются событиями, которые связаны с работой процессора и являются синхронными с его операциями. Примерами являются следующие запросы па прерывания:
при нарушении адресации (в адресной части выполняемой команды указан запрещенный или несуществующий адрес, обращение к отсутствующему сегменту или странице при организации механизмов виртуальной памяти);
при наличии в поле кода операции иезадепствованноп двоичной комбинации;
при делении на нуль;
при переполнении пли исчезновении иорядка;
при обнаружении ошибок четности, ошибок в работе различных устройств аппаратуры средствами контроля.
Могут еще существовать прерывания при обращении к супервизору ОС — в некоторых компьютерах часть команд может использовать только ОС, а не пользователи. Соответственно в аппаратуре предусмотрены различные режимы работы, и пользовательские программы выполняются в режиме, в котором эти привилегированные команда нс исполняются. При попытке использовать команду, запрещенную в данном режиме, происходит внутреннее прерывание и управление передается супервизору ОС. К привилегированным командам относятся и команды переключения режима работа центрального процессора.
Наконец, существуют собственно программные прерывания. Эти прерывания происходят по соответствующей команде прерывания, то есть по этой команде процессор осуществляет практически те же действия, что и при обычных внутренних прерываниях. Данный механизм был специально введен для того, чтобы переключение на системные программные модули происходило нс просто как переход в подпрограмму, а точно таким же образом, как и обычное прерывание. Этим обеспечивается автоматическое переключение процессора в привилегированный режим с возможностью исполнения любых команд.
Сигналы, вызывающие прерывания, формируются вне процессора или в самом процессоре; они могут возникать одновременно. Выбор одного из них для обработки осуществляется на основе прморнтетов, приписанных каждому типу прерывания. Очевидно, что прерывания от схем контроля процессора должны обладать наивысшим приоритетом (если аппаратура работает неправильно, то не имеет смысла продолжать обработку информации) На рис. 1.5 изображен обычный порядок (приоритеты) обработки прерываний в зависимости от тина прерываний. Учет приоритета может быть встроен в технические средства, а также определяться операционной системой, то есть кроме аппаратно реализованных приоритетов прерывания большинство вычислительных машин и комплексов допускают программно-аппаратное управление порядком обработки сигналов прерывания. Второй способ, дополняя первый, позволяет применять различные дисциплины обслуживания прерываний.
Рис. 1.5. Распределение прерываний по уровням приоритетаРис. 1.5. Распределение прерываний по уровням приоритета
Рис. 1.5. Распределение прерываний по уровням приоритета
Рис. 1.5. Распределение прерываний по уровням приоритета
Наличие сигнала прерывания не обязательно должно вызывать прерывание исполняющейся программы. Процессор может обладать средствами зашиты от прерываний: отключение системы прерываний, маскирование (запрет) отдельных сигналов прерывания. Программное управление этими средствами (существуют специальные команда для управления работой системы прерываний) позволяет операционной системе регулировать обработку сигналов прерывания, заставляя процессор обрабатывать их сразу по приходу, откладывать их обработку па некоторое время или полностью игнорировать. Обычно операция прерывания выполняется только после завершения выполнения текущей команды. Поскольку сигналы прерывания возникают в произ вольные моменты времени, го па момент прерывания может существовать несколько сигналов прерывания, которые могут быть обработаны только последовательно. Чтобы обработать сигналы прерывания в разумном порядке им (как уже отмечалось) присваиваются приоритеты. Сигнал с более высоким приоритетом обрабатывается в первую очередь, обработка остальных сигналов прерывания откладывается.
Программное управление специальными регистрами маски (маскирование сигналов прерывания) повволяет реализовать различные дисциплины обслуживания:
с относительными приоритетами, то есть обслуживание не прерывается даже при наличии запросов с более высокими приоритетами. После окончания обслуживания данного запроса обслуживается запрос с наивысшим приоритетом. Для организации такой дисциплины необходимо в программе обслуживания данного запроса наложить маски на все остальные сигналы прерывания пли просто отключить систему прерываний;
с абсолютными приоритетами, то есть всегда обслуживается прерывание с наивысшим приоритетом. Для реангзации этого режима необходимо па время обработки прерывания замаскировать все запросы с более низким приоритетом. При этом возможно многоуровневое прерывание, то есть прерывание программ обработки прерываний Число уровней прерывания в этом режиме изменяется и зависит от приор^ота запроса;
О по принципу стека, или, как иногда говорят, по дисциплине LCFS (last come first served — последним пришел первым обслужен), то есть запросы с бо-
лее низким приоритетом могут прерывать обработку прерывания с более высоким приоритетом. Дли этого необходимо нс накладывать маски пи на один сигнал прерывания и не выключать систему прерываний.
Следует особо отметить, что для правильной реализации последних двух дисциплин нужно обеспечить полное маскирование системы прерываний при выполнении шагов 1-4 и 6-7. Это необходимо для того, чтобы не потерять запрос и правильно его обслужить. Многоуровневое прерывание должно происходить на этапе собственно обработки прерывания, а не на этапе перехода с одного папесса па другой.
Управление ходом выполнения задач со стороны ОС заключается в организации реакций па прерывания, в организации обмена информацией (данными и программами), предоставлении необходимых ресурсов, в динамике выполнения задачи и в организации сервиса. Причины прерываний определяет ОС (модуль, который называют супервизором прерываний), опа же и выполняет действия, необходимые при данном прерывании и в данной ситуации. Поэтому в состав любой ОС реального времени прежде всего входят программы управления системой прерываний, контроля состояний задач и событии, синхронизации задач, средства распределения памяти и управления ею, а уже потом средства организации данных (с помощью файловых систем и т. д.). Следует, однако, заметить, что современная ОС реального времени должна вносить в аппаратно-программный комплекс нечто большее, нежели просто обеспечение быстрой реакции па прерывания.
Как мы уже знаем, при появлении запроса па прерывание система прерываний идентифицирует сигнал и, если прерывания разрешены, управление передается на соответствующую подпрограмму обработки. Из рис. 1.4 видно, что в подпро грамме обработки прерывания имеются две служебные секции. Это — первая секция, в которой осуществляется сохранение контекста прерванной задачи, который не смог быть сохранен на 2-м шаге, и последняя, заключительная секция, в которой, наоборот, осуществляется восстановление контекста. Для того чтобы система прерываний не среагировала повторно па сигнал запроса на прерывание, она обычно автоматически «закрывает» (отключает) прерывания, поэтому необходимо потом в подпрограмме обработки прерываний вновь включать систему прерываний. Установка рассмотренных режимов обработки прерываний (с относительными и абсолютными приоритетами, и по правилу LCFS) осуществляется в конце первой секции подпрограммы обработки. Таким образом, па время выполнения центральной секции (в случае работы в режимах с абсолютными приоритетами" и по дисциплине LCFS) прерывания разрешены. На время работы заключительной секции подпрограммы обработки система прерываний должна быть отключена и после восстановления контекста вновь включена. Поскольку эти действия необходимо выполнять практически в каждой подпрограмме обработки прерываний, во многих операционных системах первые секции подпрограмм обработки прерывании выделяются в специальный системный программный модуль, называемый супервизором прерываний.
Супервизор прерываний прежде всего сохраняет в дескрипторе текущей задачи рабочие регистры процессора, определяющие контекст прерываемого вычислительного процесса. Далее он определяет ту подпрограмму, которая должна вы-
полнить действия, связанные с обслуживанием настоящего (текущего) запроса на прерывание. Наконец, перед тем как цередать управление этой подпрограмме, супервизор прерываний устанавливает необходимый режим обработки прерывания. После выполнения подпрограммы обработки прерывания управление вновь передается супервизору, па этот раз уже на тот модуль, который занимается диспетчеризацией задач (см. раздел «Качество диспетчеризации и гарантии обслуживания», глава 2). И уже диспетчер задач, в свою очередь, в соответствии с принятым режимом распределения процессорного времени (между выполняющимися процессами) восстановит контекст той задачи, которой будет решено выделить процессор. Рассмотренная памп схема проиллюстрирована па рис. 1.6.
Рис. 1.6. Обработка прерывания при участии супервизоров ОС
Как мы видим из рис. 1.6, здесь нет непосредственного возврата в прерванную ранее программу прямо из самой подпрограммы обработки прерывания. Для прямого непосредственного возврата достаточно адрес возврата сохранить в стеке, что и делает аппаратура процессора. При этом стек легко обеспечивает возможность возврата в случае вложенных прерываний, поскольку он всегда реализует дисциплину LCFS (last come — first served).
Однако если бы контекст процессов сохранялся просто в стеке, как это обычно реализуется аппаратурой, а не в описанных выше дескрипторах задач, то у пас ио было бы возможности гибко подходить к выбору тон задачи, которой нужно передать процессор после завершения работы подпрограммы обработки прерывания. Естественно, что эго только общий принцип. В конкретных процессорах и в конкретных ОС могут существовать некоторые отступления от рассмотренной схемы и/или дополнения к ней. Например, в современных процессорах часто имеются специальные аппаратные возможности для сохранения контекста пре-
рываемого процесса непосредственно в его дескрипторе, то есть дескриптор процесса (по крайней мере, его часть) становится структурой данных, которую поддерживает аппаратура.
Для полного понимания принципов создания и механизмов реализации рассматриваемых далее современных ОС необходимо знать архитектуру персональных компьютеров и, в частности, особенности системы прерывания. Этот вопрос бо- лее подробно рассмотрен в главе 4 «Управление вводом/выводом и файловые системы», посвященном архитектуре микропроцессоров i80x86.
Основные виды ресурсов
Рассмотрим кратко основные виды ресурсов вычислительной системы и способы их разделения (см. рис. 1.1) Прежде всего, одним из важнейших ресурсов является сам процессор, точнее — процессорное время. Процессорное время делится попеременно (параллельно). Имеется множество методов разделения этого ресурса (см. раздел «Планирование и диспетчеризация процессов и задач», глава 2).
Вторым видом ресурсов вычислительной системы можно считать память. Оперативная память может быть разделена и одновременным способом (то есть в памяти одновременно может располагаться несколько процессов или, по крайней мерс, текущие фрагменты, участвующие в вычислениях), и попеременно (в разные моменты оперативная память может предоставляться для разных вычислительных процессов). Память — очень интересный вид ресурса. Дело в том, что в каждый конкретный момент времени процессор при выполнении вычислений обращается к очень ограниченному числу ячеек оперативной памяти. С згой точки зрения желательно память разделять для возможно большего числа параллельно исполняемых процессов. С другой стороны, как правило, чем больше оперативной памя ти может быть выделено для конкретного текущего процесса, тем лучше будут условия для его выполнения. Поэтому проблема вффсктпвного разделения оперативной памяти между параллельно выполняемыми вычислительными процессами является одной из самых актуальных. Дос таточно подробно вопросы распределения памяти между параателык) выполняющимися процессами рассмотрены в главе 2 «Управление задачами и памятью в операционных системах».
Когда говорят о внешней памяти (например, память на магнитных дисках), то собственно память и доступ- к ней считаются разными видами ресурса. Каждый из этих ресурсов может предоставляться независимо от другого. Но для полной работы с внешней памятью необходимо иметь оба этих ресурса. Собственно внешняя память может разделяться одновременно, а доступ к ней — попеременно.
Если говорить о внешних устройствах, то они, как правило, мотут разделяться параллельно, если используются механизмы прямого доступа. Если же устроист во работает с последовательным доступом, то оно нс может считаться разделяе-
1 Разговор о процессоре как об одном из ресурсов более характерен для мультипроцессорных систем. В случае однопроцессорных систем чаще говорят о процессорном времени.
мым ресурсом. Простыми и наглядными примерами внешних устройств, которые не могут быть разделяемыми, являются принтер и накопитель на магнитной лепте. Действительно, если допустить, что принтер можно разделять между двумя процессами, которые смогут его использовать попеременно, то результаты печати, скорее всего, нс смогут быть использованы — фрагменты выведенного текста могут перемешаться таким образом, что в них невозможно будет разобраться. Аналогично обстоит дело и с накопителем на магнитной ленте. Если один процесс начнет что-то читать или писать, а второй при этом запросит перемотку лепты па се начало, то оба вычислительных процесса не смогут выполнить свои вы- числе.ппя.
Очень важным видом ресурсов являются программные модули. Прежде всего, мы будем рассматривать системные программные модули, поскольку именно они обычно и рассматриваются как программные ресурсы, и, в принципе, могут бы ть распределены между выполняющимися процессами.
Как известно, профаммные модули могут быть однократно и многократно (или повторно) используемыми. Однократно используемыми называют такие программные модули, которые могут быть правильно выполнены только один раз. Это означает, что в процессе своего выполнения они могут испортить себя: либо повреждается часть кода, либо — исходные данные, от которых зависит ход вычислений. Очевидно, что однократно используемые программные модули явля- югся неделимым ресурсом. Более того, их обычно вообще нс распределяют как ресурс системы. Системные однократно используемые программные модули, как правило, используются только на этапе загрузки ОС. При этом следует иметь в виду тот очевидный факт, что собственно двоичные файлы, которые обычно хранятся на системном диске, и в которых и записаны эти модули, не портятся, а потому могут быть повторно использованы при следующем запуске ОС.
Повторно используемые программные модули, в свою очередь, могут быть непривилегированными, привилегированными и реентерабельными.
Пршнывгировонные программные модули работают в так называемом привплсти- рованном режиме, то есть при отключенной системе прерываний (часто говорят, что прерывания закрыты), так, что никакие внешние события не могут нарушить естественный порядок вычислений. В результате программный модуль выполняется до своего конца, после чего он может быть вновь вызван па исполнение из другой задачи (другого вычислительного процесса). С позиций стороннего наблюдателя по отношению к вычислительным процессам, которые попеременно (причем, возможно, неоднократно) в течение срока своей «жизни» вызывают некоторый привилегированный программный модуль, такой модуль будет выступать как попеременно разделяемый ресурс. Структура привилегированных программных модулей изображена па рис. 1.7. Здесь в первой секции программного модуля выключается система прерываний. В поечедпей секции, напротив, включается система прерываний.
Непривилегированные программные модули — это обычные программные модули, которые могут быть прерваны во время своей работы. Следовательно, в общем случае их нельзя считать разделяемыми, потому что если после прерывания выполнения такого модуля, исполняемого в рамках одного вычислительного процесса, запустить его еще раз по гребованию другого вычислительного процесса,
то промежуточные результаты для прерванных вычислений могут быть потеря ны В противоположность этому, реентерабельные программные модули (reenterable) допускают повторное многократное прерывание своего исполнения и повторный их запуск по обращению из других задач (вычислительных процессов). Для этого реентерабельные программные модули должны быть созданы таким образом, чтобы было обеспечено сохранение промежуточных вычислений для прерываемых вычислений и возврат к ним, когда вычислительный процесс возобновляется с прерванной ранее точки. Это может быть реализовано двумя способами: с помощью статических и динамических методов выделения памяти под сохраняемые значения. Основной, наиболее часто используемый динамический — способ выделения памяти для сохранения всех промежуточных результатов вычисления, относящихся к реентерабельному программному модулю, может быть проиллюстрирован с помощью рис. 1.8
Отключение прерыванийОтключение прерыванийСобственно тело программного модуляСобственно тело программного модуляВключение прерыванийВключение прерываний
Отключение прерываний
Отключение прерываний
Собственно тело программного модуля
Собственно тело программного модуля
Включение прерываний
Включение прерываний
Рис. 1 7. Структура привилегированного программного модуля
Привилегированныймодуль,заказывающийв системной областипамяти блок ячеекдля хранениятекущих(промежуточных)данныхПривилегированныймодуль,заказывающийв системной областипамяти блок ячеекдля хранениятекущих(промежуточных)данныхОсновное тело реентерабельного программногомодуля, которое и может быть прерваноРаботает в непривилегированном режимеОсновное тело реентерабельного программногомодуля, которое и может быть прерваноРаботает в непривилегированном режимеПривилегированныймодуль,освобождающийв системной областипамяти блок памяти,использованьюдля храненияпромежуточных данныхПривилегированныймодуль,освобождающийв системной областипамяти блок памяти,использованьюдля храненияпромежуточных данных
Привилегированныймодуль,заказывающийв системной областипамяти блок ячеекдля хранениятекущих(промежуточных)данных
Привилегированныймодуль,заказывающийв системной областипамяти блок ячеекдля хранениятекущих(промежуточных)данных
Основное тело реентерабельного программногомодуля, которое и может быть прерваноРаботает в непривилегированном режиме
Основное тело реентерабельного программногомодуля, которое и может быть прерваноРаботает в непривилегированном режиме
Привилегированныймодуль,освобождающийв системной областипамяти блок памяти,использованьюдля храненияпромежуточных данных
Привилегированныймодуль,освобождающийв системной областипамяти блок памяти,использованьюдля храненияпромежуточных данных
ВершинастекаВершинастекаСистемная областьпамяти, используемаядинамическимобразом длябуферированноговвода, выводаи рее терабельнойобработки
Вершинастека
Вершинастека
Блок ячеек памятидля текущихпеременных
Стек
Рис. 1.8. Реентерабельный программный модуль
Основная идея построения и работы реентерабельного программного модуля, структура которого представлена на рис. 1.8, заключается в том, что в первой (головной) своей части с помощью обращения из системной привилегированной секции осуществляется запрос на получение в системной области памяти блока ячеек, необходимого для размещения всех текущих (промежуточных) данных. При этом на вершину стека помещается указатель на начало области данных и ее объем. Все текущие переменные реентерабельного программного модуля в этом случае располагаются в системной области памяти. Поскольку в конце привилегированной секции система прерываний включается, то во время рабепы центральной (основной) части реентерабельного модуля возможно ее прерывание. Если прерывание нс возникает, то в третьей (заключительной) секции осущсств ляется запрос па освобождение использованного блока системной области памяти. Если же но время работы центральной секции возникает прерывание и другой вычислительный процесс обращается к тому же самому реентерабельному программному модулю, то для этого нового процесса вновь заказывается новый блок памяти в системной области памяти, и на вершину стека записывается новый указатель. Очевидно, что возможно многократное повторное вхождение в реентерабельный программный модуль до тех пор, пока в области системной памяти, выделяемой специально для реентерабельной обработки, есть свободные ячейки, число которых достаточно для выделения нового блока.
Чго касается статического способа выделения памяти, то здесь речь может идти, например, о том, что заранее для фиксированного числа вычислительных процессов резервируются области памяти, в которых будут располагаться переменные реентерабельных программных модулей: для каждого процесса — своя область памяти. Чаще всего в качестве таких процессов выступают процессы ввода/вы- вода и речь идет о реентерабельных драйверах (реентерабельный драйвер может управлять параллельно несколькими однотипными устройствами. См. более подробно в главе 4 «Управление вводом/пыводом и файловые системы»).
Кроме реентерабельных программных модулей существуют еще повторно exodu- мыв (or re-entrance). Этим термином называют программные модули, которые тоже допускают свое многократное параллельное использование, по в отличие от реентерабельных их нельзя прерывать. Повторно входимые программные модули состоят из привилегированных секций и повторное обращение к ним возможно только после завершения какой-нибудь из таких секций. После выполнения очередной привилегированной секции управление может быть передано супервизору, и если он предоставит возможность выполняться другому процессу, то возможно повторное вхождение в рассматриваемый программный модуль. Другими словами, в повторно входимых программных модулях четко предопределены все допустимые (возможные) точки входа. Следует отметить, что повторно входимые программные модули встречаются гораздо чаще реентерабельных (повторно прерываемых).
Наконец, имеются и информационные ресурсы, то есть в качестве ресурсов могут выступать данные. Информационные ресурсы могут существовать как в виде переменных, находящихся в оперативной памяти, так и в виде файлов. Если процессы используют данные только для чтения, то такие информационные ресурсы можно разделять. Если же процессы могут яемемтять информационные ресурсы,
то необходимо специальным образом организовывать работу с такими данными Это одна из наиболее сложных проблем, достаточно подробно опа обсуждается в главе 6.
Классификация операционных систем
Широко известно высказывание, согласно которому любая паука начинается с классификации. Само собой, что вариантов классификации может быть очень много, все будет зависеть от выбранного признака, по которому один объект мы будем отличать от другого. Однако, что касается ОС, здесь уже давно сформировалось относительно небольшое количество классификаций: по назначению, по режиму обработки задач, по способу взаимодействия с системой и, наконец, по способам построения (архитектурным особенностям систем).
Во введении мы уже дали «определение» операционной системы (ОС). Поэтому просто повторим, что основным предназначением ОС является организация аффективных и надежных вычислении, создание различных интерфейсов для взаимодействия с этими вычислениями и с самой вычислительной системой.
Прежде всего, различают ОС общего и специального назначения. ОС специального назначения, в свою очередь, подразделяются на следующие: для переносимых микрокомпьютеров и различных встроенных систем, организации и ведения без данных, решения задач реального времени и т. п.
По режиму обработки задач различают ОС, обеспечивающие одиопрограммный и мультипрограммный режимы. Под мультипрограммированием понимается способ организации вычислений, когда па однопроцессорной вычислительной системе создается видимость одновременного выполнения нескольких программ. Любая задержка в решении программы (например, для осуществления операций ввода/вывода данных) используется для выполнения других (таких же, либо менее важных) программ Иногда при этом говорят о мультизадачном режиме. При этом, вообще говоря, мультипрограммный и мультизадачный режимы — это не синонимы, хотя и близкие понятия. Основное принципиальное отличие в этих терминах заключается в том, чго мультипрограммный режим обеспечивает параллельное выполнение нескольких приложений и при этом программисты, создающие эти программы, не должны заботиться о механизмах организации их параллельной работы Эти функции берет на себя сама ОС; именно она распределяет между выполняющимися при южеииями ресурсы вычислительной системы, осуществляет необходимую синхронизацию вычислений и взаимодействие Мультизадачный режим, наоборот, предполагает, что забота о параллельном выполнении и взаимодействии приложений ложится как раз на прикладных программистов. В современной технической и, чем более, научно-популярной литературе об этом различии часто забывают, тем самым внося некоторую путаницу Можно, однако, заметить, что современные ОС для ПК реализуют и мультипрограммный, и мультизадачный режимы
При организации работы с вычислительной системой в диалоговом режиме можно говорить об однойользоватеЛьских (однотермиипльных) и мулътитермииаль-
ных ОС. В мультитермииальных ОС с одной вычислительной системой одновременно могут работать несколько пользователей, каждый со своего терминала. При этом у пользователей возникает иллюзия, что у каждого из них имеется своя собственная вычислительная система. Очевидно, что для организации мульти- термииалыюго доступа к вычислительной системе необходимо обеспечить мультипрограммный режим работы. В качестве одного из примеров мулы "терминальных ОС для ПК можно назвать Linux.
Основной особенностью операционных систем реального времени (ОСРВ) является обеспечение обработки поступающих заданий в течение заданных интервалов времени, которые нельзя превышать. Поток заданий в общем случае не является планомерным и не может регулироватыя оператором (характер следования событии можно предсказать лишь в редких случаях), то есть задания поступают в непредсказуемые моменты времени и без всякой очередности. В ОС, не предназначенных для решения задач реальною времени, имеются некоторые накладные расходы процессорного времени па этапе инициирования (при выполнении которого ОС распознает все пожелания пользователей относительно решения своей задачи, загружает в оперативную память нужную программу и выделяет другие необходимые для ее выполнения ресурсы). В ОСРВ подобные затраты могу г отсутствовать, так как набор задач обычно фиксирован и вся информация о задачах известна еще до поступления запросов. Для подлинной реализации режима реального времени необходима (хотя этого и недостаточно) организация мультипрограммирования Мультипрограммирование является основным средством повышения производительности вычислительной системы, а для решения задач реального времени производительность становится важнейшим фактором. Лучшие характеристики по производительности для систем реального времени обеспечиваются однотермипальпыми ОСРВ. Средства организации мультитер- мппалыюго режима всегда замедляют работу системы в целом, но расширяют функциональные возможности системы. Одной нз наиболее известных ОСРВ для ПК является ОС QNX.
По основному архитектурному принципу ОС разделяются на микроядериые и монолитные. В некоторой степени это разделение тоже условно, однако можно в качестве яркого примера микроядерпой ОС привести ОСРВ QNX, тогда как в качестве монолитной можно назвать Windows 95/98 или ОС Linux. Ядро ОС Windows мы не можем изменить, нам не доступны его исходные коды и у нас пет программы для сборки (компиляции) этого ядра. А вот в случае с Linux мы можем сами собрать ядро, которое нам необходимо, включив в пего тс необходимые про- граммные модули и драйверы, которые мы считаем целесообразным включить именно в ядро (а не обращаться к ним из ядра).
Контрольные вопросы и задачи
Вопросы для проверки
Обл.ясшпс, в чем заключается различие между такими понятиями, как процесс и задача.
Изобразите диаграмму состояний процесса, поясните все возможные переходы из одного состояния в другое.
Объясните значения следующих терминов: task (задача), process (процесс), thread (поток, нить). Как они между собой соотносятся?
Для чего каждая задача получает соответствующий дескриптор? Какие поля, как правило, содержатся в дескрипторе процесса (задачи)? Что такое «контекст задачи»?
Объясните понятие ресурса. Почему понятие ресурса является одним из фундаментальных при рассмотрении ОС? Какие виды и типы ресурсов вы знаете?
Как вы считаете: сколько и каких списков дескрипторов задач может быть в системе? От чего должно зависеть это число?
Перечислите дисциплины обслуживания прерываний; объясните, как можно реализовать каждую из этих дисциплин
С какой целью в ОС вводится специальный системный модуль, иногда называемый супервизором прерываний?
В чем заключается различие между повторно-входимыми (re-entrance) и по- вторно-прерывасмыми (re-enterable) программными модулями? Как они реализуются?
Чго такое привилегированный программный модуль? Почему нельзя создать мультипрограммную ОС, в которой бы по было привилегированных программных модулей?
ГЛАВА 2 Управление задачами и памятью в операционных системах
Итак, время центрального процессора и оперативная память являются основными ресурсами в случае реализации мультипрограммных вычислений.
Оперативная память — это важнейший ресурс любой вычислительной системы, поскольку без нее (как, впрочем, и без центрального процессора) невозможно выполнение ни одной программы. Мы уже отмечали, что память является разделяемым ресурсом. От выбранных механизмов распределения памяти между выполняющимися процессорами очень сильно зависит и эффективность использования ресурсов системы, и ее производительность, и возможности, которыми moivt пользоваться программисты при создании своих программ. Способы распределения времени центрального процессора тоже сильно влияют и на скорость выполнения отдельных вычислений, и на общую эффективность вычислительной системы.
Понятие процесса (задачи) нам уже известно. В данной главе мы не будем стараться разделять понятия процесс (process) и поток (thread), вместо этого используя как бы обобщающий термин task (задача). В других разделах, если специально это не оговаривается, под задачей или процессом следует понимать практически одно и то же. Сейчас же мы будем говорить о разделении ресурса цен трального процессора, поэтому термин задача может включать в себя и понятие треда (потока).
Итак, операционная система выполняет следующие основные функции, связанные с управлением задачами:
создание и удаление задач;
планирование процессов и диспетчеризация задач;
оипхроинзация задач, обеспечение их средствами коммуникации.
Система управления задачами обеспечивает прохождение их через компьютер. В зависимости от состояния процесса ему должен быть предоставлен тог или иной ресурс. Например, новый процесс необходимо разместить в основной памяти — следовательно, ему необходимо выделить часть адресного пространства. Новы! порожденный поток текущего процесса необходимо включить в общий список задач, конкурирующих мооду собой за ресурсы центрального процессора. Создание п удаление задач осуществляется по соответствующим запросам от пользователей Или от самих задач. Задача может породить новую задачу. При этом между процессами появляются «родственные» отношения. Порождающая задача называется «предком», «родителем», а порожденная — «потомком», «сыном» или «дочерней задачей». «Предок» может приостановить или удалить свою дочернюю задачу, тогда как «потомок» нс может управлять «предком».
Основным подходом к организации того или иного метода управления процессами, обеспечивающего эффективную загрузку ресурсов или выполнение каких- либо иных целей, является организация очередей процессов и ресурсов.
Очевидно, что на распределение ресурсов влияют конкретные потребности тех задач, которые должны выполняться параллельно. Другими словами, можно столкну! ься с ситуациями, когда невозможно эффективно распределять ресурсы с тем, чтобы они ис простаивали. Например, всем выполняющимся процессам требуется некоторое устройство с последовательным доступом. Но поскольку, как мы уже знаем, оно не может распределяться между параллельно выполняющимися процессами, то процессы вынуждены будут очень долго ждать своей очереди. Таким образом, недоступность одного ресурса может привести к тому, что длительное время нс будут использоваться и многие другие ресурсы.
Если же мы возьмем набор таких процессов, которые нс будут конкурировать между собой за неразделяемые ресурсы при параллельном выполнении, то, скорее всего, процессы смогут выполниться быстрее (из-за отсугствия дополни гель- пых ожиданий), да и имеющиеся в системе ресурсы будут использоваться более эффективно. Итак, возникав! задача подбора такого множества процессов, что при выполнении они будут как можно реже конфликтовать из-за имеющихся в системе ресурсов. Такая задача называется планированием вычиежительных процессов.
Задача планирования процессов возникла очень давно — в первых пакетных ОС при планировании пакетов задач, которые должны были выполняться на компьютере и оптимально использовать его ресурсы. В настоящее время актуальность этой задачи нс так велика. На первый план уже очень давно вышли задачи динамического (или краткосрочного) планирования, то есть текущего наиболее эффективного распределения ресурсов, возникающего практически при каждом событии. Задачи динамического планирования стали называть диспетчеризацией.
Очевидно, что планирование осуществляется гораздо реже, чем задача текущего распределения ресурсов между уже выполняющимися процессами и потоками. Основное отличие между долгосрочным и краткосрочным планировщиками заключается в частоте запуска: краткосрочный планировщик, например, может запускаться каждые 30 или 100 мс, долгосрочный — один раз за несколько минут (или чаще; тут многое зависит от общей длительности решения заданий пользователей).
Долгосрочный планировщик решает, какой из процессов, находящихся во входной очереди, должен быть переведен в очередь готовых процессов в случае освобождения ресурсов памяти. Он выбирает процессы из входной очереди с целью создания неоднородной мультипрограммной смеси. Это означает, что в очереди готовых к выполнению процессов должны находиться — в разной пропорции — как процессы, ориентированные па ввод/вывод, так и процессы, ориентированные па преимущественную работу с центральным процессором.
Краткосрочный планировщик решает, какая из задач, находящихся в очереди готовых к выполнению, должна быть передана па исполнение. В большинстве современных операционных систем, с которыми мы сталкиваемся, долгосрочный планировщик отсутствует.
Планирование и диспетчеризация процессов и задач
Стратегии планирования
Прежде всего следует отмстить, что при рассмотрении стратегий планирования, как правило, идет речь о краткосрочном планировании, то есть о диспетчеризации. Долгосрочное планирование, как мы уже отметили, заключается в подборе таких вычисуиггельных процессов, которые бы меньше всего конкурировали между собой за ресурсы вычислительной системы.
Стратегия планирования определяет, какие процессы мы планируем на выполнение для того, ч । обы достичь поставленной цели. Известно большое количество различных стратегий выбора процесса, которому необходимо предоетавт ь процессор. Среди них, прежде всего, можно назвать следующие стратегии:
по возможности заканчивать вычисления (вычислительные процессы) в том же самом порядке, в котором они были начаты;
отдавать предпочтение более коротким процессам;
предоставлять всем пользователям (процессам пользователей) одинаковые услуги, в том числе и одинаковое время ожидания.
Когда говорят о стратегии обслуживания, всегда имеют в виду понятие процесса, а не понятие задачи, поскольку процесс, как мы уже знаем, может состоять из нескольких потоков (задач).
Дисциплины диспетчеризации
Когда говорят о диспетчеризации, то всегда в явном или неявном виде имеют в виду понятие задачи (потока). Если ОС не поддерживает механизм тредов, то можно заменять понятие задачи па понятие процесса. Так как эти термины часто использую i ся именно в таком смысле, мы вынуждены будем использовать термин «процесс» как синоним термина «задача».
Известно большое количество правил (дисциплин диспетчеризации), в соответствии с которыми формируется список (очередь) готовых к выполнению задач. Различают два больших класса дисциплин обслуживания — бесприоритетные и приоритетные. При бесприоритетпом обслуживании выбор задачи производится в некотором заранее установленном порядке без учета их относительной важности и времени обслуживания. При реализации приоритетных дисциплин обслуживания отдельным задачам предоставляется преимущественное право попасть в состояние исполнения. Перечень дисциплин обслуживания и их классификация приведены на рис. 2.1.
Рис. 2.1. Дисциплины диспетчеризации
Запомним о приоритетах следующее:
приоритет, присвоенный задаче, может являться величиной постоянной;
приоритет задачи может изменяться в процессе ее решения.
Диспетчеризация с динамическими приоритетами требует дополнительных расходов па вычисление значений приоритетов исполняющихся задач, поэтому во многих ОС реального времени используются методы диспетчеризации па основе статических (постоянных) приоритетов. Хотя надо заметить, что динамические приоритеты позволяют реализовать гарантии обслуживания задач.
Рассмотрим кратко некоторые основные (наиболее часто используемые) дисциплины диспетчеризации.
Самой простой в реализации является дисциплина FCFS (first come — first served), согласно которой задачи обслуживаются «в порядке очереди», то есть в порядке их появления. Тс задачи, которые были заблокированы в процессе работы (попали в какое-либо из состояний ожидания, например, из-за операций ввода/выво- да), после перехода в состояние готовности ставятся в эту очередь готовности перед теми задачами, которые еще не выполнялись. Другими словами, образуются две очереди (рис. 2.2): одна очередь образуется из новых задач, а вторая очередь — из ранее выполнявшихся, но попавших в состояние ожидание. Такой подход позволяет реализовать стратегию обслуживания «по возможности заканчивать вычисления в порядке их появления». Эта дисциплина обслуживания не требует внешнего вмешательства в ход вычислений, при пей не происходит перераспределение процессорного времени. Существующие дисциплины диспетчеризации процессов могут быть разбиты на два класса — вытесняющие (preemptive) и не вытесняющие (non-preemptive). В первых пакетных ОС часто реализовывали параллельное выполнение заданий без принудительного перераспределения процессора между задачами. В большинстве современных ОС для мощных вычислительных систем, а также и в ОС для ПК, ориентированных на высокопроизводительное выполнение приложений (Windows NT, OS/2, Linux), реализована вытесняющая многозадачность. Можно сказать, что рассмотренная дисциплина относится к не вытесняющим.
Рис. 2.2. Дисциплина диспетчеризации FCFS
К достоинствам этой дисциплины, прежде всего, можно отнести простоту реализации и малые расходы системных ресурсов на формирование очереди задач.
Однако эта дисциплина приводит к тому, что при увеличении загрузки вычисли тельной системы растет и среднее время ожидания обслуживания, причем короткие задания (требующие небольших затрат машинного времени) вынуждены ожидать столько же, сколько и трудоемкие задания. Избежать этого недостатка позволяют дисциплины SJN и SRT.
Дисциплина обслуживания SJN (shortest job next, что означает: следующим будет выполняться кратчайшее задание) требует, чтобы для каждого задания бы ла известна оценка в потребностях машинного времени. Необходимое гь сообщать ОС характернстки задач, в которых описывались бы потребности в ресурсах вычислительной системы, привела к тому, что были разработаны соответствующие языковые- средства. В частности, язык JCL (Job control language, язык управления заданиями) бы т одним из наиболее известных. Пользователи вынуждены были указывать предполагаемое время выполнения, и для того, чтобы они нс злоупотребляли возможностью указать заведомо меньшее время выполнения (с целью получигь результаты раньше других) ввели подсчет реальных потребное гей. Диспетчер задач сравнивал заказанное время и время выполнения, и в случае превышения указанной оценки в данном ресурсе ставил данное задавшие в начало, а в конец очереди. Еще в некоторых ОС в таких случаях использовалась система штрафов, при которой в случае превышения заказанного машинного времени оплата вычислительных ресурсов осуществлялась уже ио другим расцепкам.
Дисциплина обслуживания SJN предполагает, что имеется только одна очередь заданий, готовых к выполнению. И задания, которые в процессе своего исполнения были временно заблокированы (например, ожидали завершения операций ввода/вывода), вновь понадают в конец очереди готовых к выполнению наравне с вновь поступающими. Это приводит к тому, что задания, которым требуется очень немного времени для своего завершения, вынуждены ожидать процессор наравне с длительными работами, что не всегда хорошо.
Для устранения этого недостатка и была предложена дисциплина SRT (shortest remaining time, следующее задание требует меньше всего времени для своего завершения).
Все эти три дисциплины обслуживания могут использоваться для пакетных режимов обработки, когда пользователь не вынужден ожидать реакции системы, а просто сдает свое задание и через несколько часов получает свои результаты вычислений Для интерактивных же вычислении желательно прежде всего обеспечить приемлемое время реакции системы и равенство в обслуживании, если система является мулыптермнпатьной. Если же эго однопользовательская сие тема, ио с возможностью мультипрш раммной обработки, то желательно, чтобы те программы, с которыми мы сейчас непосредственно работаем, имели лучшее время реакции, нежели наши фоновые задания. При этом мы можем пожелать, чтобы некоторые притожепия выполняясь без нашего непосредственного участия (например, программа получения электронной почты, использующая модем и коммутируемые линии для передачи данных), тем не менее гарантированно получали необходимую им долю процессорного времени. Для решения подобных
проблем используется дисциплина обслуживания, называемая RR (round robin, круговая, карусельная), и приоритетные методы обслуживания.
Дисциплина обслуживания RR предполагает, что каждая задача получает процессорное время порциями (говорят: квантами времени', q\ После окончания кванта времени q задача снимается с процессора и он передается следующей задаче. Снятая задача ставится в конец очереди задач, готовых к выполнению. Эта дисциплина обслуживания иллюстрируется рис. 2.3. Для оптимальной работы системы необходимо правильно выбрать закон, по которому кванты времени выделяются задачам.
Выполненные задачи
Очередь готовых к исполнению задач Новые задачи
Рис 2 3. Карусельная дисциплина диспетчеризации (round robin)
Величина кванта времени q выбирается как компромисс между приемлемым временем реакции системы па запросы пользователей (с тем чтобы их простейшие запросы не вызывали длительного ожидания) и накладными расходами на частую смену контекста задач. Очевидно, что при прерываниях ОС вынуждена сохранить достаточно большой объем информации о текущем (прерываемом) процессе, поставить дескриптор снятой задачи в очередь, загрузить контекст задачи, которая теперь будет выполняться (ее дескриптор был первым в очереди готовых к исполнению). Если величина q велика, то при увеличении очереди готовых к выполнению задач 1>сакция системы станет плохой. Если же величина мала, то относительная доля накладных расходов на переключения между исполняющимися задачами станет большой и это ухудшит производительность системы. В некоторых ОС есть возможность указывать в явном виде величину q либо диапазон ее возможных значений, поскольку система будет стараться выбирать оптимальное значение сама.
Например, в OS/2 в файле CONFIG.SYS есть возможность с помощью оператора TIMESLICE указать минимальное и максимальное значение для кванта q. Так, например, строка TIMESLICE=32.256 указывает, что минимальное значение q равно 32 миллисекундам, а максимальное — 256. Если некоторая задача (тред) была пре рвана, поскольку выделенный ей квант времени q израсходован, то следующий выделенный ему интервал будет увеличен на время, равное одному периоду таймера (около 32 мс), и так до тех пор, пока квант времени не станет равным максимальному значению, указанному в операторе TIMESLICE. Этот метод позволяет
1 Time slice квант времени.
OS/2 уменьшить накладные расходы на переключение задач в том случае, если несколько задач параллельно выполняют длительные вычисления.
Дисциплина диспетчеризации RR — это одна из самых распространенных дисциплин. Однако бывают ситуации, когда ОС не поддерживает в явном виде дисциплину карусельной диспетчеризации. Например, в некоторых ОС реального времени используется диспетчер задач, работающий по принципам абсолютных приоритетов (процессор предоставляется задаче с максимальным приоритетом, а при равенстве приоритетов он действует по принципу очередности) [21] Другими словами, снять задачу с выполнения можег только появление задачи с более высоким приоритетом. Поэтому если нужно организовать обслуживание задач таким образом, чтобы все они получали процессорное время равномерно и равноправно, то системный оператор может сам организовать эту дисциплину. Для этого достаточно всем пользовательским задачам присвоить одинаковые приоритеты и создать одну высокоприоритетную задачу, которая не должна ничего делать, но которая, тем нс менее, будет по таймеру (через указанные интервалы времени) планироваться на выполнение. Эта задача снимет с выполнения текущее приложение, оно будет поставлено в конец очереди, и поскольку этой высокоприоритетной задаче на самом деле ничего делать не надо, то она тут же освободит процессор и из очереди готовности будет взята следующая задача.
В своей простейшей реализации дисциплина карусельной диспетчеризации предполагает, что все задачи имеют одинаковый приоритет. Если же необходимо ввести механизм приоритетного обслуживания, то это, как правило, делается за счет организации нескольких очередей. Процессорное время будет предоставляться в первую очередь тем задачам, которые стоят в самой привилегированной очереди. Если она пустая, то диспетчер задач начнет просматривать остальные очереди. Именно по такому алгоритму действует диспетчер задач в операционных системах OS/2 и Windows NT.
Вытесняющие и не вытесняющие алгоритмы диспетчеризации
Диспетчеризация без перераспределения процессорного времени, то есть не вытесняющая .многозадачность (non-preemptive multitasking) — это такой способ диспетчеризации процессов, при котором активный процесс выполняется до тех пор, пока он сам, что называется «по собственной инициативе», не отдаст управление диспетчеру задач для выбора из очереди другого, готового к выполнению процесса или треда. Дисциплины обслуживания FCFS, SJN, SRT относятся к не вытесняющим.
Диспетчеризация с перераспределением процессорного времени между задачами, то есть вытесняющая многозадачность (preemptive multitasking) — это такой способ, при котором решение о переключении процессора с выполнения одного процесса на выполнение другого процесса принимается диспетчером задач, а не самой активной задачей. При вытесняющей многозадачности механизм диспетчеризации задач целиком сосредоточен в операционной системе, и программист может писать свое приложение, нс заботясь о том, как оно будет выполняться параллельно с другими задачами. При этом операционная система выполняет следующие функции: определяет момент снятия с выполнения текущей задачи,
сохраняет се контекст в дескрипторе задачи, выбирает из очереди готовых задач следующую и запускает се на выполнение, предварительно зафузив ее контекст. Дисциплина RR и многие другие, построенные на се основе, относятся к вытесняющим.
При не вытесняющей многозадачности механизм распределения процессорного времени распределен между системой и прикладными программами. Прикладная программа, получив управление от операционной системы, сама определяет момент завершения своей очередной итерации и передает управление суперви зору ОС с помощью соответствующего системного вызова. При атом естественно, что диспетчер задач, так же как и в случае вытесняющей мультизадач пости, формирует очереди задач и выбирает в соответствии с некоторым алгоритмом (например, с учетом порядка поступления задач или их приоритетов) следующую задачу на выполнение. Такой механизм создает некоторые проблемы как для пользователей, так и для разработчиков.
Для пользователей это означает, что управление системой может теряться на некоторый произвольный период времени, который определяется процессом выполнения приложения (а не системой, старающейся всегда обеспечить приемлемое время реакции на запросы пользователей) [56]. Если приложение тратит слишком много времени на выполнение какой-либо работы (например, на форматирование диска), пользователь не может переключиться с этой задачи па другую задачу (например, на текстовый или графический редактор, в то время как форматирование продолжалось бы в фоновом режиме). Эта ситуация нежелательна, так как пользователи обычно нс хотят долго ждать, когда машина завершит свою задачу.
Поэтому разработчики приложений для не вытесняющей операционной среды, возлагая на себя функции диспетчера задач, должны создавать приложения так, чтобы они выполняли свои задачи небольшими частями. Так, упомянутая выше программа форматирования может отформатировать одну дорожку дискеты и вернуть управление системе. После выполнения других задач система возвратит управление программе форматирования, чтобы та отформатировала следующую дорожку. Подобный метод разделения времени между задачами работает, по он существенно затрудняет разработку программ и предъявляет повышенные требования к квалификации программиста.
Например, в ныне уже забытой операционной среде Windows 3.x нативные приложения этой системы разделяли между собой процессорное время именно таким образом. И программисты сами должны были обеспечивать «дружественное» отношение своей программы к другим выполняемым одновременно с ней программам, достаточно часто отдавая управление ядру системы. Крайним про явлением «не дружественности» приложения является его зависание, которое приводит к общему краху системы. В системах с вытесняющей многозадачностью такие ситуации, как правило, исключены, так как центральный механизм диспетчеризации, во-первых, обеспечивает все задачи процессорным временем а во-вторых, дает возможность иметь надежные механизмы для мониторинга вычислений и позволяет снять зависшую задачу с выполнения.
Однако распределение функций диспетчеризации между системой и приложениями не всегда является недостатком, а при определенных условиях может
быть и преимуществом, потому что дает возможность разработчику приложений самому проектировать алгоритм распределения процессорного времени, наиболее подходящий для данного фиксированного набора задач [56|. Гак как разработчик сам определяет в программе момент времени отдачи управления, то при этом исключаются нерациональные прерывания программ в «неудобные» для них моменты времени Кроме того, легко разрешаются проблемы совместного использования данных; задача во время каждой итерации использует их монопольно и уверена, что на протяжении этого периода никто другой не изменит эти данные. Примером эффективного использования не вытесняющей многозадачности является сетевая операционная система Novell NetWare, в которой в значительной степени благодаря этому достигнута высокая скорость выполнения файловых операций. Менее удатым оказалось использование не вытесняющей многозадачности в операционной среде Windows 3.x.
Качество диспетчеризации и гарантии обслуживания
Одна из проблем, которая возникает при выборе подходящей дисциплины об служивания, — это гарантия обслуживания. Дело в том, что при некоторых дисциплинах, например при использовании дисциплины абсолютных приоритетов, низкоприоритетные процессы оказываются обделенными многими ресурсами и прежде всего, процессорным временем Возникает реальная дискриминация низкоприоритетных задач, и ряд таких процессов, имеющих к тому же большие потребности в ресурсах, могут очень длительное время откладываться или, в конце концов, вообще могут быть не выполнены. Известны случаи, когда вследствие высокой загрузки вычислительной системы отдельные процессы так и не были выполнены, несмотря на то, что прошло несколько лет (!) с момента их планирования Поэтому вопрос гарантии обслуживания является очень актуальным.
Болес жестким требованием к системе, чем просто гарантированное завершение процесса, является его гарантированное завершение к указанному моменту времени или за указанный интервал времени. Существуют различные дисциплины диспетчеризации, учитывающие жесткие временные ограничения, но нс существует дисциплин, которые могли бы предоставить больше процессорного времени, чем может быть в принципе выделено
Планирование с учетом жестких временных ограничений легко реализовать, организуя очередь готовых к выполнению процессов в порядке возрастания их временных ограничении. Основным недостатком такого простого упорядочения является то, что процесс (за счет других процессов) может быть обслужен быст рее, чем это ему реально необходимо. Для того чтобы избежать этого, проще всего процессорное время выделять все-таки квантами. Гарантировать обслуживание можно следующими тремя способами:
выделять минимальную долю процессорного времени некоторому классу про- цесоов, если по крайней мере один из них готов к исполнению. Например, можно отводить 20 % от каждых 10 мс процессам реального времени, 40 % от каждых 2с — пнИфакгивпмм процессам и 10 % от каждых 5 мин — пакетным (фоновым) процессам;
выделять минимальную долю процессорного времени некоторому конкретному процессу, если он готов к выполнению;
выделять столько процессорного времени некоторому процессу, чтобы он мог выполнить свои вычисления к сроку.
Для сравнения алгоритмов диспетчеризации обычно испольвуются следующие критерии:
Использование (загрузка) центрального процессора (CPU utilization). В большинстве персональных систем средняя загрузка процессора не превышает 2-3 %, доходя в моменты выполнения сложных вычислений и до 100 % В реальных системах, где компьютеры выполняют очень много работы, например, в серверах, загрузка процессора колеблется в пределах 15 40 % для легко загруженного процессора и до 90-100 % — для сильно загруженного процессора.
Пропускная способность (CPU throughput). Пропускная способность процессора может измеряться количеством процессов, которые выполняются в единицу времени.
Время оборота (turnaround time). Для некоторых процессов важным критерием является полное время выполнения, то есть интервал от момента появления процесса во входной очереди до момента его завершения. Это время названо временем оборота и включает время ожидания во входной очереди, время ожидания в очереди готовых процессов, время ожидания в очередях к оборудованию, время выполнения в процессоре и время ввода/вывода.
Время ожидания (waiting time). Под временем ожидания понимается суммарное время нахождения процесса в очереди готовых процессов.
Время отклика (response time). Для гиперактивных программ важным показателем является время отклика или время, прошедшее от момента попадания процесса во входную очередь до момента первого обращения к терминалу. Очевидно, что простейшая стратегия краткосрочного планировщика должна быть направлена на максимизацию средних значений загруженности и пропускной способности, времени ожидания и времени отклика.
Правильное планирование процессов сильно влияет па производительность всей системы. Можно выделить следующие главные причины, приводящие к уменьшению производительности системы:
Накладные расходы на переключение процессора. Опп определяются не только переключениями контекстов задач, но и (при переключении па процессы другого приложения) перемещениями страниц виртуальной памяти, а также необходимостью обновления данных в кэше (коды и данные одной задачи, находящиеся в кэше, нс нужны другой задаче и будут заменены, что приводит к дополнительным задержкам).
Переключение на другой процесс в тот момент, когда текущий процесс выполняет критическую секцию, а другие процессы активно ожидают входа в свою кригическую секцию (см. главу 6). В этом случае потери будут особо велики (хотя вероятность прерывания выполнения коротких критических секций мала).
В случае использования мультипроцессорных систем применяются следующие методы повышения производительности системы:
совместное планирование, при котором все потоки одного приложения (не- блокированные) одновременно выбираются для выполнения процессорами и одновременно снимаются с них (для сокращения переключений контекста);
планирование, при котором находящиеся в критической секции задачи не прерываются, а активно ожидающие входа в критическую секцию задачи не выбираются до тех пор, пока вход в секцию не освободится;
планирование с учетом так называемых «советов» программы (во время ее выполнения). Например, в известной своими новациями ОС Mach имелись два класса таких советов (hints) — указания (разной степени категоричности) о снятии текущего процесса с процессора, а также указания о том процессе, который должен быть выбран взамен текущего.
Диспетчеризация задач с использованием динамических приоритетов
При выполнении программ, реализующих какие-либо задачи контроля и управления (что характерно, прежде всего, для систем реального времени), может случиться такая ситуация, когда одна или несколько задач не могут быть реализованы (решены) в геченис длительного промежутка времени из-за возросшей нагрузки в вычисли тельной системе. Потери, связанные с невыполнением таких задач, могу г оказаться больше, чем потери от невыполнения программ с более высоким приоритетом. При этом оказывается целесообразным временно изменить приоритет «аварийных» задач (для которых истекает отпущенное для них время обработки). После выполнения этих задач их приоритет восстанавливается. Поэтому почти в любой ОС реального времени имеются средства для изменения приоритета программ*. Есть такие средства и во многих ОС, которые нс относятся к классу ОСРВ. Введение механизмов динамического изменения приоритетов позволяет реализовать более быструю реакцию системы на короткие запросы пользователей, что очень важно при интерактивной работе, но при этом гарантировать выполнение любых запросов.
Рассмотрим, например, как реализован механизм динамических приоритетов в ОС UNIX, которая, как известно, не относится к ОСРВ. Приоритет процесса вычисляется следующим образом [70]. Во-первых, в вычислении участвуют значения двух полей дескриптора процесса — p_nice и р сри. Первое из них назначается пользователем явно или формируется по умолчанию с помощью системы программирования. Второе поле формируется диспетчером задач (планировщиком разделения времени) и называется системной составляющей или текущим приоритетом. Другими словами, каждый процесс имеет два атрибута приоритета, с учетом которого и распределяется между исполняющимися задачами процессорное время: текущий приоритет, па основании которого происходит планирование, и заказанный относительный приоритет, называемый nice number (или просто nice).
Dynamic priority variation.
Схема нумерации (числовых значений) текущих приоритетов различна для различных версий UNIX. Например, более высокому значению текущею приоритета может соответствовать более низкий фактический приоритет планирования. Разделение между приоритетами режима ядра и задачи также зависит от версии. Рассмотрим частный случай, когда текущий приоритет процесса варьируется в диапазоне от 0 (низкий приоритет) до 127 (наивысший приоритет) Процессы выполняющиеся в режиме задачи, имеют более низкий приоритет чем в режиме ядра. Для режима задачи приоритет меняется в диапазоне 0- 65 для режима ядра — 66-95 (системный диапазон). Процессы, приоритеты которых лежат в диапазоне 96-127, являются процессами с фиксированным приоритетом, не изменяемым операционной системой, и предназначены для поддержки приложений реального времени.
Процессу, ожидающему недоступного в данный момент ресурса, система определяет значение приоритета сна, выбираемое ядром из диапазона системных приоритетов и связанное с событием, вызвавшее это состояние Когда процесс пробуждается, ядро устанавливает значение текущего приоритета процесса равным приоритету сна. Поскольку приоритет такого процесса находится в системном диапазоне и выше, чем приоритет режима задачи, вероятность предоставления процессу вычислительных ресурсов весьма велика. Такой подход позволяет, в частности, быстро завершить системный вызов, выполнение которою, в свою очередь, может блокировать некоторые системные ресурсы.
После завершения системного вызова перед возвращением в режим задачи яд ро восстанавливает приоритет режима задачи, сохраненный перед выполнением системного цызова. Это может привести к понижению приоритета, что в свою очередь, вызовет переключение контекста.
Текущий приоритет процесса в режиме задачи р priuser как мы только что отметили, зависит от значения nice number и степени использования вы шелитель- ных ресурсов р сри:
p priuser - a*p_nice - b*p_cpu
Задача планировщика разделения времени — справедливо распределить вычислительный ресурс между конкурирующими процессами Для принятия решения о выборе следующего запускаемого процесса планировщику необходима информация об использовании процессора. Эта составляющая приоритета уменьшается обработчиком прерываний таймера по каждому «тику» таймера. Таким обра зом, пока процесс выполняется в режиме задачи, его текущий приоритет линейно уменьшается.
Каждую секунду ядро пересчитывает текущие приоритеты процессов, готовых к запуску (приоритеты которых меньше некоторого порогового значения, в пашем примере эта величина равна 65), последовательно увеличивая их. Эго осуществляется за счет того, что ядро последовательно уменьшает отрицательную компоненту времени использования процессора Как результат, эти действия приводят к перемещению процессов в более приоритетные очереди и повышают вероятность их последующего запуска
Возможно использование следующей формулы:
р сри - р_сри/2
Это правило проявляет недостаток нивелирования приоритетов при повышении загрузки системы. Происходит это потому, что в таком случае каждый процесс получает незначительный объем вычислительных ресурсов и, следовательно, имеет малую составляющую рсри, которая еще более уменьшается благодаря формуле пересчета величины р сри. В результате степень использования процессора перестает оказывать заметное влияние па приоритет, и низкоприоритетные процессы (то есть процессы с высоким значением nice number) практически «отлучаются» от вычислительных ресурсов системы.
В некоторых версиях ОС UNIX для пересчета значения р сри используется другая формула:
p_cpu - p_cpu*(2*load)/(2*load+l)
Здесь параметр load равен среднему числу процессов, находившихся в очереди па выполнение за последнюю секунду, и характеризует среднюю загрузку системы за этот период времени. Такой алгоритм позволяет частично избавиться от недостатка планирования по формуле р сри = р_сри/2, поскольку при значительной загрузке системы уменьшение р сри при пересчете будет происходить медленнее.
Описанные алгоритмы планирования позволяют учесть интересы низкоприоритетных процессов, так как в результате длительного ожидания очереди па запуск приоритет таких процессов увеличивается, соответственно увеличивается и вероятность запуска. Эти алгоритмы также обеспечивают более вероятный выбор планировщиком интерактивных процессов по отношению к вычислительным (фоновым). Такие задачи, как командный интерпретатор или редактор, большую часть времени проводят в ожидании ввода, имея, таким образом, высокий приоритет (приоритет спа). При наступлении ожидаемого события (например, пользователь осуществил ввод данных) им сразу же предоставляются вычисли тельные ресурсы. Фоновые процессы, потребляющие значительные ресурсы процессора, имеют высокую составляющую р сри и, как следствие, менее высокий приоритет. Аналогичные механизмы имеют место и в таких ОС, как OS/2 или Windows NT. Правда, алгоритмы изменения приоритета задач в этих системах иные. Например, в Windows NT каждый поток (тред) имеет базовый уровень приоритета, который лежит в диапазоне от двух уровней ниже базового приоритета процесса, его породившего, до двух уровней выше этого приоритета, как показано па рис. 2.4. Базовый приоритет процесса определяет, сколь сильно могут различаться приоритеты потоков процесса и как они соотносятся с приоритетами потоков других процессов. Поток наследует этот базовый приоритет и может изменять его так, чтобы он стал немного больше или немного меньше. В результате получается приоритет планирования, с которым ноток и начинает исполняться. В процессе исполнения потока его приоритет может отклоняться от базового.
На рис. 2.4 показан динамический приоритет потока, нижней границей которого является базовый приоритет потока, а верхняя — зависит от вида работ, исполняемых потоком. Например, если поток обрабатывает пользовательский ввод, то диспетчер задач Windows NT поднимает его динамический приоритет; если же он выполняет вычисления, то диспетчер постепенно снижает его приоритет до базового. Снижая приоритет одного процесса и поднимая приоритет
другого, подсистемы могут управлять относительной приоритетностью потоков внутри процесса
Приоритет
15413121110987654321541312111098765432ДиапазонзначенийдинамическогоприоритетапотокаДиапазонзначенийдинамическогоприоритетапотокаБазовый приоритетпроцессаБазовый приоритетпроцессаБазовыйприоритетпотокаБазовыйприоритетпотока
15
4
13
12
11
10
9
8
7
6
5
4
3
2
15
4
13
12
11
10
9
8
7
6
5
4
3
2
Диапазонзначенийдинамическогоприоритетапотока
Диапазонзначенийдинамическогоприоритетапотока
Базовый приоритетпроцесса
Базовый приоритетпроцесса
Базовыйприоритетпотока
Базовыйприоритетпотока
Рис. 2 4 Схема динамического изменения приоритетов в W ndows NT
Для определения порядка выполнения потоков диспетчер использует систему приоритетов, направляя на выполнение потоки с высоким приоритетом раньше потоков с низкими приоритетами. Система прекращает исполнение или вытесняет (preempts) текущий поток, если становится готовой к выполнению другая задача (ноток) с более высоким приоритетом.
Существует группа очередей — по одной для каждого приоритета. Windows NT поддерживает 32 уровня приоритетов; потоки делятся на два класса: реального времени и переменного приоритета. Потоки реального времени, имеющие приоритеты от 16 до 31 — это высокоприоритетные потоки, используемыми программами с критическим временем выполнения, то есть требующие немедленного внимания системы (по терминологии Microsoft).
Диспетчер задач просматривает очереди, начиная с самой приоритетной При этом если очередь пустая, то есть нет готовых к выполнению задач с таким приоритетом, осуществляется переход к следующей очереди. Следовательно, если есть задачи, требующие процессор немедленно, они будут обслужены в первую очередь. Для собственно системных модулей, функционирующих в статусе задач, зарезервирована очередь с номером 0.
Большинство потоков в системе относятся к классу переменного приоритета с уровнями приоритета (номером очереди) от 1 до 15. Эти очереди используются потоками с переменным приоритетом (variable priority), так как диспетчер задач корректирует их приоритеты по мере выполнения задач для оитими «ацин отклика системы. Диспетчер приостанавливает исполнение текущего потока после того, как тот израсходует своп квант времени. При этом если прерванный тред — что поток переменного приоритета, то диспетчер задач понижает его приоритет на единицу п перемещает в другую очередь. Таким образом, приоритет потока, выполняющего много вычислений, постепенно понижается (до значения его базового приоритета). С другой стороны, диспетчер повышает приоритет потока после освобождения задачи (потока) из состояния ожидания. Обычно добавка к
приоритету потока определяется кодом исполнительной системы, находящимся вне ядра ОС, однако величина этой добавки зависит от типа события, которого ожидал заблокированный тред. Так, например, поток, ожидавший ввода очередного байта с клавиатуры, получает большую добавку к значт нию своего приоритета, чем процесс ввода/вывода, работавший с дисковым накопителем. Однако в любом случае значение приоритета не может достигнуть 16.
В операционной системе OS/2 схема динамической приоритетной диспетчеризации несколько иная, хотя и похожа на рассмотренную. В OS/2 имеются четыре класса задач. И для каждого класса задач имеется своя группа приоритетов с интервалом значений от 0 до 31. Итого, 128 различных уровней и, соответственно, 128 возможных очередей готовых к выполнению задач (тредов, потоков).
Класс задач, имеющих самые высокие значения приоритета, называется критическим (time critical). Этот класс предназначается для задач, которые мы в обиходе называем задачами реального времени, то есть для них должен быть обязательно предоставлен определенный минимум процессорного времени. Наиболее часто встречающимися задачами, которые относят к этому классу, являются задачи коммуникации (например, задача управления последовательным портом, принимающим биты с коммутируемой линии, к которой подключен модем или задачи управления сетевым оборудованием). Если такие задачи не получат управление в нужный момент времени, то сеанс связи может прерваться.
Следующий класс задач имеет название приоритетного. Поскольку к этому классу относят задачи, которые выполняют по отношению к остальным задачам роль сервера (о модели клиент—сервер, по которой строятся современные ОС с мик- роядерной архитектурой, см. в разделе «Микроядерные операционные системы», глава 5), то его еще иногда называют серверным. Приоритет таких задач должен быть выше, это будет гарантировать, что запрос па некоторую функцию со стороны обычных задач выполнится сразу, а нс будет дожидаться, пока до него дойдет очередь па фоне других пользовательских приложений.
Большинство задач относят к обтачному классу, его еще называют регулярным или стандартным2. По умолчанию система программирования порождает задачу, относящуюся именно к этому классу. Наконец, существует еще класс фоновых задач, называемы!! в OS/2 остаточным. Программы этого класса получают процессорное время только тогда, когда нет задач из других классов, которым сейчас нужен процессор. В качестве примера такой задачи можно привести программу проверки электронной почты
Внутри каждого из вышеописанных классов задачи, имеющие одинаковый уровень приоритета, выполняются в соответствии с дисциплиной round-robin Переход от одного треда к другому происходит либо по окончании отпущенного ему кванта времени, либо по системному прерыванию, передающему управление задаче с более высоким приоритетом (таким образом, система вытесняет задачи с
более низким приоритетом для выполнения задач с более высоким приоритетом и может обеспечить быструю реакцию на важные события).
OS/2 самостоятельно изменяет приоритет выполняющихся программ независимо от уровня, установленного самим приложением. Этот механизм называется повышением приоритета. Операционная система изменяет приоритет задачи в следующих трех случаях [96]:
Увеличение приоритета активной задачи (foreground boost). Приоритет задачи автоматически увеличивается, когда она становится активной. Это снижает время реакции активного приложения на действия пользователя по сравнению с фоновыми программами.
Увеличение приоритета ввода/вывода (input/oulput boost). По завершении операции ввода/вывода задача получает самый высокий уровень приоритета ее класса. Таким образом обеспечивается завершение всех незаконченных операций ввода/вывода.
Увеличение приоритета «забытых» задач (starvation boost). Если задача не получает управление в течение достаточно долгою времени (этот промежуток времени задает оператор MAXWAIT в файле CONFIG.SYS), диспетчер задач OS/2 временно присваивает ей уровень приоритета, не превышающий критический. В результате переключение на такую «забытую» программу происходит быстрее. После выполнения приложения в течение одного кванта времени его приоритет вновь снижается до остаточного. В сильно загруженных системах этот механизм позволяет- программам с остаточным приоритетом работать хотя бы в краткие интервалы времени. В противном случае они вообще никогда бы не получили управление.
Если нам нет необходимости использовать метод динамического изменения приоритета, то с помощью оператора PRIORITY = ABSOLUTE в файле CONFIG SYS можно ввести дисциплину абсолютных приоритетов; по умолчанию оператор PRIORITY имеет значение DYNAMIC.
Память и отображения, виртуальное адресное пространство
Если не принимать во внимание программирование па машинном языке (эта технология практически не используется уже очень давно), то можно сказать, что программист обращается к памяти с помощью некоторого набора логических имен, которые чаще всего являются символьными, а не числовыми и для которого отсутствует отношение порядка Другими словами, в общем случае множество переменных нсунорядочепо, хотя отдельные переменные и могут иметь частичную упорядоченность (например, элементы массива). Имена переменных и входных точек программных модулей составляют пространство имен
С другой стропы, существует понятие физической оперативной памяти, собственно с которой и работает процессор, извлекая из нее команды и данные и помещая в нее результаты вычислений. Физическая память представляет собой упорядоченное множество ячеек, и все они пронумерованы, то есть к каждой из них можно обратиться, указав ее порядковый номер (адрес). Количество ячеек (физической памяти ограничено и фиксировано.
Системное программное обеспечение должно связат ь каждое указанное пользователем имя с физической ячейкой памяти, то есть осуществить отображение пространства имен па (физическую память компьютера. В общем случае это отображение осуществляется в два этапа (рис. 2.5): сначала системой программирования, а затем операционной системой (с помощью специальных программных модулей управления памятью и использования соответствующих аппаратных средств вычислительной системы). Между этими этапами обращения к памяти имеют (форму виртуального или логического адреса. При этом можно сказать, что множество всех допустимых значений! виртуального адреса для некоторой программы определяет ее виртуальное adjmcnbe пространство или виртуальную память. Виртуальное адресное пространство программы прежде всего зависит от архитектуры процессора и от системы программирования и практически не зависит от объема реальной (физической памяти, установленной в компьютер. Можно еще сказать, что адреса команд и переменных в готовой машинной программе, подготовленной к выполнению системой программирования, как раз и являются в и р 1 уал ы пя м и адресам 1i.
Рис. 2.5. Память и отображения
Как мы знаем, система программирования осуществляет трансляцию и компоновку программы, используя библиотечные программные модули (подробно об этом см. часть 2 настоящего учебника). В результате работы системы программирования полученные виртуальные адреса могут иметь как двоичную форму, гак и символьно-двоичную, то есть некоторые программные модули (их, как правило, большинство) и их переменные получают какие-то числовые значения, а тс модули, адреса для которых нс могут быть сейчас определены, имеют по- прежнему символьную форму и окончательная привязка их к физическим ячейкам будет осуществлена па этапе загрузки программы в память перед ее непосредственным выполнением.
Одним из частных случаев отображения пространства имен на физическую память является тождественность впртуан.пого адресного пространства физической памяти. При этом нет необходимости осуществлять второе отображение. В данном случае говорят, ч то система программирования генерирует абсолютную двоичную программу, в згой программе все двоичные адреса таковы, что программа может исполняться только в том случае, если ее виртуальные адреса будут точно coinнетствовать физическим. Часть программных модулей любой операционной системы обязательно должна быть абсолютными двоичными программами. Эти программы размещаются но фиксированным адресам и с их помощью уже можно впоследствии реализовывать размещение остатьных программ, подготовленных системой программирования таким образом, что они могут работать на различных физических адресах (то есть па тех адресах, па которые их разместит операционная система).
Другим частным случаем этой общей схемы трансляции адресного пространства является тождественность виртуадыюго адресного пространства исходному пространству имен. Здесь уже отображение выполняется самой ОС, которая во время исполнения использует таблицу символьных имен. Такая схема отображения используется чрезвычайно редко, так как отображение имен на адреса необходимо выполнять для каждого вхождения имени (каждого нового имени) и особенно много времени тратится на квалификацию имен. Данную схему можно было встретить в интерпретаторах, в которых стадии трансляции и исполнения практически неразличимы. Эго характерно для простейших компьютерных систем, в которых вместо операционной системы использовался встроенный интерпретатор (например, Basic).
Возможны и промежуточные варианты. В простейшем случае транслятор-компилятор генерирует относительные адреса, которые, по сути, являются виртуальными адресами с последующей настройкой программы на один из непрерывных разделов. Второе отображение осуществляется перемещающим загрузчиком. После загрузки программы виртуальный адрес теряется, и доступ выполняется непосредственно к физическим ячейкам. Болес эффективное решение достигается в том случае, когда транслятор вырабатывает в качестве виртуального адреса относительный адрес и информацию о начальном адресе, а процессор, используя подготавливаемую операционной системой адресную информацию, выполняет второе отображение нс один раз (при загрузке программы), а при каждом обра- щепни к памяти.
Термин виртуальная память фактически относится к системам, которые сохраняют виртуальные адреса во время исполнения. Гак как второе отображение
осуществляется в процессе исполнения задачи, то адреса физических ячеек могут изменяться. При правильном применении такие изменения могут улучшить использование памяти, избавив программиста от деталей управления ею, и даже увеличить надежность вычислений.
Если рассматривать общую схему двухэтапного отображения адресов, го с позиции соотношения объемов упомянутых адресных пространств можно отметить наличие следующих трех ситуаций:
объем виртуального адресного пространства программы Vv меньше объема физической памяти Vp;
Vv»Vp;
О Vv>Vp.
Первая ситуация, при которой \\. < Vp, ныне практически не встречается, но тем не менее это реальное соотношение. Скажем, не так давно 16-разрядпыс мини- ЭВМ имели систему команд, в которых пользователи-программисты могли адресовать до 2,6=64К адресов (обычно в качестве адресуемой единицы выступала ячейка памяти размером с байт). А физически старшие модели этих мини-ЭВМ могли иметь объем оперативной памяти в несколько мегабайт. Обращение к памяти столь большого объема осуществлялось с помощью специальных регистров, содержимое которых складывалось с адресом операнда (или команды), извлекаемым н/или определяемым из поля операнда (или из указателя команды) Со- ответствующие значения в эти специальные регистры, выступающие как базовое смещение в памяти, заносила операционная система. Для одной задачи в регистр заносилось одно значение, а для второй (третьей, четвертой и т. д.) задачи, размещаемой одновременно с первой, но в другой области памяти, заносилось, соот- ветегвеино, другое значение. Вся физическая намять, таким образом, разбивалась на разделы объемом по 64 Кбайт, и на каждый такой раздет осуществлялось отображение своего виртуального адресного пространства.
Ситуация, когда Vv = Vp встречалась достаточно часто, особенно характерна она была для недорогих вычислительных комплексов. Для этого случая имеется большое количество методов распределения оперативной памяти.
Наконец, в паше время мы уже достигли того, эт о ситуация Vv > Vp встречается даже в ПК, то есть в самых распространенных и недорогих компьютерах. Теперь это самая распространенная ситуация, и для нее имеется несколько методов распределения памяти, отличающихся как сложностью, так и эффективностью.
Простое непрерывное распределение и распределение с перекрытием (оверлейные структуры)
Простое непрерывное распределение это самая простая схема, согласно кото рой вся память условно может быть разделена на три части
область, занимаемая операционной системой;
область, и которой размещается исполняемая задача;
незанятая ничем (свободная) область памяти.
Изначально являясь самой первой схемой, опа продолжает и сегодня быть достаточно распространенной. Эта схема предполагает, что ОС не поддерживает мультипрограммирование, поэтому не возникает проблемы распределения памя- 1п между несколькими задачами. Программные модули, необходимые для всех программ, располагаются в области самой ОС, а вся оставшаяся память может быть предоставлена задаче. Эта область памяти при этом получается непрерывной, что облегчает работу системы программирования. Поскольку в различных однотипных вычислительных комплексах может быть разный состав внешних устройств (и, соответственно, они содержат различное количество драйверов), для системных нужд могут быть отведены отличающиеся объемы оперативной памяти, и получается, что можно не привязывать жестко виртуальные адреса программы к физическому адресному пространству. Эта привязка осуществляется па этапе загрузки задачи в память.
Чтобы доя задач отвести как можно больший объем памяти, операционная система строится таким образом, что постоянно в оперативной памяти располагается только самая нужная ее часть. Эту часть ОС стали называть ядром Остальные модули ОС могут быть обычными диск-резидептиымп (или транзитными), то есть загружаться в оперативную память только по необходимости, и после своего выполнения вновь освобождать память.
Такая схема распределения влечет за собой два вида потерь вычислительпых ресурсов — потеря процессорного времени, потому что процессор простаивает, пока задача ожидает завершения операций ввода/вывода, и потеря самой оперативной памяти, потому что далеко не каждая программа использует всю память, а режим работы в этом случае однопрограммный. Однако это очень недорогая реализация и можно отказаться от многих функций операционной системы. В частности, такая сложная проблема, как защита памяти, здесь почти не стоит.
Мы не будем рассматривать все различные варианты простого непрерывного распределения памяти, которых было очень и очень много, а ограничимся схемой распределения памяти для MS-DOS (см. раздел «Распределение оперативной памяти в MS-DOS», глава 2).
Если есть необходимость создать программу, логическое (и виртуальное) адресное пространство которой должно быть больше, чем свободная область памяти, или даже больше, чем весь возможный объем оперативной памяти, то используется распределение с перекрытием (так называемые оверлейные структуры'). Этот метод распределения предполагает, что вся программа может быть разбита на части — сегменты. Каждая оверлейная программа имеет одну главную часть (main) и несколько сегментов (segment), причем в памяти машины одновременно могут находиться только ее главная часть и один или несколько нс перекрывающихся сегментов.
От tmerlay — перекрытие, расположение поверх чего-то.
Пока в оперативной памяти располагаются выполняющиеся сегменты, остальные находятся во внешней памяти. После того как текущий (выполняющийся) сегмент завершит свое выполнение, возможны два варианта. Либо он сам (если данный сегмент не нужно сохранить во внешней памяти в его текущем состоянии) обращается к ОС с указанием, какой сегмент должен быть загружен и память следующим. Либо он возвращает управление главному сегменту задачи (в модуль main), и уже тог обращается к ОС с указанием, какой сегмент сохранить (если это нужно), а какой сегмент загрузить в оперативную память, и вновь отдает управление одному из сегментов, располагающихся в памяти. Простейшие схемы сегмшггнровапия предполагают, что в памяти в каждый конкретный момент времени может располагаться только один сегмент (вместе с модулем main). Более сложные схемы, используемые в больших вычислительных системах, позволяют располагать по несколько сегментов. В некоторых вычислительных комплексах могли существовать отдельно сегменты кода и сегменты данных. Сегменты кода, как правило, не претерпевают изменений в процессе своего исполнения, поэтому при загрузке нового сегмента кода на место отработавшего последний можно не сохранять во внешней памяти, в отличие от сегментов данных, которые сохранять необходимо.
Первоначально программисты сами должны были включать в тексты своих программ соответствующие обращения к ОС (их называют вызовами) и тщательно планировать, какие сегменты могут находиться в оперативной памяти одновременно, чтобы их адресные пространства не пересекались. Однако с некоторых пор эти вызовы система программирования стала подставлять в код программы сама, автоматически, если в том возникает необходимость. Так, в известной и популярной системе программирования Turbo Pascal, начиная с третьей версии, программист просто указывал, что данный модуль является оверлейным. И при обращении к нему из основной программы модуль загружался в память и ему передавалось управление. Все адреса определялись системой программирования автоматически, обращения к DOS для загрузки оверлеев тоже генерировались системой Turbo Pascal.
Распределение статическими и динамическими разделами
Для организации мультипрограммного режима необходимо обеспечить одновременное расположение в оперативной памяти нескольких задач (целиком или их частями). Самая простая схема распределения памяти между несколькими задачами предполагает, что память, незанятая ядром ОС, может быть разбита на несколько непрерывных частей (зон, разделов1). Разделы характеризуются именем, типом, границами (как правило, указываются начало раздела и его длина).
Разбиение памяти на несколько непрерывных разделов может быть фиксированным (статическим), либо динамическим (то есть процесс выделения нового раздела памяти происходит непосредственно при появлении новой задачи).
' Partition, region — раздел.
Вначале мы кратко рассмотрим статическое распределение памяти па несколько разделов.
Разделы с фиксированными границами
Разбиение всего объема оперативной памяти па несколько разделов может осу ществляться единовременно (то есть в процессе генерации варианта ОС, который потом и эксплуатируется) пли по мере необходимости оператором системы Однако и во втором случае при выполнении разбиения памяти на разделы вычислительная система более ни для каких целей в этот момент не используется. Пример разбиения памяти па несколько разделов приведен на рис. 2 6.
Ядро операционной системы
>- Раздел № О
Транзитная область ОС
Задача А
>- Раздел № 1
Неиспользуемая область
Задача В
>- Раздел Ne 2
Неиспользуемая область
Задача С
Неиспользуемая областьНеиспользуемая областьРаздел Ne 3Раздел Ne 3
Неиспользуемая область
Неиспользуемая область
Раздел Ne 3
Раздел Ne 3
Рис. 2 6. Распределение памяти разделами с фиксированными границами
В каждом разделе в каждый момент времени может располагаться по одной про грамме (задаче). В этом случае по отношению к каждому разделу можно примени гь все гс методы создания программ, которые используются для одноиро- граммных систем. Возможно использование оверлейных структур, что позволяет создавать большие сложные программы и в то же время Поддерживать коэффициент мультипрограммирования (под коэффициентом мультипрограммирования (р) попимакх количество параллельно выполняемых программ) на должном уровне. Первые мультипрограммные ОС строились по этой схеме Исиользова- 1ась эта схема и много лет спустя при создании недорогих вычислительных смс-
тем, ибо она является несложной и обеспечивает возможность параллельного выполнения программ. Иногда в некотором разделе размещалось но несколько небольших программ, которые постоянно в нем и находились. Такне программы назывались ОЗУ-рсзидеитными (или просто — резидентными). Опи же используются и в современных встроенных системах; правда, для них характерно, что все программы являются резидентными и внешняя память во время работы вычислительного оборудования не используется.
При небольшом объеме памяти и, следовательно, небольшом количестве разделов увеличить количество параллельно выполняемых приложений (особенно когда эти приложения интерактивны и во время своей работы они фактически не используют процессорное время, а в основном ожидают операций ивода/вы- вода) можно за счет своппинга (swapping). При своппынге задача может быть целиком выгружена па магнитный диск (перемещена во внешнюю память), а на ее место загружается либо более привилегированная, либо просто готовая к выполнению другая задача, находившаяся на диске в приостановленном состоянии. При своппингс из основной памяти во внешнюю (и обратно) перемещается вся программа, а нс се отдельная часть.
Серьезная проблема, которая возникает при организации мультипрограммного режима работы вычислительной системы, — это защита как самой ОС от ошибок и преднамеренного вмешательства задач в ее работу, так и самих задач друг от Друга.
В самом деле, программа может обращаться к любым ячейкам в пределах своего виртуального адресного пространства. Если система отображения памяти не содержит ошибок и в самой программе их тоже нет, то возникать ошибок при выполнении программы не должно. Однако в случае ошибок адресации, что нс так уж и редко случается, исполняющаяся программа может начать «обработку» чужих данных или кодов с непредсказуемыми последствиями. Одной из простейших, по достаточно сильных мер является введение регистров защиты памяти. В эти регистры ОС заносит граничные значения области памяти раздела текущей исполняющейся задачи. При нарушении адресации возникает прерывание и управление передается супервизору ОС. Обращения к ОС за необходимыми сервисами осуществляются не напрямую, а через команды программных прерываний, что обеспечивает передачу управления только в предопределенные входные точки кода ОС, и в системном режиме работы процессора, при котором регистры защиты памяти игнорируются. Таким образом, выполнение функции защиты требует введения специальных аппаратных механизмов, используемых операционной сис темой.
Основным недостатком такого способа распределения памяти является наличие порой достаточно большого объема неиспользуемой памяти (см. рис. 2.G). Неиспользуемая память может быть в каждом из разделов. Поскольку разделов несколько, го и неиспользуемых областей получатся несколько, поэтому такие потерн стали называть фрагментацией памяти. В отдельных разделах потери памяти могут быть очень значительными, однако использовать фрагменты свободной памяти при таком способе распределения не представляется возможным. Желание разрабо тчиков сократить столь значительные потери привело их к следующим двум решениям:
выделять раздел ровно такого объема, который нужен иод текущую задачу;
размещать задачу не в одной непрерывной области памяти, а в нескольких областях.
Второе решение реализовалось в нескольких способах оргапи танин виртуальной памяти. Мы их обсудим в следующем разделе, а сейчас кратко рассмотрим первое решение.
Разделы с подвижными границами
Чтобы избавиться от фрагментации, можно попробовал, размещать в оперативной памяти задачи плотно. одну за другой, выделяя ровно столько памяти, сколько задача требует. Одной из первых ОС, реализовавшей такой способ распределения памяти, была OS MVT1. Специальный планировщик (диспетчер памяти) ведет список адресов свободной оперативной памяти. При появлении повой задачи диспетчер памяти просматривает этот список и выделяет для задачи раздет объем которого либо равен необходимому, либо чуть больше, если память выделяется не ячейками, а некими дискретными единицами. При этом модифицируется список свободной памяти. При освобождении раздела диспетчер памяти пытается объединить освобождающийся раздел с одним из свободных участков, если таковой является смежным.
При этом список свободных участков может быть упорядочен либо ио адресам, либо по объему. Выделение памяти под новый раздел может осуществляться одним из трех способов:
первый подходящий участок;
самый подходящий участок;
самый неподходящий участок.
В первом случае список свободных областей упорядочивается но адресам (на пример, но возрастанию адресов). Диспетчер памяти просматривает этот список и выделяет задаче раз,чел в той области, которая первой подойдет по объему В этом случае, если такой фрагмент имеется, то в среднем необходимо просмотреть половину списка. При освобождении раздела также необходимо просмотреть половину списка. Правило «первый подходящий» приводит к тому, чю память для небольших задач преимущественно будет выделят ься в облас и младших адресов и, слеЛоватодыю, это будет увеличивать вероятность того, что в области старших адресов будут образовываться фрагменты достаточно большого объема Способ «самый подходящий» предполагает, что список свободных обтает ей упорядочен по возрастанию объема этих фрагментов. В этом случае при просмотре списка для нового раздела будет использован фрагмент свободной памяти, объем которой наиболее точно соответствует требуемому Требуемый раздет будет определяться по-прежнему в результате просмотра в среднем половины списка.
MI-7 (multiprogramming with a variable number of tasks) — мультипрограммирование с переменным числом задач. Эта ОС быта одной из самых распространенных п)>и жеипу mi- ровапни больших ЭВМ класса IBM 360 (370).
Однако оставшийся фрагмент оказывается настолько малым, что в нем уже вряд ли удое гея разместить каком-либо еще раздел и при э том этот фрагмент попадет в самое начало списка. Поэтому в целом такую дисциплину нельзя назвать эффективной.
Как ни странно, самым эффективным правилом является последнее, по которому для нового раздела выделяется «самый неподходящий» фрагмент свободной памяти. Для этой дисциплины список свободных областей упорядочивается по убыванию объема свободного фрагмента. Очевидно, что если есть подходящий фрагмент памяти, то он сразу же и будет найден, п поскольку этот фрагмент является самым большим, то, скорее всего, после выделения из него раздела памяти для задачи оставшаяся область памяти еще сможет быть использована в дальнейшем.
Однако очевидно, что при любой дисциплине обслуживай ня, но которой работа- i г диспетчер памяти, из-за того, что задачи появляются и завершаются в произвольные моменты времени и при этом они имеют разшяе объемы, то в памяти всегда будет наблюдаться сильная фрагментация При этом возможны ситуации, когда из-за сильной фрагментации памяти диспетчер памяти не сможет Оиразо- в<нь новый раздел, хотя суммарный объем свободных областей будет больше, чем необходимо для задачи. В этой ситуации возможно ортапизовать так называемое «уплотнение памяти*,. Для уплотнения памяти все вычисления приостанавливаются, и диспетчер памяти корректирует свои списки, перемещая разделы в начало памяти (млн, наоборот, в область старших адресов). При определении физических адресов задачи будут участвовать новые значения базовых регистров. с помощью которых и осуществляется преобразование виртуальных адресов в физические. Недостатком этого решения является потеря времени на уплсенс- ние и, что самое главное, невозможность при этом выполнять сами вычислительные процессы.
Данный способ распределения намят и тем не менее применялся достаточно длительное время в нескольких операционных системах, поскольку в нам для задач выделяется непрерывное адресное пространство, а эго упрощает создание систем программирования и нх работу.
Сегментная, страничная и сегментностраничная организация памяти
Методы рас шределения памяти, при которых задаче уже может не предоставляться сплошная (иопрерывпая) область памяти, называют разрывными Идея выделять память задаче не одной сплошной областью, а фрагментами требует для своей реализации соответствующей аппаратной поддержки — нужно иметь относительную адресацию. Если указывать адрес начала текущего фрагмеша программы и величину смещения относительно этого ввчального адреса, то можно указать необходимую нам переменную или команду. Таким образом, виртуальный адрес можно представить состоящим из двух нолей. Первое поте будет указывать честь программы (с которой сейчас осуществляется работа) для определения местоположения этой части в памяти, а второе поле виртуального адре-
са позволит найти нужную нам ячейку относительно найденного адреса. Программист может либо самостоятельно разбивать программу на фрагменты, либо автоматизировать эту задачу и возложить се на систему программирования.
Сегментный способ организации виртуальной памяти
Первым среди разрывных методов распределения памяти был сегментный. Д 1я Этого метода программу необходимо разбивать на части и уже каждой такой час- ги выделять физическую память Естественным способом разбиения программы на части является разбитпие се на логические элементы — так называемые сегменты. В принципе каждый программный модуль (или их совокупность, если мы юго пожелаем) может быть военрнияч как отдельный сегмент, и вся программа тогда будет представлять собой множество сегментов. Каждый сегмент размещается в памяти как до определенной степени самостоятельная единица Логически обращение к элементам программы в этом случае будет представляться как указание имени сегмента и смещения относительно начала этого cci мен га. Физически имя (или порядковый номер) сегмента будет соопи тствоваи. некоторому адресу, с которою этот сегмент начинается при его размещении в i а- мяти, и смещение должно прибавляться к этому базовому адресу.
Преобразование имени сегмента в его порядковый номер осуществит система программирования. а операционная система будет размощеть сегменты в иашггь и для каждого сегмента получит информацию о его начале. Таким образом, виртуальный адрес для этого способа будет состоять из двух полей — номер е егмента и смещение относительно начала сегмента. Соответствующая иллюстрация при велепа па рис. 2.7. Па этом рисунке изображен случай обращения к ячейке виртуальный адрес которой равен сегмешу с номером 11 и смещением от начала лого сегмента. равным 612 Как мы видим, операционная система разместила данный сегмент в памяти, начиная с ячейки с номером 19 700.
Каждый сегмеиг, размещаемый в памяти, имеет соответствующую информационную структуру. часто называемую дескриптором сежмента. Именно операционная система строит для каждого исполняемого процесса соответствующую таблицу дескрипторов сегментов и при размещении каждого из сегментов в оие ративпой или внешней памяти в дескрипторе отмечает его текущее местоположение. Если сегмент задачи в данный момент находится в оперативно! памяти, го об этом делается пометка в дескрипторе. Как правило, для этого используется «бит присутствия» Р (present) В этом случае в поле «адрес» диспетчер памяти записывает адрес физической памяти, с которого сегмент начинается, а в поле «длина сегмента» (limit) указывается количество адресуемых ячеек памяти Это иоле используется не только для того, чтобы размещать сегменты без наложения один па другой, но и для того, чтобы ироксипролировать, не обращается ли код нсполпяюще-йся задачи за продеты текущего сегмента. В случае превышения длины сегмента вследствие ошибок программирования мы можем говорить о нарушении адресации и с помощью введения специальных аинартгпых средств генерировать сигналы прерывания, которые позволят фиксировать (обнаруживать) такого рода ошибки.
Регистр таблицыРегистр таблицысегментов (таблицы дескрипторов сегментов)сегментов (таблицы дескрипторов сегментов)Виртуальный адресВиртуальный адресРис. 2.7. Сегментный способ организации виртуальной памятиРис. 2.7. Сегментный способ организации виртуальной памяти
Регистр таблицы
Регистр таблицы
сегментов (таблицы дескрипторов сегментов)
сегментов (таблицы дескрипторов сегментов)
Виртуальный адрес
Виртуальный адрес
Рис. 2.7. Сегментный способ организации виртуальной памяти
Рис. 2.7. Сегментный способ организации виртуальной памяти
Если бит present в дескрипторе указывает что сейчас этот сегмент находится не в оперативной, а во внешней памяти (например, на винчестере) то названные поля адреса и длины используются для указания адреса сегмен а в коор, инатах внешней памяти. Помимо информации о местоположении сегмента, в дескрипторе сегмента, как правило, содержатся данные о его типе (сегмент кода г и сегмент данных), нравах доступа к этому сегменту (можно или нельзя его модифицировать, предоставлять другой задаче), отметка об обращениях к да! пому сегменту (информация о гом, как часто или как давио/педавпо этот сегмент не пользуется или не используется, на основании которой можно принять решение о том чтобы предоставить место, занимаемое текущим сегментом др тому сегменту).
При передаче управления следующей задаче ОС должна занес и в соответствую щий peiнегр адрес табтицы дескрипторов сегментов этой задачи Сама таблица дескрипторов сегментов, в свою очередь также представ, яст собой сегмент дай пых который обрабатывается диспетчером памяти операционной системы
При таком подходе появляется возможность размещать в оперативной памяти не все сегменты задачи, а только те, с которыми в настоящий момент происходит работа. С одной стороны, становится возможным, чтобы общий обьем виртуальною адресного пространства задачи превосходил объем ф> зической памяти компьютера, на котором эта задача будет выполняться. С другой стороны, даже если потребности в памяти не превосходят имеющуюся физическую памя ь, появляется возможность размещать в памяти как можно больше задач А увеличение коэффициента мультипрограммирования ц, как мы знаем, полю ше уве шчить загрузку системы и более эффективно использовать ресурсы вычислительной системы. Очевидно, однако, что увеличивать количество задач можно тон ко до
определенного предела, ибо если в памяти не будет хватать места для часто используемых сегментов, то производительность системы резко упадет. Ведь сегмент, который сейчас находится вне оперативной памяти, для участия в вычислениях дотжен быть перемещен в оперативную память. При этом если в памяти есть свободное пространство, то необходимо всего лишь найти его во внешней памяти и загрузить в оперативную память. А если свободного места сейчас нет, го необходимо будет принять решение — на место какого из ныне присутствующих сегментов будет загружаться требуемый.
Итак, если требуемого сегмента в оперативной памяти нет, то возникает прерывание и управление передается через диспетчер памяти программе загрузки сегмента. Пока происходит поиск сегмента во внешней памяти и загрузка его в оперативную, диспетчер памяти определяет подходящее для сегмента место. Возможно, что свободного места нет, и тогда принимается решение о выгрузке какого-нибудь сегмента и его перемещение во внешнюю память. Если при этом еще остается время, то процессор передается другой готовой к выполнению задаче. После загрузки необходимого сегмента процессор вновь передается задаче, вызвавшей прерывание из-за отсутствия сегмента. Всякий раз при считывании сегмента в оперативную память в таблице дескрипторов сегментов необходимо установить адрес начала сегмента и признак присутствия сегмента.
При поиске свободного места используется одна из вышеперечисленных дисциплин работы диспетчера памяти (применяются правила «первого подходящего» и «самого неподходящего» фрагментов). Если свободного фрагмента памяти достаточного объема сейчас пет, но тем не менее сумма этих свободных фрагментов превышает требования ио памяти для нового сегмента, то в принципе может быть применено «уплотнение памяти», о котором мы уже говорили в подразделе «Разделы с фиксированными границами» при рассмотрении динамического способа разбиения памяти на разделы.
В идеальном случае размер сегмента должен быть достаточно малым, чтобы его можно было разместить в случайно освобождающихся фрагментах оперативной памяти, ио достаточно большим, чтобы содержать логически законченную чисть программы с тем, чтобы минимизировать межсегментные обращения.
Для решения проблемы замещения (определения того сегмента, который должен быть либо перемещен во внешнюю память, либо просто замещен новым) используются следующие дисциплины:
правило FIFO (first in — first out, что означает: «первый пришедший первым и выбывает»);
правило LRU (least recently used, что означает «последний из недавно использованных» или, иначе говоря, «дольше всего неиспользуемый»);
правило LFU (least frequently used, что означает: «используемый реже всех остальных»);
случайный (random) выбор сегмента.
Первая и последняя дисциплины являются самыми простыми в реализации, но они не учитывают, насколько часто используется тот или иной сегмент и, следовательно, диспетчер памяти может выгрузить нлп расформировать тот сегмент, к которому в самом ближайшем будущем будет обращение. Безусловно, достоверной информации о том, какой из сегментов потребуется в ближайшем будущем, в общем случае иметь нельзя, ио вероятность ошибки для этих дисциплин многократно выше, чем у второй и третьей дисциплины, которые учитывают информацию об использовании сегментов.
Алгоритм FIFO ассоциирует с каждым сегментом время, когда он был помещен в намять. Для замещения выбирается наиболее старый сегмент. Учет времени необязателен, когда все сегменты в памяти связаны в FIFO-очередь и каждый помещаемый в память сегмент добавляется в хвост ягой очереди. Алгоритм учитывает только время нахождения сегмента в памяти, ио не учитывает фактическое использование сегментов. Например, первые загруженные сегменты программы могут содержать переменные, используемые па протяжении работы всей программы. Эго приводит к немедленному возвращению к только что замещенному сегменту.
Для реализации дисциплин LRU и LFU необходимо, чтобы процессор имел дополнительные аппаратные средства. Минимальные требования — достаточно, чтобы при обращении к дескриптору сегмента для получения физического адреса, с которого сегмент начинает располагаться в памяги, соответствующий биг обращения менял свое значение (скажем, с пулевого, которое установила ОС, в единичное). Тогда диспетчер памяти моекст время от времени просматривать таблицы дескрипторов исполняющихся задач и собирать для соответствующей обработки статистическую информацию об обращениях к сегментам. В результате можно составить список, упорядоченный либо но длительности нс использования (для дисциплины LRU), либо по частою использования (для дисциплины LFU).
Важнейшей проблемой, которая возникает при организации мультипрограммного режима, является защита памяти. Для того чтобы выполняющиеся приложения нс смогли испортить саму ОС и другие вычислительные процессы, необходимо, чтобы доступ к таблицам сегментов с целью их модификации был обеспечен только для кода самой ОС. Для этого код ОС должен выполняться в некотором привилегированном режиме, из которого можно осуществлять манипуляции с дескрипторами сегментов, тогда как выход за пределы сегмента в обычной при- кдадной программе должен вызывать прерывание ио защите памяти. Каждая прикладная задача должна иметь возможность обращаться только к своим собственным сегментам.
При использовании сегментного способа организации виртуальной памяти появляется несколько интересных возможностей. Во-первых, появляется возможность при загрузке программы на исполнение размещать се в памяти нс целиком, а «по мере необходимости». Действительно, поскольку в подавляющем большинстве случаев алгоритм, но которому работает код программы, является разветвленным, а не линейным, то в зависимости от исходных данных некоторые части программы, расположенные в самостоятельных сегментах, могут быть и нс за- действованы: значит, их можно и не загружать в оперативную память
Во-вторых, некоторые программные модули могут быть разделяемыми. Эти про граммные модули являются сегментами, и в этом случае отпоентслыю легко организовать доступ к таким сегмептам. Сегмент с разделяемым кодом распо игается в памяти в единственном экземпляре, а в нескольких таблицах дескрипторов сегментов исполняющихся задач будут находиться указатели на такие разделяемые сегменты.
Однако у сегментного способа распределения памяти есть и недостатки. Прежде всего, из рис. 2.7 видно, что для получения доступа к искомой ячейке памяти необходимо потратить намного больше времени. Мы должны сначала найти и прочитать дескриптор сегмента, а уже потом, используя данные из него о местонахождении нужного нам сегмента, можем вычислить и конечный (физический адрес. Для того чтобы уменьшить эти потери, используется кэширование — го есть те дескрипторы, с которыми мы имеем дело в данный момент, могут быть размешены в сверхоперативной памяти (специальных регистрах, размещаемых и процессоре).
Несмотря на то что этот способ распределения памяти приводит к существенно меньшей фрагментации памяти, нежели способы с неразрывным распределением, фрагментация остается. Кроме этого, мы имеем большие потерн памяти и процессорного времени иа размещение и обработку дескрип горших таблиц. Ведь на каждую задачу необходимо иметь свою таблицу дескрипторов сегментов. А при определении (физических адресов необходимо выполнять операции сложения.
Поэтому следующим способом разрывного размещения задач в памяти стал способ, при котором все (фрагменты задачи одинакового размера и длины, кратной степени двойки, чтобы операции сложения можно было заменить операциями конкатенации (слияния). Это — страничный способ организации вир1уальпой памяти.
Примером использования сегментного способа организации виртуальной памяти является операционная система для ПК OS/2 первого поколения, которая была создана для процессора i80286. В этой ОС в полной мере использованы аппаратные средства микропроцессора, который специально проектировался для поддержки сегментного способа распределения памяти.
OS/2 v.l поддерживала распределение памяти, при котором выделялись сегмеп ты программы и сегменты данных. Система позволяла работать как с именованными, так и неименованными сегмеп гамм. Имена разделяемых сегментов данных имели ту же форму, что и имена фантов. Процессы получали доступ к нмепбйаи- ным разделяемым сегментам, используя их имена в специальных см с темных вызовах. OS/2 v.l допускала разделение программных сегментов приложений и подсистем, а также глобальных сегментов данных подсистем. Вообще, вся концепция системы OS/2 была построена на понятии разделения памяти: процессы почти всегда разделяют сегменты с друз ими процессами. В этом состояло существенное отличие от систем тина UNIX, которые обычно разделяют только рееи- терабетьиые программные модули между процессами.
Сегменты, коеррые активно не использовались, могли выгружаться на жесткий диск. Система восстанавливала их, когда в этом возникала необходимость. Так как все области памяти, используемые сегментом, должны были быть непрерывными, OS/2 перемещала в основной памяти сегменты таким образом, чтобы максимизировать объем свободной физической памяти. Такое размещение называется компрессией или перемещением сегментов (уплотнением памяти). Программные сегменты не выгружались, поскольку они могли просто перезагружаться с исходных дисков. Области в младших адресах физической памяти, которые использовались для запуска DOS-программ и кода самой OS/2, не участвовали в перемещении или подкачке. Кроме этого, система или прикладная программа могли временно фиксировать сегмент в памяти с тем, чтобы гарантировать наличие буфера ввода/вывода в физической памяти до тех пор, пока операция ввода/вывода не завершится.
Если в результате компрессии памяти не удавалось создать необходимое свободное пространство, то супервизор выполнял операции фонового плана для перекачки достаточного количества сегментов из физической памяти, чтобы дать возможность завершиться исходному запросу.
Механизм перекачки сегментов использовал файловую систему для перекачки данных из (физической памяти и в нее. Ввиду' того, что перекачка и сжатие влияют па производительность системы в целом, пользователь может сконфигурировать систему так, чтобы эти функции не выполнялись.
Было организовано в OS/2 и динамическое присоединение обслуживающих программ. Программы OS/2 используют команды удаленного вызова. Ссылки, генерируемые этими вызовами, определяются в момент загрузки самой программы или се сегментов. Такое отсроченное определение ссылок называется динамическим присоединением. Загрузочный формат модуля OS/2 представляет собой расширение формата загрузочного модуля DOS. Он был расширен, чтобы под- держивагь необходимое окружение для свопинга сегментов с динамическим присоединением. Динамическое присоединение уменьшает объем памяти для программ в OS/2, одновременно делая возможным перемещения подсистем и обслуживающих программ без необходимости повторного редактирования адресных ссылок к прикладным программам.
Страничный способ организации виртуальной памяти
Как мы уже сказали, при таком способе все фрагменты программы, на которые она разбивается (за исключением последней ее части), получаются одинаковыми. Одинаковыми полагаются и единицы памяти, которые мы предоставляем для размещения фрагментов программы Эги одинаковые части называют страницами и говорят, чю память разбивается на (физические страницы, а программа — на виртуальные страницы. Часть виртуальных страниц задачи размещается в оперативной памяти, а часть — во внешней. Обычно место во внешней памяти, в качестве которой в абсолютном большинстве случаев выступают накопители на магнитных Дисках (поскольку они относятся к быстродействующим устройст-
вам с прямым доступом), называют файлом подкачки или страничным файлом (paging file). Иногда этот файл называют swap-файлом, тем самым подчеркивая, что записи этого файла — страницы — замещают друг друга в оперативной памяти. В некоторых ОС выгруженные страницы располагаются не в файле, а в специальном разделе дискового пространства. В UNIX-системах для этих целей выделяется специальный раздел, по кроме него могут быть использованы и файлы, выполняющие те же функции, если объема раздела недостаточно.
Разбиение всей оперативной памяти иа страницы одинаковой величины, причем величина каждой страницы выбирается кратной степени двойки, приводит к тому, что вместо одномерного адресного пространства памяти можно говорить о двумерном. Первая координата адресного пространства — это номер страницы, а вторая координата — номер ячейки внутри выбранной страницы (его называют индексом). Таким образом, физический адрес определяется парой (Рр, i), а виртуальный адрес — парой (Pv, i), где Pv — это номер виртуальной страницы, Рр — это номер физической! страницы и i — это индекс ячейки внутри страницы. Количество битов, отводимое под индекс, определяет размер страницы, а количество битов, отводимое под номер виртуальной страницы, — объем возможной виртуальной памяти, которой может пользоваться программа. Отображение, осуществляемое системой во время исполнения, сводится к отображению Pv в Рр и приписывании к полученному значению битов адреса, задаваемых величиной i. При этом нет необходимости ограничивать число виртуальных страниц числом физических, то есть не поместившиеся страницы можно размещать во внешней памяти, которая в данном случае служит расширением оперативной.
Для отображения виртуального адресного пространства задачи иа физическую память, как и в случае с сегментным способом организации, для каждой задачи необходимо иметь таблицу страниц для трансляции адресных пространств. Для описания каждой страницы диспетчер памяти ОС заводит соответствующий дескриптор, который отличается от дескриптора сегмента прежде всего тем, что в нем пет необходимости иметь поле длины — ведь все страницы имеют одинаковый размер. По номеру виртуальной страницы в таблице дескрипторов страниц текущей задачи находится соответствующий элемент (дескриптор). Если бит присутствия имеет единичное значение, значит, данная страница сейчас размещена в оперативной, а не во внешней памяти и мы в дескрипторе имеем номер физической страницы, отведенной под данную виртуальную. Если же бит при- сутствия равен нулю, то в дескрипторе мы будем иметь адрес виртуальной страницы, расположенной сейчас во внешней памяти. Таким образом и осуществляется трансляция виртуального адресного пространства па физическую память. Этот механизм трансляции проиллюстрирован на рис. 2.8.
Защита страничной памяти, как и в случае с сегментным механизмом, основана на контроле уровня доступа к каждой странице. Как правило, возможны следующие уровни доступа: только чтение; чтение и запись; только выполнение. В этом случае каждая страница снабжается соответствующим кодом уровня доступа. При трансформации логического адреса в физический сравнивается значение кода разрешенного уровня доступа с фактически требуемым. При их несовпадении работа программы прерывается.
Регистр таблицы страницРегистр таблицы страницВиртуальный адресВиртуальный адресРис 2.8. Страничный способ организации виртуальной памятиРис 2.8. Страничный способ организации виртуальной памяти
Регистр таблицы страниц
Регистр таблицы страниц
Виртуальный адрес
Виртуальный адрес
Рис 2.8. Страничный способ организации виртуальной памяти
Рис 2.8. Страничный способ организации виртуальной памяти
При обращении к виртуальной странице, не оказавшейся в данный момент в оперативной намят, возникает прерывание и управление передается диспетчеру памяти, который должен найти свободное место. Обычно предоставляется первая же свободная страница. Если свободной физической страницы пет, го диспетчер памяти по одной из вышеупомянутых дисциплин замещения (LRU. LFU, FIFO, random) опреде ни страницу, подлежащую расформированию или сохранению во внешней памяти. Па ее место он размет тит ту новую виртуальную страницу, к которой было обращение из задачи, но ее не оказалось в оперативной памяти.
Напомним, что алгоритм выбирает для замещения ту страницу, па которую не было ссылки на протяжении наиболее длинного периода времени. Дисциплин; LRU (least recently used) ассоциирует с каждой страницей время последнего ее использования. Для Замещения выбирается та страница, которая дольше всех не пспользовалаг ь.
Для использования дисциплин LRU и LFU в процессоре должны быть соответ ствующпе аппаратные средства. В дескрипторе страницы размешается бег обращения (подразумевается, что па рис. 2.8 этот <5т расположен в последнем поле), н этот бит становится единичным при обращении к дескриптору.
Если объем физической памяти небольшой и даже часто трсбуцмыс страницы не удается разместить в оперативной памяти, то возникает так называемая «пробуксовка». Другими словами, пробуксовка — это ситуация, при которой загрузка нужной нам страницы вызывает перемещение во внешнюю память той страницы, с которой мы тоже активно работаем. Очевидно. что это очень плохое явление.. Чтобы его не Допускать, желательно увеличить объем оперативной памяти (сейчас это стало самым простым решением), уменьшить количество параллельно выполняемых задач, либо попробовать использовать более эффективные- дне- । и । пл ины та мощения.
В абсолютном большинстве современных ОС используется дисциплина замещения страниц LRU как самая эффективная. Так, именно эта дисциплина используется в OS/2 и Linux. Однако в такой ОС, как Windows NT, разработчики, желая сделать систему максимально независимой от аппаратных возможностей процессора поппи на отказ от этой дисциплины и применили правило FIFO. А для того, чтобы хоть как-нибудь сгладить се неэффективность, была введена «буферы шипя» тех страниц, которые должны быть записаны в файл подкачки на диск пли просто расформированы. Принцип буферирования прост. Прежде км замещаемая страница действительно будет перемещена во внешнюю память пли просто расформирована, опа помечается как кандидат на выгрузку. Если в следующий раз произойдет обращение к странице, находящейся в таком «бутаре», то страница никуда не выгружается и уходит в конец списка FIFO. В противном случае страница действительно выгружается, а на ее место в «буфере» попадает следующий «кандидат». Ве личина такого «буфера» не может быть большой, по этому эффективность страничной реализации памяти в Windows NT намного ниже, чем у вышеназванных ОС, и явление пробуксовки начинается даже при существенно большем объеме оперативной памяти.
В ряде ОС с пакетным режимом работы для борьбы с пробуксовкой используется метод «рабочего множества». Рабочее множество — это множество «активных» страниц задачи за некоторой интерна 'Го есть тех страниц, к которым было обращение за гот интервал времени. Реально количество активных страниц задачи (за интервал Т) все время изменяется, и это естественно, но тем не менее для каждой задачи можно определить среднее количество ее активных страниц. Эго среднее число активных страниц и есть рабочее множество задачи Наблюдения за исполнением множества различных программ показали [28, 37. 501, что даже если Т равно времени выполнения веси работы то размер рабочего множества часто существенно меньше, чем общее число страниц программы. Таким образом. если ОС может определить рабочие множества исполняющихся задач, то для предотвращения пробуксовки достаточно планировать на выполнение только такое количество задач, чтобы сумма их рабочих множеств не превышала возможности системы
Как п в случав ( ссгмешгпым способом организации виртуальной памяти, страничный механизм приводи ! к тому, что без специальных аппаратных средств он будет существенно замедлять работу вычислительной системы. Поэтому обычно используется кэширование страничных дескрипторов. Наиболее бффек^пвпым способом кэширования является использование ассоциативного кэша. Именно такой ассоциативный ни и создан в 32-раЗрядных микропроцессорах 180x86 Начиная с i80386, который поддерживает страничный способ распределения памяти, в этих микропроцессорах имеется кэш на 32 страничных дескрип тора. По-
скольку размер страницы в этих микропроцессорах равен 4 Кбайт, возможно быстрое обращение к 128 Кбайт памяти
Итак, основным достоинством страничного способа распределения памяти является минимально возможная фрагментация. Поскольку на каждую задачу может приходиться по одной незаполненной странице, то становится очевидно, что память можно использовать достаточно эффективно; этот метод органжзацпи виртуальной памяти был бы одним из самых лучших, если бы не два следующих обстоятельства.
Первое — это то, что страничная трансляция виртуальной памяти требует существенных накладных расходов. В самом деле, таблицы страниц нужно тоже размещать в памяти. Кроме этого, эти таблицы нужно обрабатывать; именно с ними работает диспетчер памяти.
Второй существенный недостаток страничной адресации заключается в том, что программы разбиваются па страницы случайно, без учета логических взаимосвязей, имеющихся в коде. Это приводит к тому, что межеграничные переходы, как правило, осуществляются чаще, нежели межсегментные, и к тому, что становится трудно организовать разделение программных модулей между выполняющимися процессами.
Для того чтобы избежать второго недостатка, постаравшись сохранить достоинства страничного способа распределения памяти, был предложен еще один способ — сегментно-страничный. Правда, за счет дальнейшего увеличения накладных расходов на его реализацию.
Сегментно-страничный способ организации виртуальной памяти
Как и в сегментном способе распределения памяти, программа разбивается на логически законченные части — сегменты — и виртуальный адрес содержит указание па помер соответствующего сегмента. Вторая составляющая виртуального адреса — смещение относительно начала сегмента — в свою очередь, может состоять из двух полей: виртуальной страницы и индекса. Другими словами, получается, ч то виртуальный адрес теперь состоит из трех компонентов: сегмент, страница, индекс. Получение физического адреса и извлечение из памяти необходимого элемента для этого способа представлено на рис. 2.9.
Из рисунка сразу видно, что этот способ организации виртуальной памяти вносит еще большую задержку доступа к памяти. Необходимо сначала вычислить адрес дескриптора сегмента и прочитать его, затем вычислить адрес элемента таблицы страниц этого сегмента и навлечь и» памяти необходимый элемент, и уже только после этого можно к номеру физической страницы приписать номер ячейки в странице (индекс). Задержка доступа к искомой ячейке получается по крайней мерс в три раза больше, чем при простой прямой адресации. Чтобы избежать этой неприятности, вводится кэширование, причем кэш, как правило, строится по ассоциативному принципу. Другими словами, просмотры двух таблиц в памяти могут бьп ь заменены одним обращением к ассоциативной памяти.
Регистр таблицы сегментов (таблиц । деск ип оров сегментов)Регистр таблицы сегментов (таблиц । деск ип оров сегментов)Виртуальным адресВиртуальным адресРис. 2.9. Сегментно-страничный способ организации виртуальной памятиРис. 2.9. Сегментно-страничный способ организации виртуальной памяти
Регистр таблицы сегментов (таблиц । деск ип оров сегментов)
Регистр таблицы сегментов (таблиц । деск ип оров сегментов)
Виртуальным адрес
Виртуальным адрес
Рис. 2.9. Сегментно-страничный способ организации виртуальной памяти
Рис. 2.9. Сегментно-страничный способ организации виртуальной памяти
Напомним, что принцип действия ассоциативного запоминающего устройства предполагает, чго Каждой ячейке памяти такого устройства ставится в соответствие ячейка, в которой записывается некий ключ (признак, адрес), позволяющий однозначно идентифицировать содержимое ячейки памяти. Сопутствующую ячейку с информацией, позволяющей идентифицировать основные данные, обычно называют полем тега. Просмотр полей тега всех ячеек ассоциативного устройства памяти осуществляется одновременно, то есть в каждой ячейке тега есть необходимая логика, позволяющая посредством побитовой конъюнкции найти данные по их признаку за одно обращение к памя ти (если они там, конечно, присутствуют). Часто поле тегов называют аргументом, а поле с данными — (функцией. В качестве аргумен га при доступе к ассоциативной памяти выступают номер сегмента и номер виртуальной страницы, а в качестве функции от этих аргументов получаем номер (физической страницы. Остается приписать помер ячейки в странице к полученному номеру, и мы получаем искомую команду или операнд.
Оценим достоинства сегментно-страничного способа. Разбиение программы на сегменты позволяет размещать сегменты в памяти целиком. Сегменты разбиты па страницы, все страницы сегмента загружаются в память. Это позволяет уменьшить обращения к отсутствующим страницам, поскольку вероятность выхода за пределы сегмента меньше вероятности выхода за пределы страницы. Страницы исполняемого сегмента находятся в памяти, но при этом они могут находиться не рядом друг с другом, а «россыпью», поскольку диспетчер памяти манипулирует страницами. Наличие сегментов облегчает реализацию разделения программных модулей между параллельными процессами. Возможна и динамическая компоновка задачи. А выделение памяти страницами позволяет минимизировать фрагментацию.
Однако, поскольку этот способ распределения памяти требует очень значительных затрат вычислительных ресурсов н его нс гак просто реализовать, используется он редко, причем в дорогих, мощных вычисли тельных системах. Возможность реализовать сегментно-страничное распределение памяти заложена и в семейство микропроцессоров 180x86, однако вследствие слабой аппаратной поддержки, трудностей при создании систем программирования и оперпцшшной системы, практически он не используется в ПК.
Распределение оперативной памяти в современных ОС для ПК
Первый вопрос, который хочется задать, — это какие ОС следует отпоешь к современным, а какие — нет? Стоит ли в наше время изучать такую «несовременную» ОС, как MS-DOS?1 С нашей точки зрения, прежде всего к современным ОС следует отнести те. что используют аппаратные возможности микропроцессоров, специально заложенные для организации высокопроизводительных и падежных вычислений. Однако эти ОС, как правило, очень сложны и громоздки. Опи занимают большое дисковое пространст во, требуют и большого объема оперативной памяти. Поэтому для решения некоторого класса задач вполне подходят и системы, использующие микропроцессоры в так называемом реальном режиме работы (см. об этом в следующей главе).
В последние годы можно встретить студентов, обучающихся специальностям, иепосредствщшо связанным с вычислительной техникой, которые совсем нс знают DOS-chcicm. Скорее всего, э го является доказательством того, что такие ОС уже не являются современными. Однако достаточно часто для обслуживания компьютера необходимо выполнить простейшие программы — утилиты. Эти программы были созданы для DOS, они не требуют больших ресурсов, для их
Широко известно. что было много версий ОС, которые мы, упрощая ситуацию, относим к MS-DOS. MS DOS — jго вариант фирмы Microsoft реализации дисковой операционной системы [29,52]. Фирма Microsoft прекратила разработку подобных сиск*. и по- сходиен их ремшацней была MS-DOS 6 22 Были (и есть ныне) реализации такого рода систем н от других разработчиков. Поскольку у MS-DOS 6.22 сушею вуют известны! проблемы с 2000 годом, большой популярностью пользуется PC-DOS 7.0 tn IBM
функционирования достаточно запух ппь MS-DOS или аналогичную простую ОС. Однако без выполнения этих программ невозможно порой установить или загрузить иные ОС (хоть и современные, но очень сложные и громоздкие). Поэтому мы считаем правильным сделать хотя бы первичное, пусть по очень глубокое, ознакомление с MS-DOS
Распределение оперативной памяти в MS-DOS
Как известно, MS DOS — это одноирограммная ОС. В ней, конечно, можно организовать запуск резидентных или I SR-задач, ио в целом опа предпазпа iena для выполнения только одного вычислительного процесса Поэтому распределт пие памяти в ней построено по самой простой схеме, которую мы уже рассматривали в разделе «Простое непрерывное распределение и распределение с перекрытием (оверлейные с фук гуры)». Здесь мы лишь уточним некоторые характерные деист.
В IBM PC использовался 16-раврядпый микропрощ < < op 18088, который за счет введения сегментного способа адресации позволял адресоваться к памяти объемом до 1 Мбайт. В последующих ПК (IBM PC AT, АТ386 и др.) было при пято решение поддерживать совместимость с первыми поэтому при работе с DOS прежде всего рассматривают первый мегабайт. Вся эта память разделялась па несколько облас тей, что проиллюстрировано па рис. 2.10. На этом рисунке изображено, что памяти может быть и больше, чем 1 Мбайт, по бо тсс подробное рас- смогрелше этого вопроса мы здесь опустим, отослав желающих изучить данную тему глубже к монографии |9|.
Если не вдаваться в детали можно сказать, что в состав MS-DOS входят следующие основные компоненты
Базовая подсистема ввода/вывода — BIOS (base input-output system), включающая в себя помимо программы тестирования ПК (POST) обработчики прерываний (драйверы), расположенные в постоянном запоминающем устройстве. В конечном итоге, почти все остальные модули MS DOS обращаются к BIOS. Если и не напрямую, то через модули более высокого уровня иерархии.
Модуль расширения BIOS — файл IO.SYS (в других DOS-системах он может называться иначе, например, IBMIO СОМ).
Основной, базовый модуль обработки прерываний DOS — файл MS DOS.SYS Именно этот модуль в основном реализует работу с файловой системой. (В PC DOS аналогичный по значению файл называется IBMDOS.COM)
Командный процессор (интерпретатор команд) — файл COMMAND СОМ
Утилиты и драйверы, расширяющие возможности системы.
Программа загрузки MS-DOS — загрузочная запись (boot record), расположенная на дискете (подробнее о ней и о других загрузчиках см. главу 4).
0000-003FF
1Кб
Таблица векторов прерываний
00400-005FF
512 байт
Глобальные переменные BIOS
Глобальные переменные DOS
В ранних версиях здесь располагались глобальные переменные интерпретатора Бейсик
00600-0А000
35—60 Кб
Модуль IO.SYS
Модуль MSDOS.SYS:
обслуживающие функции;
буферы, рабочие и управляющие области;
инсталируемые драйверы
Резидентная часть COMMAND.COM
обработка программных прерываний;
системная программа загрузки:
программа загрузки транзитной части COMMAND.COM
Размер этой области зависит . от версии MS-DOS и, главное, от конфигурационного файла CONFIG.SYS
= 560 Кб
Область памяти для выполнения программ пользователя и утилит MS-DOS. В эту область попадают программы типа * СОМ и * EXE
Объем этой области сильно зависит от объема, занимаемого ядром ОС. Программа может перекрывать транзитную область COMMAND.COM
Область расположения стека исполняющейся программы
Стек «растет» снизу вверх
18 Кб
Транзитная часть командного процессора COMMAND.COM
Собственно командный * интерпретатор
A0000-C7FFF
160 Кб
Видеопамять Область и размер используемого видеобуфера зависит от используемого режима
При работе в текстовом режиме область памяти
>- A0000-B0000 свободна и может быть использована в программе
С8000-Е0000
96 Кб
Зарезервировано для расширения BIOS
F0000-FFFFF
64 Кб
Область ROM BIOS (System BIOS)
Обычно объем этой области равен 32 Кб. но может достигать и 128 Кб, занимая и младшие адреса
100000-10FFFF
High Memory Area
При наличии драйвера HIMEM.SYS здесь можно расположить основные системные файлы MS-DOS, освобождая тем самым область основной памяти в первом мегабайте
Может использоваться при наличии специальных
>- драйверов. Используются спецификации
XMS и EMS
Рис. 2.10. Распределение оперативной памяти в MS-DOS
Вся память в соответствии с архитектурой IBM PC условно может быть разбита на три части.
В самых младших адресах памяти (первые 1024 ячейки) размещается таблица векторов прерывании (см. раздел «Система прерываний 32-разрядпых микропроцессоров 180x86», глава 3). Это связано с аппаратной реализацией процессора 18088, на котором была реализована ПК. В последующих процессорах (начиная с i80286) адрес таблицы прерываний определяется через содержимое соответствующего регистра, по для обеспечения полной совместимости с первым процессором при включении или аппаратном сбросе в этот регистр заносятся нули. При желании, однако, в случае использования современных микропроцессоров 180x86 можно разместить векторы прерываний и в другой области.
Вторая часть памяти отводи гея для размещения программных модулей самой MS-DOS и для программ пользователя. Рассмотрим их размещение чуть ниже. Здесь, однако, заметим, что эта область памяти называется Conventional Memory (основная, стандартная память).
Наконец, третья часть адресного пространства отведена для постоянных запоминающих устройств и функционирования некоторых устройств ввода/вывода. Эта область памяти получила название UMA (upper memory areas — область верхней памяти).
В младших адресах основной памяти размещается то, что можно назвать ядром этой ОС — системные переменные, основные программные модули, блоки данных для буферирования операции ввода/вывода. Для управления ус тройствами, драйверы которых не входят в базовую подсистему ввода/вывода, загружаются так называемые загружаемые (или инсталлируемые) драйверы. Перечень инсталлируемых драйверов определяется специальным конфигурационным файлом CONFIG.SYS. После загрузки расширения BIOS — файла IO.SYS — последний (загрузив модуль MSDOS.SYS) считывает файл CONFIG.SYS и уже в соответствии с ним подгружает в память необходимые драйверы. Кстати, в конфигурационном файле CONFIG.SYS могут иметься и операторы, указывающие на количество буферов, отводимых для ускорения операций ввода/вывода, и на количество файлов, которые могут обрабатываться (для работы с файлами необходимо зарезервировать место в памяти для храпения управляющих структур, с помощью которых выполняются операции с записями файла). В случае использования микропроцессоров 180x86 и наличия в памяти драйвера HIMEM SYS модули IO.SYS и MSDOS.SYS могут быть размещены за пределами первого мегабайта в области, которая получила название НМА (high memory area).
Память с адресами, большими чем WFFFFh, может быть использована в DOS- программах при выполнении их на микропроцессорах, имеющих такую возможность. Так, например, микропроцессор i80286 имел 24-разрядную шину адреса, а i80386 — уже 32-разрядную шипу адреса. Но для этого с помощью специальных драйверов необходимо переключать процессор в другой режим работы, при котором он сможет использовать адреса выше 10FFFF11. Широкое распространение получили две основные спецификации: XMS (extended memory specification) и EM.S (expanded memory specification). Поскольку основные утилиты, необходимые для обслуживания ПК, как правило, не используют эти спецификации, мы не будем здесь их рассматривать.
Остальные программные модули MS-DOS (в принципе, большинство из них яв лжчея утилитами) оформлены как обычные исполняемые файлы. В основном они являются транзитными модулями, то есть загружаются в память только па время своей работы, хотя средн них имеются и TSR-про граммы.
Для того чтобы предоставить больше памяти программам пользователя, в MS-DOS применено то же решение, что и во многих других простейших ОС — командный процессор COMMAND.COM сделан состоящим из двух частей. Первая часть является резпдептпой, опа размещается в области ядра. Вторая часть транзитная; опа размешается в области старших адресов раздела памяти, выделяемой для программ пользователя. И если программа пользователя перекрывает собой область, в котором была расположена транзитная часть командного процессора, то последний при необходимости восстанавливает в памяти свою транзитную часть, поскольку после выполнения программы она возвращает управление резидентной части COMMAND СОМ.
Поскольку размер основной памяти (conventional memoiy) относительно небольшой, то очень часто системы программирования реализуют оверлейные структуры. Для этого в MS-DOS есть специальные вызовы.
Распределение оперативной памяти
в Microsoft Windows 95/98
С точки зрения базовой архитектуры ОС Windows 95/98 они обе являются 32- разрядными, многопотоковыми ОС с вытесняющей многозадачностью. Основной пользовательский интерфейс этих ОС — графический.
Для своей загрузки они используют операционную систему MS-DOS 7.Х (MS- DOS 98), и в случае если в файле MSDOS.SYS в секции [Options] прописано BootGUI = 0, то процессор работает в обычном реальном режиме (см. следующий , раздел). Распределение памяти в MS-DOS 7.Х. такое же, как и в предыдущих версиях DOS. Однако при загрузке GUI-интерфейса перед загрузкой ядра Windows 95/98 процессор переключается в защищенный режим работы и начинает распределять память уже с помощью страничного механизма.
Использование так называемой плоской модели памяти, при которой все возможные сегменты, которые может использовать программист, совпадают друг с другом и имеют максимально возможный размер, определяемый системными соглашениями данной ОС, приводит к тому, что с точки зрения программиста память получается неструктурированной. За счет представления адреса как нары (Р, i) память можно трактовать и как двумерную, то есть «плоскую», но при этом ее можно трактовать и как линейную, и это существенно облегчает создание системного программного обеспечения и прикладных программ с помощью соответствующих систем программирования.
Таким образом, в системе фактически действует только страничный механизм преобразования виртуальных адресов в физические. Программы используют классическую «small» (малую) модель памяти |73]. Каждая прикладная программа определяется 32-битными адресами, в которых сегмент кода имеет то же значение. что п сегменты данных. Единственный сегмент программы отображается непосредственно в область виртуального линейного адресного простраш тва. кото-
рый, в свою очередь, состоит из 4 килобайтных страниц. Каждая страница может располагаться где угодно в оперативной памяти (естественно, в том месте, куда ее разместит диспетчер памяти, который сам находится в невыгружаемой области) или может быть перемещена на диск, если нс запрещено использовать страничный файл.
Младшие адреса виртуального адресною пространства совместно используются всеми процессами. Э го сделано для обеспечения совместимости с драйверами устройств реального режима, резидентными программами и некоторыми 16-раз- рядными программами Windows. Безусловно, эю плохое решение с точки зрения надежности, поскольку оно приводит к тому, что любой процесс может непреднамеренно (или же, наоборот, специально) испортить компоненты, находящиеся в этих адресах.
В Windows 95/98 каждая 32-разрядпая прикладная программа выполняется в своем собственном адресном пространстве, ио все они используют совместно один и гот же 32-разрядный системный код. Доступ к чужим адресным пространствам в принципе возможен. Другими словами, виртуальные адресные пространства не используют всех аппаратных средств защиты, заложенных в микропроцессор В результате неправильно написанная 32 разрядная прикладная программа может привести к аварийному сбою всей системы. Все 16-битовые прикладные программы Windows разделяют общее адресное пространство, поэтому они так же уязвимы друг перед другом, как и в среде Windows З.Х.
Системный код Windows 95 размещается выше границы 2 Loaitr. В пространстве с отметками 2 и 3 Гбайт находятся системные библиотеки DLL1, используемые несколькими программами. Заметим, что в 32-бнтовых микропроцессорах семейства 180x86 имеются четыре уровня защиты, именуемые кольцами с номерами от 0 до 3. Кольцо с номером 0 является наиболее привилегированным, то есть максимально защищенным. Компоненты системы Windows 95, относящиеся к кольцу 0, отображаются на виргуДдыюе адресное пространство между 3 и 4 Гбайт-. К этим компонентам относятся собственно ядро Windows, подсистема управления виртуальными машинами, модули файловой системы и виртуальные драйверы (VxD).
Область памяти между 2 и 4 Гбайт адресного пространства каждой 32-разрядпой прикладной программы совместно используется всеми 32-разрядными прикладными прогр шмами Такая организация позволяет обслуживать вызовы API непосредственно в адресном пространстве прикладной программы и ограничивает размер рабочего множества. Однако за это приходится расплачиваться снижением падежное!и. Ничто не может помешать программе, содержащей ошибку, произвести запись в адреса, принадлежащие системным DLL, и вызвать крах всей спсгемы.
В области между 2 и 3 Гбайт также находятся все запускаемые 16-ра рядные прикладные программы Windows. С целью обеспечения совместимости : ти программы выполняю 1ся в совместно используемом адресном пространстве, где они могут испортить друг друга так же, как и в Windows 3.x.
' DLL (dynan ic link library) динамически загружаемы® библиотечный модул!
Адреса памяти ниже 4 Мбайт также отображаются в адресное пространство каждой прикладной программы и совместно используются всеми процессами. Благодаря этому становится возможной совместимость с существующими драйверами реального режима, которым необходим доступ к этим адресам. Это делает еще одну область памяти не защищенной от случайной записи. К самым нижним 64 Кбайт этого адресного пространства 32-разрядные прикладные программы обращаться не могут, что дает возможность перехватывать неверные указатели, но 16-разрядные программы, которые, возможно, содержат ошибки, могут записывать туда данные.
Вышеизложенную модель распределения памяти можно проиллюстрировать с помощью рис. 2 11
4 Гб
3 Гб3 ГбСистемные компоненты, относящися к О кольцу звщитыСистемные компоненты, относящися к О кольцу звщитыСистемные DLLПрикладные программы Win 16Совместно используемые DLLСистемные DLLПрикладные программы Win 16Совместно используемые DLLАдреса между 2 и 4 Гб отображаются в адресное пространство каждой программы Win32 и совместно используютсяАдреса между 2 и 4 Гб отображаются в адресное пространство каждой программы Win32 и совместно используются
3 Гб
3 Гб
Системные компоненты, относящися к О кольцу звщиты
Системные компоненты, относящися к О кольцу звщиты
Системные DLL
Прикладные программы Win 16
Совместно используемые DLL
Системные DLL
Прикладные программы Win 16
Совместно используемые DLL
Адреса между 2 и 4 Гб отображаются в адресное пространство каждой программы Win32 и совместно используются
Адреса между 2 и 4 Гб отображаются в адресное пространство каждой программы Win32 и совместно используются
2 Гб2 ГбПрикладные программы Win 32Прикладные программы Win 324 Мб64 КбО4 Мб64 КбОКомпоненты реального режимаКомпоненты реального режимаВ этой области адресного пространстве каждой прикладной программы располагается свое собственное адресное прос ранство. «Личные» адресные пространства других программ евидимы для программы, и следователыно, она не может никак измените их содержимоеЭта областы используется всеми роцессамиВ этой области адресного пространстве каждой прикладной программы располагается свое собственное адресное прос ранство. «Личные» адресные пространства других программ евидимы для программы, и следователыно, она не может никак измените их содержимоеЭта областы используется всеми роцессами
2 Гб
2 Гб
Прикладные программы Win 32
Прикладные программы Win 32
4 Мб
64 Кб
О
4 Мб
64 Кб
О
Компоненты реального режима
Компоненты реального режима
В этой области адресного пространстве каждой прикладной программы располагается свое собственное адресное прос ранство. «Личные» адресные пространства других программ евидимы для программы, и следователыно, она не может никак измените их содержимое
Эта областы используется всеми роцессами
В этой области адресного пространстве каждой прикладной программы располагается свое собственное адресное прос ранство. «Личные» адресные пространства других программ евидимы для программы, и следователыно, она не может никак измените их содержимое
Эта областы используется всеми роцессами
Рис. 2.11. Модель памяти ОС Windows 95/98
Минимально допустимый объем оперативной памяти начиная с которого ОС Windows 95 может функционировать, равен 4 Мбайт, однако при таком объеме пробуксовка столь велика, что практически работать нельзя Страничный файл, с помощью которого реализуется механизм виртуальной памяти, по умолчанию располагается в каталоге самой Windows и имеет переменный размер. Система отслеживает его длину, увеличивая или сокращая этот файл при необходимости. Вмес те с фрагментацией файла подкачки это приводит к тому, что быстродействие системы становится меньше, чем если бы файл был фиксированного размера и располагался в смежных кластерах (был бы дефрагментирован). Сделать файл подкачки заданного размера можно либо через специально разработанный для этого апплет (Панель управления ► Система ► Быстродействие ► Файловая система), либо просто прописав в файле SYSTEM INI в секции [386Enh] строчки с указанием диска и имени этого файла, например:
PagingDrive-C:
PagingF11e=C \PageFile.sys
М1и Pag тngF 1eSize-65536
MaxPagingFi1eSize=262144
Первая и вторая строчки указывают имя страничного файла и его размещение, а две последних — начальный и предельный размер страничного файла (значения указываются в килобайтах). Для определения необходимого минимального размера этого файла можно рекомендовать запустить программу SysMon (системный монитор) и, выбрав в качест ве наб подаемых параметров размер файла подкачки и объем свободной памяти, оценить потребности в памяти, запуская те приложения, с которыми чаще всего приходится работать.
Распределение оперативной памяти в Microsoft Windows NT
В операционных системах Windows NT тоже испольнуется плоская модель памяти. Заметим, что Windows NT 4.0 server практически не отличается от Windows NT 4.0 workstation; разница лишь в наличии у сервера некоторых дополнительных служб, дополнительных утилит для управления доменом и несколько иных значений в настройках системного реестра. Однако схема распределения возможного виртуального адресного пространства в системах Windows NT разительно отличается от модели памяти Windows 95/98. Прежде всего, в отличие от Windows 95/98 в гораздо большей степени используется ряд серьезных аппаратных средств защиты, имеющихся в микропроцессорах, а также применено принципиально другое логическое распределение адресного пространства.
Во-первых, все системные программные модули находятся в своих собственных виртуальных адресных пространствах, и доступ к ним со стороны прикладных программ невозможен. Ядро системы и несколько драйверов работают в пулевом кольце защиты в отдельном адресном пространстве.
Во-вторых, остальные программные модули самой операционной системы, которые выступают как серверные процессы по отношен ню к прикладным программам (клиентам), функционируют также н своем собственном системном виртуальном адресном пространстве, невидимом для прикладных процессов. Логическое распределение адресных пространств приведено на рис. 2.12.
Прикладным программам выделяется 2 Гбайт локального (собственного) линейного (неструктурированного) адресного пространства oi границы 64 Кбайт до 2 Гбайт (первые 64 Кбайт полностью недоступны). Прикладные программы изолированы друг от друга, хо1я могут общаться через буфер обмена (clipboard), механизмы DDE и OLE.
4 Гб4 Гб
4 Гб
4 Гб
Код ядра(работает в кольцезащиты с номером 0)
2 Гб2 ГбDLL Win32 клиентской стороныDLL Win32 клиентской стороныПрикладныепрограммы Win32 Процесс системного (у каждой программы серверасвое собственноевиртуальноепространствопамяти)Прикладныепрограммы Win32 Процесс системного (у каждой программы серверасвое собственноевиртуальноепространствопамяти)Прикладные программы обращаются к DLL, которые перенаправляют обращение к системеПрикладные программы обращаются к DLL, которые перенаправляют обращение к системе64 Кб064 Кб0Виртуальные машиныWin16Виртуальные машиныWin16Этот системный код находится в своем собственном адресном пространстве и недоступен вызывающим его процессамЭтот системный код находится в своем собственном адресном пространстве и недоступен вызывающим его процессам
2 Гб
2 Гб
DLL Win32 клиентской стороны
DLL Win32 клиентской стороны
Прикладные
программы Win32 Процесс системного (у каждой программы сервера
свое собственное
виртуальное
пространство
памяти)
Прикладные
программы Win32 Процесс системного (у каждой программы сервера
свое собственное
виртуальное
пространство
памяти)
Прикладные программы обращаются к DLL, которые перенаправляют обращение к системе
Прикладные программы обращаются к DLL, которые перенаправляют обращение к системе
64 Кб
0
64 Кб
0
Виртуальные машиныWin16
Виртуальные машиныWin16
Этот системный код находится в своем собственном адресном пространстве и недоступен вызывающим его процессам
Этот системный код находится в своем собственном адресном пространстве и недоступен вызывающим его процессам
Рис. 2.12. Модель распределения виртуальной памяти в Windows NT
В верхней части каждой 2-гпгабайтиой области прикладной программы размещен код системных DLL кольца 3, который выполняет перенаправление вызовов в совершенно изолированное адресное пространство, где содержится уже собст- ведшо системный кол. Этот системный кол, выступающий как сервер-процесс (server process), проверяет значения параметров, исполняет запрошенную функцию и пересылает результаты назад в адресное пространство прикладной программы. Хотя сервер-процесс сам по себе остается процессом прикладного уровня он полностью защищен от вызывающей его прикладной программы и изолирован от нее.
Между отметками 2 и 4 Гбайт расположены низкоуровневые системные компоненты Windows NT кольца 0, в том числе ядро, планировщик потоков и диспетчер виртуальной памяти. Системные страницы в в гой области наделены привилегиями супервизора, которые задаются физическими схемами кольцевой защиты процессора. Это делает низкоуровневый системный код невидимым и недоступным для записи для программ прикладного уровня, ио приводит к падению производительности во время переходов между кольцами.
Для 16-разрядпых прикладных Windows-программ ОС Windows NT реализует сеансы Windows он Windows (WOW). В отличие от Windows 95/98 ОС Windows NT дает возможность выполнять 16-разрядпые программы Windows индивидуально в собственных пространствах памяти или совместно в разделяемом адресном пространстве. Почти во всех случаях 16- и 32-разрядпые прикладные программы Windows могут свободно взаимодействовать, используя OLE, независимо от того, выполняются они в отдельной или общей памяти. Собственные
прикладные программы и сеансы WOW выполняются в режиме вытесняющей многозадачности, основанной на управлении отдельными потоками. Множественные 16 разрядные прикладные программы Windows в одном сеансе WOW выполняются в соотве стяни с кооперативной моде чью многозадачности. Windows NT может также выполнять в многозадачном режиме несколько сеансов DOS Поскольку Windows NT имеет полностью 32-разрядпую архитектуру, не существует теорсгических ограничении па ресурсы GDI п USER.
При запуске приложения создастся процесс со своей информационной структурой. В рамках процесса запускается палача При необходимости этот тред (задача) может запустить множество других тредов (зада i), кгяорые будут выполняться параллельно в рамках одного процесса. Очевидно, что множество запущенных процессов также выполняются параллельно и каждый пз процессов может представлять из себя мультизадачное приложение. Задачи (треды) в рамках одною процесса выполняются в едином виртуальном адресном пространстве, а процессы выполняются в различных виртуальных адресных пространствах. Отображение различных виртуальных адресных пропрапств исполняющихся процессов па физическую память реализует сама ОС; именно корректное выполнение этой задачи гарантирует изоляцию приложении от невмешательства процессов Для обеспечения взаимодействия между выполняющимися приложениями и между приложениями и кодом самой операционной системы используются соогвегст вующпе механизмы защиты памяти, поддерживаемые аппаратурой микропроцессора (см. следующую главу).
Процессами выделения памяти, ее резервирования, освобождения и подкачки управляет диспетчер виртуальной памяти Windows NT (Windows NT virtual memory manage-, VMM). В своей работе этот компонент реализует сложную стратегию учета требований к коду и данным процесса для минимизации доступа к диску, поскольку реализация виртуальной памяти часто приводит к большому количеству дисковых операций.
КаЖддя виртуальная страница памяти, отображаемая па физическую страницу, иереиошгея в так называемый страничный фрейм (page frame). Прежде чем код или данные можно будет переместить с диска в память, диспетчер виртуальной памяти (модуль VMM) должен найти или создать свободный страничный фрейм или фрейм, заполненный нулями Заметим, что заполнение страниц пулями представляет собой одно пз требований стандарта па системы безопасности уровня С2 , принятого правительством США Страничные фреймы должны заполняться пулями для того, чтобы исключить возможность непользовання их предыдущего содержимого другими процессами. Чтобы фрейм можно было Освободить, необходимо скопировать на диск изменения в его странице данных, и только нос ie этого фрейм можно будет повторно использовать. Программы, как правило, не меняют страницы кода. С границы кода, в которые программы не внесли измепе- пш(, можно удалить.
11Диспетчер виртуальной памяти может быстро и относительно легко удовлетворить программные прерывания типа «ошибка страницы» (page fault) Что касается аппаратных прерываний типа «ошибка страницы», то они приводят к иод
1
1
качке (paging), которая снижает производительность системы. Мы уже говорили о том, что в Windows NT, к большому сожалению, выбрана дисциплина FIFO для замещения страниц, а не более эффективные дисциплины LRU и LFU.
Когда процесс использует код или данные, находящиеся в физической памяти система резервирует место для этой страницы в файле подкачки Pagefile.sys на диске. Это делается с расчетом на тот случай, что данные потребуется выгрузить на диск. Файл Pagefile.sys представляет собой зарезервированный блок дискового пространства, который используется для выгрузки страниц, помеченных как «грязные», при необходимости освобождения физической памяти. Заметим, что этот файл может быть как непрерывным, так и фрагментированным; он может быть расположен на системном диске либо на любом другом и даже на нескольких дисках. Размер этого страничного файла ограничивает объем данных, которые могут храниться во внешней памяти при использовании механизмов виртуальной памяти. По умолчанию размер файла подкачки устанавливается равным объему физической памяти плюс 12 Мбайт, однако пользователь имеет возможность изменить его размер по своему усмотрению Проблема нехватки виртуальной памяти часто может быть решена за счет увеличения размера файла подкачки. В системах Windows NT 4.0 объекты, создаваемые и используемые приложениями и операционной системой, хранятся в так называемых пулах памяти (memory pools). Доступ к этим пулам может быть получен только в привилегированном режиме работы процессора, в котором работают компоненты операционной системы. Поэтому для того, чтобы объекты, хранящиеся в пулах, стали видимы тредам приложений, эти треды должны переключиться в привилет ировапный режим. Перемещаемый или нерезидентный пул (paged pool) содержит объекты, которые могут быть при необходимости выгружены на диск Неперемещаемый или резидентный пул (nonpaged pool) содержит объекты, которые должны постоянно находиться в памяти. В частности, к такого рода объектам относятся структуры данных, используемые процедурами обработки прерываний, а также структуры, используемые для предотвращения конфликтов в мультипроцессорных системах. Исходный размер пулов определяется объемом физической памяти, доступной Windows NT. Впоследствии размер пула устанавливается динамически и, в зависимости от работающих в системе приложений и сервисов, будет вменяться в широком диапазоне.
Вся виртуальная намять в Windows NT подразделяется на классы: зарезервированную (reserved), выделенную (committed) и доступную (available).
Зарезервированная память представляет собой набор непрерывных адресов, которые диспетчер виртуальной памяти (VMM) выделяет для процесса, но не учитывает в общей квоте памяти процесса до тех пор, пока опа не будет фактически использована. Когда процессу требуется выполнить запись в па- мять, ему выделяется нужный объем из зарезервированном памяти. Если процессу потребуется больший объем памяти, то дополнительная память может быть одновременно зарезервирована и использована, если в системе имеется доступная память
Память выделена, если Диспетчер VMM резервирует для нее место в файле Pagefile.sys на тот случай, когда потребуется выгрузить содержимое памяти
на диск. Объем выделенной памяти процесса характеризует фактически потребляемый им объем памяти. Выделенная память ограничивается размером файла подкачки. Предельный объем выделенной памяти в системе (commit limit) определяется гем, какой объем памяти можно выделить процессам без увеличения размеров файла подкачки. Если в системе имеется достаточный объем дискового пространства, то файл подкачки может быть увеличен и, тем самым, будет расширен предельный объем выделенной памяти.
Вся память, которая не является ни выделенной, ни зарезервированной, является доступной. К доступной относится свободная память, обнуленная память (освобожденная и заполненная нулями), а также память, находящаяся в списке ожидания (standby list), которая была удалена из рабочего набора процесса, ио может быть затребована вновь.
Контрольные вопросы и задачи
Вопросы для проверки
Перечислите и поясните основные функции ОС, которые связаны с управлением задачами.
Какие дисциплины диспетчеризации задач вы знаете? Опишите их.
Что такое «гарантия обслуживания»? Как ее можно реализовать?
Сравните механизмы диспетчеризации задач в ОС Windows NT и OS/2. В чем заключаются основные различия?
Л. Опишите механизм динамической диспетчеризации, реализованный в UNIX- системах.
Что такое «виртуальный адрес», «виртуальное адресное пространство»? Чем (в общем случае) определяется максимально возможный объем виртуального адресного пространст ва программы?
Что такое «фрагментация памяти»? Какой метод распределения памяти позволяет добиться минимальной фрагментации?
Что такое «уплотнение памяти»? Когда оно применяется?
Объясните сегментный способ организации виртуальной памяти Что представляет собой (в общем случае) дескриптор сегмента?
Сравните сегментный и страничный способы организации виртуальной памя ти. Перечислите достоинства и недостатки каждого.
Какие дисциплины применяются для решения задачи замещения страниц? Какие из них являются наиболее эффективными и как они реализуются?
Что такое «рабочее множество»? Что позволяет разрешить реализация этого понятия?
Изложите принципы распределения памяти в MS-DOS
Что означает термин «плоская модель памяти»? В чем заключаются достоинства (и недостатки, если они есть) использования этой модели?
Г ЛАВА 3 Особенности архитектуры микропроцессоров 180x86
В рамках данного учебного пособия мы, естественно, нс будем рассматривать все многообразие современных 32-разрядных микропроцессоров, используемых в ПК и иных вычислительных системах. Здесь мы ограничимся рассмотрением только архитектурных, а не технических характеристик микропроцессоров Под обозначением 180x86 будем понимать любые 32-битовые микропроцессоры, имеющие такой же основной набор команд, как и в первом 32-битовом микропроцессоре Intel 80386, и те же архитектурные решения, что и в микропроцессо- рах фирмы Intel.
Реальный и защищенный режимы работы процессора
Широко известно, что первым микропроцессором, на базе которого была создана IBM PC, бь 1 Intel 8088. Этот микропроцессор отличался от первого 16-разряд- ного микропроцессора фирмы Intel — 8086 - прежде всего тем, что у него была 8-битовая шина данных, а не 16-битовая (как у 8086) Оба эти микропроцессора предназначались для создания вычислительных устройств, которые бы работали в однозадачном режиме, то есть специальных аппаратных средств для поддерж кн надежных и эффективных мультипрограммных ОС в них не бы ю.
Однако к тому времени, когда разработчики осознали необходимость включения в микропроцессор специальной аппаратной поддержки для мультипрограммных вычислений, уже было создано очень много программных продуктов. Поэтому для совместимости с первыми компьютерами в последующих версиях микропроцессоров была реализована возможность использовать их в двух режимах — реальном (real mode так назвали режим работы первых 16-битовых микропро-
цессоров) и защищенном (protected mode — оапачает, что параллельные вычисления могут быть защищены аппаратно программными мс ани мамп).
Подробно рассмацтивать архитектуру первых 16-битовых микрон ю к соров 18086/18088 мы не будем, поскольку' этот материал должен изучаться в предыдущих дисциплинах учебного плана. Для тех же, кто с ним нс знаком, можно рекомендовать такие книги, как [52, 73], и многие другие. Одпеко напомним, что в этих микропроцессорах (а значит, и в остальных микропроцессорах семейства 180x86 при работе их в реальном режиме) обращение к памяти с ш зможг гм адресным пространством в 1 Мбайт осуществляется посредством механизма сегментной адресации (рис. 3.1). Этот механизм был использован для увеличения количества разрядов, участвующих в указании адреса ячейки памяти с которой в данный момент осуществляется работа, с 16 до 20 и тем самым уве- щчепия объема памяти
Конкретизируем задачу и ограничимся рассмотрением определения адреса команды. Для адресации операндов используется аналогичный механизм. только участвуют в этом случае другие сегментные регистры. Напомним, что для определения физического адреса команды содержимое сегментного регистра CS (code segment) умножается на 16 за счет добавления справа (к младшим битам) четырех нулей, после чего к полученному значению прибавляется содержимое указателя команд (регистр IP, instruction pointer). Получается двадцатибитовос значение, которое и позволяет указать любой байт in 22
Ре истр CSРе истр CSРе истр IPРе истр IPФизический адресФизический адресРис. 3.1. Схема определения физического адреса для процессора 8086Рис. 3.1. Схема определения физического адреса для процессора 8086
Ре истр CS
Ре истр CS
Ре истр IP
Ре истр IP
Физический адрес
Физический адрес
Рис. 3.1. Схема определения физического адреса для процессора 8086
Рис. 3.1. Схема определения физического адреса для процессора 8086
В защищенном режиме работы определение физического адреса осуществляется совершенно иначе. Прежде всего используется сегментный механизм для организации виртуальной памяти. При этоат адресе задаются 32-бт(>вымп значениями. Кроме этого, возможна страничная трансляция адресов, также с 32-битовыми
1 Па самом деле, поскольку происходит именно сложение и каждое из слагаемых может иметь значение в интервале от пуля До 21С-1 = 65535 ” 64К, мы можем указать адрес начала сегмента, равный FFFFFFFF00I1, п к нему прибавить смещение FFFFFFFFH. В атом случае мы получим переполнение разрядной сетки, но для современных 32-битоных процессоров (и для уже забытого i80286) имеется возможность указать первые 64 Кбайт выше первого мегабайта
значениями. Наконец, при работе в защищенном режиме, который по умолчанию предполагает 32-битовый код, возможно исполнение двоичных программ, созданных для работы микропроцессора в 16-битовом режиме. Для этого введен режим виртуальной 16-битовой машины и 20-битовые адреса реального режима транслируются с помощью страничного механизма в 32-битовые значения защищенного режима. Наконец, есть еще один режим — 16-битовый защищенный, позволяющий 32-битовым микропроцессорам выполнять защищенный 16-битовый код, который был характерен для микропроцессора 80286. Правда, следует отметить, что это последний режим практически нс используется, поскольку программ, созданных для него, не так уж и много.
Для изучения этих возможностей рассмотрим сначала новые архитектурные возможности микропроцессоров 180x86.
Новые системные регистры микропроцессоров i80x86
Основные регистры микропроцессора 180x86, знание которых необходимо для понимания защищенного режима работы, приведены на рис. 3.2. Следует обратить внимание на следующее:
указатель команды EIP — 32 битовый регистр, младшие 16 разрядов этого регистра есть регистр IP;
регистр флагов EFLAGS — 32 бита, младшие 16 разрядов представляют регистр FLAGS;
регистры общего назначения EAX, ЕВХ, ЕСХ, EDX, а также ESP, EBP, ESI, EDI — 32-битовыс, однако их младшие 16 разрядов представляют собой из цветные регистры АХ, ВХ, СХ, DX, SP, BP, SI, DI;
сегментные регистры CS, SS, DS, ES, FS, GS — 16-битовые. При каждом из регистров CS, SS, DS, ES, FS, GS изображены пунктиром скрытые от программистов (недоступны никому, кроме собственно микропроцессора) 64-би- товые регистры, в которые загружаются дескрипторы соответствующих сегментов;
регистр-указатель на локальную таблицу сегментов текущей задачи— LDTR (16 битов). При этом регистре также имеется «теневой» (скрытый от программиста) 64-битовый регистр, в который микропроцессор заносит дескриптор, указывающий на таблиц)' дескрипторов сегментов задачи, описывающих ее локальное виртуальное адресное пространство;
регистр-указатель задачи TR* (16 битов). Указывает на дескриптор в глобальной таблице дескрипторов, позволяющий получить доступ к дескриптору задачи TSS — информационной структуре, которую поддерживает микропроцессор для управления задачами;
регистр GDTR (48 битов) глобальной таблицы GDT, содержащей как дескрипторы общих сегментов, так и специальные системные дескрипторы. В частности, в GDTR находятся дескрипторы, с помощь которых можно получить доступ к сегментам TSS;
регистр IDTR (48 битов) таблицы дескрипторов прерываний. Содержит информацию, необходимую для доступа к «таблице прерываний» IDT;
управляющие регистры CR0 — CR3 (32-битовые) и некоторые другие регистры.
EAXAXEBXBXECXCXEDXDXEAXAXEBXBXECXCXEDXDX16 8р16 8рESPSPEBPBPESISIEDIDIESPSPEBPBPESISIEDIDI168 О168 О
EAX
AX
EBX
BX
ECX
CX
EDX
DX
EAX
AX
EBX
BX
ECX
CX
EDX
DX
16 8р
16 8р
ESP
SP
EBP
BP
ESI
SI
EDI
DI
ESP
SP
EBP
BP
ESI
SI
EDI
DI
168 О
168 О
634715634715Права доступаПрава доступаБазовый адресБазовый адрест' Длина сегментат' Длина сегмента.48..48.Базовый адресДлина сегмента160Базовый адресДлина сегмента160GDTRIDTRGDTRIDTR
634715
634715
Права доступа
Права доступа
Базовый адрес
Базовый адрес
т
' Длина сегмента
т
' Длина сегмента
.48.
.48.
Базовый адрес
Длина сегмента
16
0
Базовый адрес
Длина сегмента
16
0
GDTR
IDTR
GDTR
IDTR
EIP
IP
EFLAGS
FLAGS
Рис. 3.2 Основные системные регистры микропроцессоров 680x86
Управляющий регистр CR0 содержит целый ряд флагов, которые определяют режимы работы микропроцессора. Подробно об этих флагах можно прочитать в книгах [2, 22, 48]. Мы же просто ограничимся тем фактом, что самый младший бит (РЕ, protect enable) этого регистра определяет режим работы процессора. При РЕ=О процессор функционирует в реальном режиме рабсл ы, а при единичном значении микропроцессор переключается в защищенный режим. Самый старший биг регистра CR0 (бит PG, paging) определяет, включен (PG-1) или нет (PG=0) режим страничного преобразования адресов.
Регистр CR2 предназначен для размещения в нем адреса подпрограммы обработки страничного исключения, то есть в случае использования страничного механизма отображения памяти обращение к отсутствующей странице будет вызывать переход на соответствующую подпрограмму диспетчера накоп и, и для определения этом подпрограммы будет задействован регистр CR2
Регистр CR3 содержит номер физической страницы, в которой располагается таблица каталогов таблиц страниц текущем задачи. Очевидно, что, приписав к этому номеру нули, мы попадем на начало этой страницы.
Адресация в 32-разрядных микропроцессорах180x86 при работе в защищенном режиме
Поддержка сегментного способа организации виртуальной памяти
Как мы уже знаем, для организации эффективной и падежной работы вычислительной системы в мультипрограммном режиме необходимо иметь соответствующие аппаратные механизмы, поддерживающие независимость адресных пространств каждой задачи и в то же время позволяющие организовать обмен данными и разделение кода. Для этого желательно выполнение следующих двух требований:
чтобы у каждого вычислительного процесса могло быть свое собственное (личное, локальное) адресное пространство, которое никак не может пересекаться с адресными пространствами других задач;
чтобы существовало общее (разделяемое) адресное пространство.
Поэтому в микропроцессорах i80x86 реализован сегментный способ организации распределения памяти. Помимо этого, в этих микропроцессорах может быть задействована и страничная трансляция. Поскольку для каждого сегмента нужен дескриптор, устройство управления памятью поддерживает соответствующую информационную структуру. Формат дескриптора сегмента приведен на рис. 3.3 Поля дескриптора (базовый адрес, поле предела) размещены в дескрипторе нс непрерывно, а в разбивку, во-первых, из-за того, чго разработчики постарались минимизировать количество перекрестных соединений в полупроводниксиюн
структуре микропроцессора, а во-вторых — вследствие необходимости обеспечить потную совместимость* микропроцессоров (предыдущий микропроцессор i80286 работал с 16-битовым кодом и тоже поддерживал сегментный механизм реализации виртуальной памяти). Необходимо заметить, что формат дескриптора сегмента, изображенный па рис. 3.3, справедлив только для случая нахождения соответствующего сегмента в оперативной памяти. Если же бит присутствия в поле прав доступа равен нулю (сегмент отсутствует в памяти), то все биты, за исключением поля прав доступа, считаются неопределенными и могут использоваться системными программистами (для указания адреса сегмента во внешней памяти) произвольным образом.
Старшее двойное слово дескриптора
31231915117
База сегмента (биты 31-24)
G
D
0
0
Биты 19-16 поля Limit
Р DPL S Туре А
База сегмента (биты 23-16)
Байт прав доступа
Базовый адрес сегмента (биты 15-0)
Limit-размер (предел) сегмента (биты 15-0)
31150
Первое (младшее) двойное слово дескриптора
Рис. 3.3. Дескриптор сегмента
Локальное адресное пространство задачи определяется через таблицу LI)'I (local descr ptor table). У каждой задачи может быть свое локальное адресное пространство Общее пли глобальное адресное пространство определяется через таблицу (ДУГ (global descriptor table). Семо собой, что работу с этими таблицами (их заполнение и последующую модификацию) должна осуществлять операционная система. Доступ к таблиц м LDT и GDT со стороны прикладных задач должен быть исключен.
При переключении микропроцессора в защищенный режим он начинает совершенно другим образом, чем в реальном режиме, вычислять физические адреса команд и операндов. Прежде всего, содержимое сегментных регистров пигерпрс- тпруется иначе: считается, что там содержится не адрес начала сегмента, а номер соответствующего cei мента. Для того чтобы подчеркнуть этот факт, сегментные регистры CS, SS, DS, ES, FS, GS в таком случае даже называются иначе — селекторами сегментов. При этом каждый селекторный регистр разбивается па следующие три потя (рис. 3.4):
поле индекса (index) — старшие 13 битов (3-15). Определяет собственно номер сегмента (его индекс в соответствующей таблице дескрипторов):
поле индикатора таблицы сегментов (table index, TI) — бит с номером 2. Определи г часть виртуального адресного пространства (общее пли принадле-
1 Естественно, совместимость обес печена только «снизу вверх», то есть программы, >азра- ботанные для предыдущих версий микропроцессора, должны выно шиться па последующих бей какое шбо персдечки.
жащее только данной задаче). Если TRO, то Index указывает на элемент в глобальной таблице дескрипторов GDT, то есть идет обращение к общей памяти. Если Т1=1, то идет обращение к локальной области памяти текущей задачи; это пространство описывается локальной таблицей дескрипторов LDT
поле уровня привилегий — биты 0 и 1. Указывает запрашиваемый уровень привилегий (RPL, requested privilege level).
Операционная система в процессе своего запуска инициализирует многие регистры и, прежде всего, GDTR. Этот регистр содержит начальный адрес глобальной таблицы дескрипторов (GDT) и ее размер. Как мы уже знаем, в GDT находятся дескрипторы глобальных сегментов и системные дескрипторы.
Index (номер дескрипторе)TIRPLIndex (номер дескрипторе)TIRPLРис. 3.4. Селектор сегментаРис. 3.4. Селектор сегмента
Index (номер дескрипторе)
TI
RPL
Index (номер дескрипторе)
TI
RPL
Рис. 3.4. Селектор сегмента
Рис. 3.4. Селектор сегмента
Для манипулирования задачами ОС имеет информационную структуру, которую мы уже определили как дескриптор задачи (см. раздел «Понятия вычислительного процесса и ресурса», глава 1). Микропроцессор поддерживает работу с наиболее важной частью дескриптора задачи, которая меньше всего зависит от операционной системы. Эта инвариантная часть дескриптора, с которой и работает микропроцессор, названа сегментом состояния задачи (task state segment, TSS). Перечень полей TSS изображен на рис. 3.5. Видно, что в основном этот сегмент содержит контекст задачи. Процессор получает доступ к этой структуре с помощью регистра задачи (task register, TR).
Регистр TR содержит индекс (селектор) элемента в GDT. Этот элемент представляет собой дескриптор сегмента TSS. Дескриптор заносится в теневую часть регистра (см. рис. 3.2). К рассмотрению TSS мы еще вернемся, а сейчас заметим что в одном из полей TSS содержится указатель (селектор) на локальную таблицу дескрипторов данной задачи. При переходе процессора с одной задачи на дру гую содержимое поля LDTR заносится микропроцессором в одноименный регистр. Инициализировать регистр TR можно и явным образом.
Итак, регистр LDTR содержит селектор, указывающий на один из дескрипторов глобальной таблицы GDT. Этот дескриптор заносится микропроцессором в теневую часть регистра LDTR и описывает таблицу LDT для текущей задачи. Теперь, когда у нас определены как глобальная, так и локальная таблица дескрипторов, можно рассмотреть процесс определения линейного* адреса. Для примера рассмотрим процесс получения адреса команды. Адреса операндов определяются по аналогии, но задействованы будут другие регистры.
Микропроцессор анализирует бит TI селектора кода и в зависимости от его значения, извлекает из таблицы GDT или LDT дескриптор сегмента кода с номером (индексом), который равен полю index (биты 3—15 селектора, см. рис. 3.4). Этот
1 В микропроцессорах 180x86 линейным называется адрес, полученный в результате преобразования виртуального адреса формата (S, d) в 32-битовый адрес.
дескриптор заносится в теневую (скрытую) часть pei истра CS. Далее микропроцессор сравнивает значение регистра EIP* с полем размера сегмента, содержащегося в извлеченном дескрипторе, и если смещение относительно начала сегмента не превышает размера предела, то значение EIP прибавляется к значению поля начала сегмента и мы получаем искомый линейный адрес команды. Дипейпый адрес — это одна из форм виртуального адреса. Исходный двоичный виртуальный адрес, вычисляемый в соответствии с используемой адресацией, преобразуется в линейный. В свою очередь, линейный адрес будет либо равен физическому (если страничное преобразование отключено), либо с помощью страничной трансляции преобразуется в физический адрес. Если же смещение из регистра EIP превышает размер сегмента кода, то эта аварийная ситуация вызывает прерывание и управление должно передаваться супервизору ОС.
Адрес карты ввода/выводвLDTRСегментные регистры ES, CS, SS. DS, FS, GS (на каждый регистр отведено по 4 байта, из которых используются только 2 младших)Общие регистры (EAX ЕСХ, EDX, ЕВХ, ESP, EBP, ESI, EDI)Регистр флагов EFLAGSУказатель команд (регистр EIP)Привилегированные указатели стековLinkАдрес карты ввода/выводвLDTRСегментные регистры ES, CS, SS. DS, FS, GS (на каждый регистр отведено по 4 байта, из которых используются только 2 младших)Общие регистры (EAX ЕСХ, EDX, ЕВХ, ESP, EBP, ESI, EDI)Регистр флагов EFLAGSУказатель команд (регистр EIP)Привилегированные указатели стековLinkПоля, определяемые ОС (их количество и состав может быть любым)Поля, определяемые ОС (их количество и состав может быть любым)3118 150.3118 150.68b60h68b60h48h48h28h24h20h28h24h20h4040Собственно TSS (108 байтов)Собственно TSS (108 байтов)
Адрес карты ввода/выводв
LDTR
Сегментные регистры ES, CS, SS. DS, FS, GS (на каждый регистр отведено по 4 байта, из которых используются только 2 младших)
Общие регистры (EAX ЕСХ, EDX, ЕВХ, ESP, EBP, ESI, EDI)
Регистр флагов EFLAGS
Указатель команд (регистр EIP)
Привилегированные указатели стеков
Link
Адрес карты ввода/выводв
LDTR
Сегментные регистры ES, CS, SS. DS, FS, GS (на каждый регистр отведено по 4 байта, из которых используются только 2 младших)
Общие регистры (EAX ЕСХ, EDX, ЕВХ, ESP, EBP, ESI, EDI)
Регистр флагов EFLAGS
Указатель команд (регистр EIP)
Привилегированные указатели стеков
Link
Поля, определяемые ОС (их количество и состав может быть любым)
Поля, определяемые ОС (их количество и состав может быть любым)
3118 150.
3118 150.
68b
60h
68b
60h
48h
48h
28h
24h
20h
28h
24h
20h
4
0
4
0
Собственно TSS (108 байтов)
Собственно TSS (108 байтов)
Рис. 3.5. Сегмент состояния задачи (TSS)
Рассмотренный нами процесс получения линейного адреса проиллюстрирован на рис. 3.6. Стоит отметить, что поскольку межсегментные переходы происходят нечасто, то, как правило, определение линейного адреса заключается только в сравнении значения EIP с полем предела сегмента и в прибавлении смещения к началу сегмента Все необходимые данные уже находя 1Ся в микропроцессоре, и операция получения линейного адреса происходил очень быстро
Итак, линейный адрес может считаться физическим адресом, если не включен режим страничной трансляции адресов. Аппаратные средства микропроцессора для поддержки рассмотренного способа двойной трансляции виртуальных адре
EIP (extended instruction pointer) — указатель инструкции (команды).
cod в физические явно недостаточны, и при наличии большого количества небольших сегментов приводят к медленной работе. В самом деле, теневой регистр при каждом селекторе имеется в единственном экземпляре, и при переходе па другой сегмент потребуется вновь находить и извлекать соответствующий дескриптор сегмента, а это требует времени. Страничный же способ трансляции виртуальных адресов, как мы знаем, имеет немало своих достоинств. Поэтому в паши щепном режиме работы, при котором всегда действует описанный выше механизм определения линейных адресов, может быть вкдючен еще и страничный механизм.
Рис. 3.6. Процесс получения линейного адреса командыРис. 3.6. Процесс получения линейного адреса командыLDTLDTGDTGDT
Рис. 3.6. Процесс получения линейного адреса команды
Рис. 3.6. Процесс получения линейного адреса команды
LDT
LDT
GDT
GDT
Поддержка страничного способа организации виртуальной памяти
При создании микропроцессора i80386 разработчики столкнулись с очень серьезной проблемой в реализации страничного механизма. Дело в том, что микропроцессор имеет широкую шипу адреса — 32 бита — и возникает вопрос о раз- бпеипи всего адреса па иоле страницы и иоле индекса Если большое количество битов адреса отвести под индекс, то страницы станут очень большими что повлечет большие потери и на фрагментацию, и па операции ввода/вывода, связанные с замещением страниц. Хотя количество страниц стало бы при этом меньше, и накладные расходы иа их поддержание тоже уменьшились бы. Если же размер
страницы уменьшить, то большое поле номера страницы привело бы к появлению громадного количества возможных страниц н необходимо было либо вводить какие-то механизмы контроля за номером страницы (с тем, чтобы он не выходил за размеры таблицы страниц), либо создавать эти таблицы максимально возможного размера. Разработчики пошли ио пути, при котором размер страницы все же небольшой (он выбран равным 212 = 4096 = 4К) а иоле номера страницы величиной в 20 битов, в свою очередь, разбивается на два поля и осуществляется двухэтапная (двухшаговая) страничная трансляция.
Для описания каждой страницы создастся соответствующий дескриптор. Длина дескриптора выбрана рапной 32 битам: 20 битов линейного адреса определяют номер страницы (по существу — ее адрес, поскольку добавление к нему (приписывание в качестве младших разрядов) 12 пулей приводит к определению начальною адреса страницы), а остальные биты разбиты па следующие поля, которые изображены на рис. 3.7. Как видно, три бита дескриптора зарезервированы для использования системными программистами при разработке подсистемы организации виртуальной памяти. С этими битами микропроцессор сам не работает.
3112 11109 87 65 43 210
Адрес таблицы страниц или адрес страничного кадра
Для ОС
00
5 Q
Ь- ш
00
о
<2
Z)a. со
"О <D
£5
с
</> Ф
IX
Рис. 3.7. Дескриптор страницы
Прежде всего, микропроцессор анализирует самый младший бит дескриптора — бит присутствия, ибо если поле present равно пулю, то это означает отсутствие данной страницы в оперативной памяти, и такая ситуация влечет прерывание в работе процессора с передачей управления соответствующей программе, которая должна будет загрузить затребованную страницу. Бит dirty — «грязный» — предназначен для отметки, что данную страницу модифицировали и при замещении этого с граничного кадра следующим се необходимо сохранить во внешней памяти. Бит обращения (access) свидетельствует о том, что к данной таблице или странице осуществлялся доступ. Он используется для определения < т]кшицы, которая будет участвовать в замещении при использовании дисциплин LRU пли LFU. Наконец, первый и второй биты используются для защиты памяти.
Ciapnnie 10 битов линейного адреса определяют номер таблицы страниц (page table entry, РТЕ), из которой посредством вторых 10 бигов линейного адреса выбирается соответствующий дескриптор виртуальной страницы. И уже из этого дескриптора выбирается номер физической страницы, если данная виртуальная страница от ображена сейчас на оперативную память. Эта схема определения физического адреса по линейному изображена па рис. 3.8.
Первая таблица, которую мы индексируем первыми (старшими) 10 битами линейного адреса, названа таблицей каталогов таблиц страниц (page directory entry, РОЕ). Ее адрес в оперативной памяти определяется старшими 20 битами управляющего ри негра CR3.
Линейный адрес
Регистр СгЗ
Рис. 3.8. Трансляция линейного адреса в микропроцессорах 180x86
Каждая из таблиц PDE и РТЕ состоит из 1024 элементов (2*°“1024). В свою очередь, каждый элемент (дескриптор страницы) имеет длину 4 байта (32 бита), поэтому размер этих таблиц как раз соответствует размеру страницы.
Оценим теперь эту двухшаговую схему трансляции с позиций расхода памяти. Каждый дескриптор описывает страницу размером 4 Кбайт. Следовательно, одна таблица страниц, содержащая 1024 дескриптора, описывает пространство памяти в 4 Мбайт. Если наша задача пользуется виртуальным адресным пространством, например, в 50 Мбайт (предположим, что речь идет о некотором графическом редакторе, который обрабатывает изображение, состоящее из большого количества пикселов*), то для описания этой памяти необходимо иметь 14 страниц, содержащих таблицы РТЕ. Кроме этого, нам потребуется для этой задачи еще одна таблица PDE (тоже размером в одну страницу), в которой 14 дескрипторов будут указывать на местонахождение упомянутых таблиц РТЕ. Остальные дескрипторы PDE могут быть не задействованы. Итого, для описания 50 Мбайт адресного пространства задачи потребуется всего 15 страниц, то есть 60 Кбайт памяти, что можно считать приемлемым.
Если бы не был использован такой двухшаговый механизм трансляции, то потери памяти на описание адресного пространства могли бы составить 4(Кбайт) • 210 - - 4 (Мбайт)! Очевидно, что это уже неприемлемое решение.
Итак, микропроцессор для каждой задачи, для которой у него есть TSS, позволяет иметь таблицу PDE и некоторое количество РТЕ. Поскольку это дает возмож-
1 Напомним, что термин пиксел происходит от английского picture element (графический элемент). Множество пикселов образуют изображение.
ность адресоваться к любому байту из 232, а шина адреса как раз и позволяет использовать физическую память с таким объемом, то можно как бы отказаться от сегментного способа адресации. Другими словами, если считать, что задача состоит из одного единственного сегмента, который, в свою очередь, разбит на страницы, то фактически мы получаем только один страничный механизм работы с виртуальной памятью. Этот подход получил название «плоской памяти». При использовании плоской модели памяти упрощается создание и операцион иых систем, и систем программирования. Кроме этого, уменьшаются расходы памяти для поддержки системных информационных структур. Поэтому в абсолютном большинстве современных 32-разрядных ОС, создаваемых для микропроцессоров 180x86, используется плоская модель памяти.
Режим виртуальных машин для исполнения приложений реального режима
Разработчики рассматриваемого семейства микропроцессоров в своем стремлении обеспечить максимально возможную совместимость архитектуры пошли не только на то, чтобы обеспечить возможность программам, созданным для первых 16-разрядиых ПК, без проблем выполняться на компьютерах с более поздними моделями микропроцессоров за счет введения реального режима работы. Опи гасже обеспечили возможность выполнения 16-разрядиых приложений реального режима при условии, что сам процессор при этом функционирует в защищенном режиме работы, и операционная система, используя соответствующие аппаратные средства микропроцессора, организует мультипрограммный (мультизадачный) режим. Другими словами, микропроцессоры 180x86 поддерживают возможность создания операционных сред реального режима при работе микропроцессора в защищенном режиме. Если условно назвать 16-разрядные приложения DOS- ириложепиями (поскольку в абсолютном большинстве случаев это именно так), то можно сказать, что введена поддержка для организации виртуальных DOS- машин, работающих вместе с обычными 32-битовыми приложениями защищенного режима. Это даже нашло отражение в названии режима работы микропроцессоров 180x86 — режим виртуального процессора 18086, иногда (для краткости) его называют режимом V86 или просто виртуальным режимом, — при котором в защищенном режиме работы может исполняться код DOS-ириложепия. Муль- тизадачность при выполнении нескольких программ реального режима будет поддержана аппаратными средствами защищенного режима.
Переход в виртуальный режим осуществляется посредством изменения бита VM (virtual mode) в регистре EFLAGS. Когда процессор находится в виртуальном режиме, для адресации памяти используется схема реального режима работы (сегмен' смещение) с размером сегментов до 64 Кбайт, которые могут располагаться в адресном пространстве размером в 1 Мбайт, однако полученные адреса считаются не физическими, а линейными. В результате применения страничной трансляции осуществляется отображение виртуального адресного пространства 16-битового приложения па физическое адресное пространство. Это позволяет организовать параллельное выполнение нескольких задач разработанных для реального режима, да еще и совместно с обычными 32-би юными приложениями, требующих защищенного режима работы.
Естественно, что для обработки прерываний, возникающих при выполнении 16- битовых приложении в виртуальном режиме, процессор возвращается из втого режима в обычный защищенный режим. В противном случае невозможно было бы организовать полноценную виртуальную машину. Очевидно, что обработчики прерываний для виртуальной машины должны эмулировать работу подсистемы прерываний процессора 18086. Другими словами, прерывания отображаются в операционную систему, работающую в защищенном режиме, и уже основная ОС моделирует работу операционной среды выполняемого приложения.
Вопрос, связанный с операциями ввода/вывода, которые недоступны для обычных приложений (см. следующую главу), решается аналогично. При попытке выполнить недопустимые команды вйода/вывода возникают прерывания, и необходимые операции выполняются операционной системой, хотя задача об этом и «нс подозревает». При выполнении команд IN, OUT, INS, OUTS, CLI, STI процессор, находящийся в виртуальном режиме и исполняющий код па уровне привилегий третьего (самого нижнего) кольца защиты, за счет возникающих вследствие этого прерываний переводится па выполнение высоко привилегированного кода операционной системы.
Таким образом, ОС может полностью виртуалпзировать ресурсы компьютера: и аппаратные, и программные, создавая иную полноценную операционную среду; при существовании так называемых нативных приложений, создаваемых по собственным спецификациям данной ОС. Очень важным моментом для организации полноценной виртуальной машины является реализация виртуализации не только программных, по и аппаратных ресурсов. Так, например, в ОС Windows NT эта задача выполнена явно неудачно, тогда как в OS/2 имеется полноценная виртуальная машина как для DOS-приложсинп, так и для приложений, работающих в среде спецификаций Winl6. Правда, в последнее время это уже перестало быть актуальным, поскольку появилось большое количество приложений, работающих по спецификациям Win32 API.
Защита адресного пространства задач
Для возможности создания надежных мультипрограммных ОС в процессорах семейства i80x86 имеется несколько механизмов защиты. Это и разделение адресных пространств задач, и введение уровней привилегий для сегментов кода и сегментов данных. Все это позволяет обеспечить как защиту задач друг от друга, гак и защиту самой операционной системы от прикладных задач, защиту одной части ОС от других ее компонентов, защиту самих задач от некоторых своих собственных ошибок.
Защита адресного пространства задач осуществляется относительно легко за счет того, что каждая задача может иметь свое собственное локальное адресное пространство. Операционная система должна корректно манипулировать таблицами трансляции сегментов (дескрипторными таблицами) и таблицами гранс-
ляции страничных кадров. Сами таблицы дескрипторов как сегменты данных (а соответственно, в свою очередь, и как страничные кадры) относятся к адресному пространству операционной системы и имеют соответствующие привилегии доступа: исправлять их задачи не могук Этими информационными структурами процессор пользуется сам, па аппаратном уровне, без возможности их читать и редактировать из пользовательских приложении. Если используется модель плоской памяти, то возможность микропроцессора контролировать обращения к памяти только внутри текущего сегмента фактически не используется, и остается в основном только механизм отображения страничных кадров Выход за пределы страничного кадра невозможен, и этому фиксируется только выход за пределы своего сегмента В этом случае приходится полагаться только иа систему программирои пия, которая должна корректно распределять программные модули в пределах единого неструктурированного адресного пространства задачи. Поэтому при с отдании многопоточных приложении, ко да каждая тадача (в данном случае — поток) может испортит ь адресное пространство другой задачи, эта проблема становится очень сложной особенно если не использовать системы программирования на языках высокого уровня.
Однако для организации взаимодействия задач, имеющих разные виртуальные адресные прост[>апства, необходимо, как мы уже говорили, иметь общее адресное пространство И здесь, для обеспечения защиты самой ОС, а зпа шт, н повышения надежности всех вычислений, используется механи м защиты сегментов с помощью уровней привилегий.
Уровни привилегий для защиты адресного пространства задач
Для того чтобы запретить пользовательским задачам модифицировать области памяти, принадлежащие самой ОС, необходимо иметь специальные средства Одного разграничения адресных пространств через механизм се ме гов мало, ибо можно указывать различные значения адреса начала пимента и тем самым получать доступ к чужим сегментам. Другими словами, необходимо в явном виде разграничивать системные сегменты данных и кода от сегментов, принадлежащих пользовательским Программам Поэтому были введены два основных режи ма работы процессора пользователя и супервизора. Большинство современных процессоров имеют по крайней мере два л их режима. 1 ак, в /тежш/е сунериизора программа может выполнять все действия и иметь доступ ио любым адресам, тогда как в пользовательском режимё должны быть ограничения, с тем чтобы обнаруживать и пресекать запрещенные действия, перехватывая их и передавая управление супервизору ОС Часто в пользовательском режиме запрещается выполнение команд ввода/вывода и некоторых других, чтобы гарантировать, что только ОС выполняет эти от рации Можно сказать, что эти два режима имеют разные уровни привилегий
В микропроцессорах i80x86 имеются нс два, а четыре уровня привилегий. Часто уровни лривилс! ий называют кольцами защиты, поскольку это иногда помогает объяснить принцип действия самого механизма; поэтому говорят, что пекоторь й программный модуль «исполняется в кольце защиты с таким-то номером». Для
указания уровня привилегий используются два бита, поэтому код 00 обозначает самый высший уровень, а код 11(2) (“3) — самый низший. Самый высокий уровень привилегий предназначен для операционной системы (прежде всего, для ядра ОС), самый низкий — для прикладных задач пользователя. Промежуточные уровни привилегий введены для большей свободы системных программистов в организации надежных вычислений при создании ОС и иного системного ПО. Предполагалось, что уровень с номером (кодом) 1 может быть использован, например, для системного сервиса — программ обслуживания аппаратуры, драйверов, работающих с портами ввода/вывода. Уровень привилегий с кодом 2 может быть использован для создания пользовательских интерфейсов, систем управления базами данных и т. п., то есть для реализации специальных системных функций, которые по отношению к супервизору ОС недуг себя как обычные приложения. Так, например, система OS/2 использует три уровня привилегий: с нулевым уровнем привилегий исполняется код самой ОС, на втором уровне исполняются системные процедуры подсистемы ввода/вывода, на третьем уровне исполняются прикладные задачи пользователей. Однако чаще всего на практике используются только два уровня — нулевой и третий. Таким образом, упомянутый режим супервизора для микропроцессоров i80x86 соответствует выполнению кода с уровнем привилегий 0 (его обозначают так: PL0). Подводя итог, можно констатировать, что именно уровень привилегий задач определяет, какие команды в них можно использовать и какое подмножество сегментов и/или страниц в их адресном пространстве они могут обрабатывать.
Основными системными объектами, которыми манипулирует процессор при работе в защищенном режиме, являются дескрипторы. Дескрипторы сегментов содержат информацию об уровне привилегии соответствующего сегмента кода или данных. Уровень привилегии исполняющейся задачи определяется значением поля привилегии, находящегося в дескрипторе ее текущего кодового сегмента. Напомним, что в каждом дескрипторе сегмента (см. рис.3.3) имеется поле DPL в байте прав доступа, которое и определяет уровень привиле! ни связанного с ним сегмента. Таким образом, поле DPL текущего сегмента кода становится полем CPL. При обращении к какому-нибудь сегменту в соответствующем селекторе указывается запрашиваемый уровень привилегий RPL (см. рис. 3 4).
В пределах одной задачи используются сегменты с различным уровнем привилегии п в определенные моменты времени выполняются или обрабатываются сегменты с соответствующими им уровнями привилегии. Механизм проверки привилегий работает в ситуациях, которые можно назвать межсегментными переходами (обращениями). Это доступ к сегменту данных или стековому сегменту, межсегментные передачи управления в случае прерываний (и особых ситуаций), при использовании команд CALL, JMP, INI, IRET, RET. В таких межсегментных обращениях участвуют два сегмента: целевой сегмент (к которому мы обращаемся) и текущий сегмент кода, из которого идет обращение.
Процессор сравнивает упомянутые значения CPL, RPL, DPL и па основе понятия эффективного уровня привилегий' (EPL - max (RPL, DPL)) ограничивает возможности доступа к сегментам по следующим правилам, в зависимости от того, идет ли речь об обращении к коду или к данным.
При доступе к сегментам данным проверяется условие CPL<EPL. Нарушение этого условия вызывает так называемую особую ситуацию ошибки защиты и возникает прерывание. Уровень привилегии сегмента данных, к которому осуществляется обращение должен быть таким же, как и текущий уровень, или меньше его. Обращение к сегменту с более высоким уровнем привилегии воспринимается как ошибка, гак как существует опасность изменения данных с высоким уровнем привилегий в программе с низким уровнем привилегии. Доступ к данным с меньшим уровнем привилегии разрешается.
Если целевой сегмент является сегментом стека, то правило проверки имеет вид
CPL=DPL=RPL.
В случае его нарушения также возникает исключение. Поскольку стек может использоваться в каждом сегменте кода и всего имеются четыре уровня привилегий кода, то используются и четыре стека. Сегмент стека, адресуемый регистром SS, должен иметь тот же уровень привилегий, что и текущий сегмент кода.
Правила для передачи управления, то есть когда осуществляется межсегментный переход с одного сегмента кода на другой сегмент кода, несколько сложнее. Если для перехода с одного сегмента данных на другой сегмент данных считается допустимым обрабатывать менее привилегированные сегменты, то передача управления из высоко привилегированного кода на менее привилегированный код должна контролироваться дополнительно. Другими словами, код операционной системы не должен доверять коду прикладных задач. И обратно, нельзя просто так давать задачам возможность исполнять высоко привилегированный код, хотя потребность в этом всегда имеется (ведь многие функции, в том числе и функции ввода/вывода, считаются привилегированными и должны выпол- няться только самой ОС). Для реализации возможностей передачи управления в сегменты кода с иными уровнями привилегий введен механизм шлюзования, который мы вкратце рассмотрим нижа Итак, если DPL - CPL. то переход в другой сегмент кода возможен. Более подробное рассмотрение затронутых вопросов по замыслу авторов выходит за рамки настоящего учебника (для получения более детальных сведений по этому и некоторым другим вопросам особенностей архитектуры микропроцессоров i80x86 рекомендуется обратиться к материалам [2, 59]). Здесь мы рассмотрим только основные идеи.
Механизм шлюзов для передачи управления на сегменты кода с другими уровнями привилегий
Поскольку межсегментные переходы контролируются с использованием уровней привилегий и существует потребность в передаче управления с одного уровня
EPL (effective privilege level) — эффекпитый уровень привилегий. Значение эффективного уровня привилегий определяется минимальной привилегией, то есть как максимальное значение из двух: RPL и DPL.
привилс!ini на другой уровень, в микропроцессорах 180x86 реализован механизм шлюзов, который мы поясним с помощью рис. 3.9. Шлюзование повволяет организовать обращение к так называемым подчиненным сегментам кода, которые выполняют часто встречающиеся функции и должны быть доступны многим задачам, располагающимся на том же пли более низком уровне привилегии.
Адресное пространство программных модулей ОС
Уровень привилегий 0Уровень привилегий 0Запрашиваемыйпрограммный модульЗапрашиваемыйпрограммный модуль
Уровень привилегий 0
Уровень привилегий 0
Запрашиваемыйпрограммный модуль
Запрашиваемыйпрограммный модуль
Уровень привилегий 1
Уровень привилегий 2
Уровень привилегий 3Уровень привилегий 3Адресное пространство процесса ААдресное пространство процесса ААдресное пространствопроцесса ВАдресное пространствопроцесса В
Уровень привилегий 3
Уровень привилегий 3
Адресное пространство процесса А
Адресное пространство процесса А
Адресное пространствопроцесса В
Адресное пространствопроцесса В
Рис. 3.9. Механизм шлюзов для перехода на другой уровень привилегий
Помимо дескрипторов сегментов системными объектами, с которыми работает микропроцессор, являются специальные системные дескрипторы, названные шлюзами или вентилями. Главное различие между дескриптором сегмента и шлюзом вызова заключается в том, что содержимое дескрип юра указывает на сегмент в памяти, а шлюз обращается к дескриптору. Другими словами, если дескриптор служит механизмом отображения памяти, то шлюз служит механизмом перенаправления.
Для получения доступа к более привилегированному коду задача должна обратиться к нему ле непосредственно (путем указания дескриптора этого кода), а обращением к шлюзу этого сегмента (рис. 3 10).
В этом дсскрипто]Ю вместо адреса сегмента указываются селектор, позволяющий найти дескриптор искомого сегмента кода, и адрес (смещение назначения), с которого будет выполняться подчиненный сегмент, то есть полный 32-битпый ад рес. Формат дескриптора шлюза приведен на рис. 3 11. Адресовать шлюз вызова можно с помощью команды CALL*. По существу, дескрипторы шлюзов вызова не являются дескрипторами (сегментов), но они могут быть расположены средн обычных дескрипторов в дескрипторных таблицах процесса. Смещение, указы васмое в команде перехода па другой сегмент (FAR CALL), игнорируется, и факти-
1 FAR CALL межсегментный вызов процедуры.
чески осуществляется переход па команду, адрес которой определяется через смещение из шлюза вызова. Этим гарантируется попадание только на разрешенные точки входа в подчиненные сегменты.
CALLCALLРис. 3.10. Переход на сегмент более привилегированного кодаРис. 3.10. Переход на сегмент более привилегированного кода
CALL
CALL
Рис. 3.10. Переход на сегмент более привилегированного кода
Рис. 3.10. Переход на сегмент более привилегированного кода
Старшее двойное слово дескриптора
3118 15 14 13 12118 7О
Смещение назначения (биты 31-16)
Р DPL 0 1100
ООО
Счетчик DWORD
Байт прав доступа
Селектор сегмента назначения
Смещение назначения (биты 15-0)
3118 15 хО
Первое (младшее) двойное слово дескриптора
Рис. 3.11. Формат дескриптора шлюза
Введены следующие правила использования шлюзов:
значение DPL шлюВа вызова должно быть больше или равно значению текущего уровня привилегий CPL
значение DPL шлюза вызова должно быть больше или равно значению поля RPL селектора шлюза;
значение DPL шлюза вызова должно быть больше пли равно значению DPL целевого сегмента кода;
значение DPL целевого сегмента кода должно быть меньше или равно значению текущего уровня привилегий CPL.
Требование наличия и доступности ш. поза вызова для перехода па более привилегированный код ограничивает менее привилегированный код заданным пабо-
ром точек входа в код с большей привилегией. Так как шлюзы вызова являются элементами в дескрипторных таблицах (а мы говорили, что их не только можно, но и желательно там располагать), то менее привилегированная программа не может создать дополнительных (а значит, и неконтролируемых) шлюзов. Таким образом, рассмотренный механизм шлюзов дает следующие преимущества в организации среды для выполнения надежных вычислений:
привилегированный код надежно защищен и вызывающие его программы не могут сто разрушить. Естественно, что такой системный код должен быть особенно тщательно отлаженным, нс содержать ошибок, быть максимально эффективным;
шлюзы межсегментных переходов для вызова системных функций делают эти самые системные функции невидимыми для программных модулей, расположенных на внешних (более низких) уровнях привилегий;
поскольку вызывающая программа непосредственно адресует только шлюз вызова, реализуемые вызываемым модулем (сегментным кодом) функции можно изменить или переместить в адресном пространстве, не затрагивая интерфейс со шлюзом;
программные модули вызываются с более привилегированного уровня
Изложенный коротко аппаратный механизм защиты по привилегиям оказывается довольно сложным и жестким. Однако поскольку все практические ситуации учесть в схемах микропроцессора невозможно, то при разработке процедур операционных систем и иного высоко привилегированного кода следует придерживаться приведенных ниже рекомендаций, заимствованных из книги [22].
Основной риск связан с передачей управления через шлюз вызова более привилегированной процедуре. Нельзя предоставлять вызывающей программе никаких преимуществ, вытекающих из-за временного повышения привилегий. Это замечание особенно важно для процедур нулевого уровня привилегий (PLO-npo- цедур).
Вызывающая программа может нарушить работу процедуры, передавая ей «плохие» параметры. Поэтому целесообразно как можно раньше прокошролировать передаваемые процедуре параметры. Шлюз вызова сам по себе не проверяет значений параметров, которые копируются в новый стек, поэтому достоверность каждого передаваемого параметра должна контролировать вызванная процедура. Вот некоторые способы контроля передаваемых параметров.
Следует проверять счетчики циклов и повторений на минимальные и максимальные значения.
Необходимо проверить 8- и 16-битные параметры передаваемые в 32-битных регистрах. Когда процедуре передается короткий параметр, его следует рас- ширш ь со знаком или нулем для заполнения всего 32-битного регистра.
Следует стремиться свести к минимуму время работы процессора с запрещенными прерываниями. Если процедуре требуется запрещать прерывания, необходимо, чтобы вызывающая программа не могла влиять на время нахождения процессора с запрещенными прерываниями (флажок IF=0).
Процедура никогда не должна воспринимать как параметр код или указатель кода.
В операциях процессора следует явно задавать состояние флажка направления DF для цепочечных команд.
Заключительная команда RET или RETn в процедуре должна точно соответствовать полю счетчика WC шлюза вызова; при этом n - 4x(WC), так как счетчик задает число двойных слов, а п соответствует байтам.
Не следует применять шлюзы вызовов для функций, которым передается переменное число параметров (см. рекомендацию 6). При необходимости нужно воспользоваться счетчиком и указателем параметров.
Функции не могут возвращать значения в стеке (см. рекомендацию 6), так как после возврата стеки процедуры и вызывающей программы находятся точно в таком состоянии, в каком они были до вызова.
В процедуре следует сохранять и восстанавливать все сегментные регистры Иначе, если какой-либо сегментный регистр привлекался для адресации данных, недоступных вызывающей программе, процессор автоматически загрузит в него пустой селектор.
Рекомендуется контролировать все обращения к памяти. Нетрудно представить себе, что РТЗ-программа передаст PLO-процедуре указатель селекторхмещение и запросит считывание или запись нескольких байтов по этому адресу. Типичным примером может служить процедура дискового ввода/вывода, которая воспринимает как параметр системный номер файла, счетчик байт и адрес, по которому записываются данные с диска. Хотя PLO-процсдура имеет привилегии для производства такой операции, но у РЬЗ-программы разрешения на эго может не быть.
Система прерываний 32-разрядных микропроцессоровi80x86
В микропроцессорах семейства i80x86 система прерываний построена таким образом, чтобы, с одной стороны, обеспечить возможность создавать эффективные и надежные мультипрограммные операционные системы, которые должны функционировать в защищенном режиме, а с другой стороны — обеспечить возможность выполнять программы, разработанные для реального режима. Рассмотрим коротко оба режима.
Работа системы прерываний в реальном режиме работы процессора
В реальном режиме работы система прерываний использует понятие вектора прерывания. Термин «вектор прерываний» используется потому, что для указа!
1 WC (Word Counter) — счетчик слов, см. рис. 3.11.
имя адреса используется не одно значение, а два, то есть мы имеем дело не со скалярной величиной, а с «векторной».
Итак, каждый вектор прерываний состоит из 4 байтов или 2 слов: первые два содержат новое значение для регистра IP, а следующие два — новое значение регистра CS. Таблица векторов прерываний занимает 1024 байга. Таким образом, в ней может быть задано 256 векторов прерываний. В процессоре 18O86 зга таблица располагается на адресах 0O0OOH -O03FFH Расположение этой таблицы в процессорах i80286 и старше определяется значением регистра JDTR — Interrupt Descriptor Table Register. При включении или сбросе процессора i80x86 этот ре- гистр обнуляется. Однако при необходимости можно в регистре IDTR указать смещение и. таким образом, иерейги на новую таблицу векторов прерываний
Таблица векторов прерываний заполняется (инициализируется) при запуске системы, но в принципе может быть изменена или перемещена.
Каждый вектор прерывания имеет свой номер, называемый номером прерывания, который указывает его место в таблице. Этот номер, помноженный па четыре (сдвиг на два разряда влево и заполнение освободившихся битов нулями), и стоженный с содержимым регистра IDTR, дает абсолютный адрес первого байга вектора в оперативной памяти.
Подобно вызову процедуры, прерывание заставляет микропроцессор сохранить в стеке информацию для последующего возврата, а затем перейти к группе команд, адрес, которых определяется вектором прерывания. Таким образом, изрывание вызывает косвенный переход к своей подпрограмме обработки за счет получения ее адреса из вектора прерывания.
В IBM PC, как и в других вычислительных системах, прерывания бывают двух видов: внутренние и внешние.
Внутренние прерывания, как мы уже знаем, возникают в результате работы процессора. Они возникают в ситуациях, которые нуждаются в специальном обслуживании, или при выполнении специальных инструкций — INT или INTO. Это следующие прерывания:
прерывание при делении па ноль помер прерывания 0:
прерывание по флагу TF (trap flag*). В этом случае прерывание обычно используется специальными программами отладки тина DEBUG. Помер прерывания — 1;
О инструкции INT (interrupt — выполнить прерывание с соответствующим номером) и INTO (interrupt if overflow прерывание по переполнению). Эти прерывания называются программными.
В качестве операнда команды INT указывается помер прерывания, которое нужно выполнигь, например INT ЮН. Программные прерывания как средство перехода
1 Trap Flag — специальный бит в регистре PSW (program status word, слово состояния программы), который в случае значения TF^l вызывает «останов» после каждой команды и генерируется прерывание для организации режима отладки с пошаговым выполнением программы. Чаше всего этот регистр в микропроцессорах Intel 80x86 называют регистром ф инов.
на соответствующую процедуру были введены для того, чтобы выполнение этой процедуры осуществлялось в привилегированном режиме, а не в обычном пользовательском.
Внешние прерывания возникают по сигналу какого-нибудь внешнего устройства. Существуют два специальных внешних сигнала средн входных сигналов процессора, при помощи которых можно прервать выполиеппе текущей щюграммы и тем самым переключить работу центрального процессора. Эго сигналы NMI (по mask interrupt, немаскируемое прерывание) и INTR (interrupt request, запрос- па прерывание). Соответственно, внешние прерывания подразделяются па немаскируемые и маскируемые.
Маскируемые прерывания генерируются контроллером прерывании по заявке определенных периферийных устройств*. Контроллер прерываний (его обозначение — I8259A) поддерживает восемь уровней (липин) приоритета; к каждому уровню «привязано» одно периферийное устройство. Маскируемые прерывания часто называют еще аппаратными прерываниями. В ПК, начиная с IBM PC AT, построенных па базе микропроцессора i80286, используются два контроллера прерываний I8259A; они соединяются каскадным образом. Схема последовательного соединения этих контроллеров изображена па рис. 3.12.
Таким образом, иа IBM PC АТ предусмотрено 15 линий IRQ’, часть которых используется внутренними контроллерами системной платы, а остальные заняты стандартными адаптерами либо не используются. Ниже перечислены липни запроса на прерывание, которые мы приводим потому, что каждый специалист по вычислительной технике должен знать основные стандарты ПК. Итак, линии IRQ:
О — системны]! таймер;
— контроллер клавиатуры;
— сигнал возврата по кадру (EGA/VGA), на АТ соединен с IRQ 9;
— обычно СОМ2/СОМ4;
- обычно СОМ 1 /СО М3;
— контроллер HDD (па первых компьютерах IBM PC XT), обычно свободен на IBM PC АТ и используется звуковой каргой;
— кот роллер FDD;
— LPT1, многими LPT-контроллерами нс используется;
— часы реального времени с автономным питанием (RTC — real time clock);
— параллельна IRQ 2;
Сигнал запроса иа прерывание чаще всего является сигналом готовности внешнего уст- ронства (соответствующего контроллера внешнего устройства) на выполнение следующей команды, связанной с управлением операциями ввода/вывода.
— не используется, то есть свободно;
— свободно;
— обычно контроллер мыши типа PS/2;
— математический сопроцессор;
— обычно контроллер IDE0 (первый канал);
— обычно контроллер IDE1 (второй канал).
От часов и КМОП-памяти
От 8-разрядных секций слотов (бывшая линия ЗПр 9)
От 16-разрядных секций слотов
От сопроцессора
От контроллера IDE 0
От контроллера IDE 1
От системного таймера
От контроллера клавиатуры
Последовательный порт СОМ 2(4)
Последовательный порт СОМ 1 (3) Sound Blaster или свободно От контроллера FDD
Параллельный порт LPT 1
Рис 3 12. Каскадирование контроллеров прерывания
Как известно, прерывания могут быть инициированы внешним устройством ПК или специальной командой прерывания из программы. В любом случае, если прерывания разрешены, то выполняется следующая процедура:
В стек помещается регистр флагов PSW (его еще называют FLAGS).
Флаг включения/выключения прерываний IF и флаг трассировки TF, находящиеся в регистре PSW, обнуляются для блокировки других маскируемых прерываний и исключения пошагового режима исполнения команд.
Значения регистров CS и IP сохраняются в стеке вслед за PSW.
Вычисляется адрес вектора прерывания, и из вектора, соответствующего номеру прерывания, загружаются новые значения IP и CS.
Когда системная подпрограмма принимает управление, она может снова разрешить маскируемые прерывания командой STI (set interrupt flag, установить флаг
прерываний), которая переводит флаг IF в состояние 1, что разрешает микропроцессору вновь реагировать на прерывания, инициируемые внешними устройствами, поскольку стековая организация позволяет вложение прерываний друг в друга.
Закончив работу, подпрограмма обработки прерывания должна выполнить инструкцию IREI (interrupt return), которая извлекает из стека три 16-битовых значения и загружает их в указатель команд IP, регистр сегмента команд CS и регистр PSW соответственно. Таким образом, процессор сможет продолжить работу с того места, где он был прерван.
В случае внешних прерываний процедура перехода па подпрограмму обработки прерывания дополняется следующими шагами:
Контроллер прерываний получает заявку от определенного периферийного устройства и, соблюдая схему приоритетов, генерирует сигнал INTR (interrupt request), который является входным для микропроцессора.
Микропроцессор проверяет флаг IF в регистре PSW. Если он установлен в 1, то переходим к шагу 3. В противном случае работа процессора не прерывается. Часто говорят, что прерывания замаскированы, хотя правильнее говорить, что они отключены. Маскируются (запрещаются) отдельные линии запроса на прерывания посредством программирования контроллера прерываний.
Микропроцессор генерирует сигнал INTA (подтверждение прерывания). В ответ на этот сигнал контроллер прерывания посылает по шипе данных номер прерывания. После этого выполняется описанная нами ранее процедура передачи управления соответствующей программе обработки прерывания.
Номер прерывания и его приоритет устанавливаются на этапе инициализации системы. После запуска ОС пользователь, как мы уже отмечали, может изменить таблицу векторов прерывания, поскольку она ему доступна.
Работа системы прерываний в защищенном режиме работы процессора
В защищенном режиме работы система прерываний действует совершенно иначе. Прежде всего, система прерываний микропроцессора 180x86 при работе в защищенном режиме вместо таблицы векторов, о которой мы говорили выше, имеет дело с таблицей дескрипторов прерываний (IDT, interrupt descriptor table). Дело здесь не столько в названии таблицы, сколько в том, что таблица IDT представляет собой не таблицу с адресами обработчиков прерываний, а таблицу со специальными системными структурами данных (дескрипторами), доступ к которой со стороны пользовательских (прикладных) программ невозможен. Только сам микропроцессор (его система прерываний) и код операционной системы могут получить доступ к этой таблице, которая представляет собой специальный сегмент, адрес и длина которого содержатся в регистре IDTR (см. рис. 3.2). Этот регистр аналогичен регистру GDTR в том отношении, что он инициализируется один раз при загрузке системы. Интересно заметить, что в реальном режиме работы регистр IDTR так же указывает адрес таблицы прерываний, но при этом, как и в процессоре 18086, каждый элемент таблицы прерываний (вектор) зани-
мает всего 4 байта и содержит 32-бигиый адрес в формате селектор:смешение (CS:IP). Начальное значение этого регистра равно нулю, но в него можно занести и другое значение. В этом случае таблица векторов прерывании будет находиться в другом месте оперативной памяти. Естественно, что перед тем, как это сделать (занести в регистр IDTR новое значение), необходимо подготовить саму таблицу векторов. В защищенном режиме работы загрузку регистра IDTR может произвести только код с максимальным уровнем привилегий.
Каждый элемент в таблице дескрипторов прерываний, о которой мы говорим уже в защищенном режиме, представляет собон 8-байтовую структуру, более похожую на дескриптор шлюза (gate), нежели на дескриптор ссгмыгга.
Как мы уже знаем, в зависимости от причины прерывания процессор автоматически индексирует таблицу прерываний и выбирает соответствующий элемент, с помощью которого и осуществляется перенаправление в исполнении кода, то есть передача управления на обработчик прерывания. Однако таблица IDT содержит только шлюзы, а не дескрипторы сегментов кода, палому фактически получается косвенная адресация, по с использованием рассмотренного ранее механизма защиты с помощью уровнен привилегии. Благодаря этому пользователи уже не могут сами изменить обработку прерываний, которая предопределяется системным программным обеспечением.
Дескриптор прерываний может принадлежать к одному из трех типов:
коммутатор прерывания (interrupt gate);
коммутатор перехвата (trap gate);
коммутатор задачи (task gate).
При обнаружении зап|юса на прерывание и при условии, что прерывания сейчас разрешены, процессор действует в зависимости от типа дескриптора (коммутатора), соответствующего номеру прерывания. Первые два типа дескриптора прерывании вызывают переход на соответствующие сегменты кода, принадлежащие виртуальному адресному пространству текущего вычислительного процесса. Поэтому про них говорят, что обработка прерываний по этим дескрипторам осуществляется под контролем текущей задачи. Последний тин дескриптора — коммутатор задачи — вызывает полное переключение процессора па новую задачу со сменой всего контекста в соответствии с сегментом состояния задачи (TSS). Рассмотрим эти варианты.
Обработка прерываний в контексте текущей задачи
Рассмотрим рис. 3.13, поясняющий обработку прерывания в контексте текущей задачи. При возникновении прерывания процессор по номеру прерывания индексирует таблицу IDT, то есть адрес соответствующего коммутатора определяется путем сложения содержимого поля адреса в регистре 1DTR п номера прерывания, умноженного на 8 (справа к номеру прерывания добавляются три нуля). Полученный дескриптор анализируется, и если его тин соответствует коммутатору trap gate пли коммутатору interrupt gate, то выполняются следующие -действия.
В стек на уровне привилегий текущего сегмента кода помешаются:
О значения SS и SP, если уровень принилстий в коммутаторе выше уровня привилегии ранее исполнявшегося кода;
О регистр флагов EFLAGS;
О регистры CS и IP.
Если рассматриваемому прерыванию соответствовал коммутатор interrupt gate, то запрещаются прерывания (флаг 1Е:=0 в регистре EFLAGS). В случае коммутатора trap gate флаг прерывании не сбрасывается и обработка новых прерывании па период обработки текущего прерывания тем самым не заире шается.
Поле селектора из дескриптора прерываний используется для индексирования таблицы дескрипторов задачи. Дескриптор сегмента заносится в теневой регистр, а смещение относительно начала нового сегмен та кода определяется полем смещения из дескриптора прерывания.
Регистр дескриптора
Рис. 3.13. Схема передачи управления при прерывании в контексте текущей задачи
Таким образом, в случае обработки прерываний, когда дескриптором прерываний является коммутатор interrupt gate или trap gate, мы остаемся в том же виртуальном адресном пространстве, и полной смены контекста текущей задачи нс происходит. Просто мы переключаемся на исполнение другого (как правило, более привилегированною) кода, но также принадлежащего (или, правильнее ска-
зать, доступного) исполняемой задаче. Этот код создается системными программистами, и прикладные программисты его просто используют. В то же время механизмы защиты микропроцессора позволяют обеспечить недоступность этого кода для его исправления (со стороны приложений, его вызывающих) и недоступность самой таблицы дескрипторов прерываний. Удобнее всего код обработчиков прерываний располагать в общем адресном пространстве, то есть селекторы, указывающие на такой код, должны располагаться в глобальной таблице дескрипторов.
Обработка прерываний с переключением на новую задачу
Совершенно иначе осуществляется обработка прерываний в случае, если дескриптором прерываний является коммутатор задачи. Формат коммутатора задачи (task gate) отличается от коммутаторов interrupt gate и trap gate, прежде всего, тем, что в нем вместо селектора сегмента кода, на который передается управление, указывается селектор сегмента состояния задачи (TSS). Это иллюстрируется с помощью рис. 3.14. В результате осуществляется процедура перехода на новую задачу с полной сменой контекста, ибо сегмент состояния задачи полностью определяет новое виртуальное пространство и адрес начала программы, а текущее состояние прерываемой задачи аппаратно (по микропрограмме микропроцессора) сохраняется в ее собственном TSS.
Рис 3 14. Схема передачи управления при прерывании с переключениемна новую задачу
При этом происходит полное переключение на новую задачу с вложением, то есть выполняются следующие действия:
Сохраняются все рабочие регистры процессора в текущем сегменте TSS, базовый адрес этого сегмента берется в регистре TR (см. раздел «Адресация в 32- разрядных микропроцессорах 180x86 при работе в защищенном режиме»),
Текущая задача отмечается как занятая.
По селектору из Task Gate выбирается новый TSS (поле селектора помещается в регистр TR) и загружается состояние новой задачи. Напомним, что загружаются значения регистра LDTR, EFLAGS, восемь регистров общего назначения, указатель команды регистр EIP и шесть сегментных регистров.
Устанавливается бит NT (next task).
В поле обратной связи TSS помещается селектор прерванной задачи
Значения CS:IP, взятые из нового TSS, позволяют найти и выполнить первую команду обработчика прерывания.
Таким образом, коммутатор task gate дает указание процессору произвести переключение задачи, и обработка прерывания осуществляется под контролем отдельной внешней задачи. В каждом сегменте TSS имеется селектор локальной дескрипторной таблицы LDT, поэтому при переключении задачи процессор загружает в регистр LDTR новое значение. Это позволяет обратиться к сегментам кода, которые абсолютно не пересекаются с сегментами кода любых других задач, поскольку именно локальные дескрипторные таблицы обеспечивают эффективное изолирование виртуальных адресных пространств. Новая задача начинает свое выполнение на уровне привилегий, определяемом полем RPL нового содержимого регистра CS, которое загружается из сегмента TSS. Достоинством этого коммутатора является то, что он позволяет сохранить все регистры процессора с помощью механизма переключения задач, тогда как коммутаторы trap gate и interrupt gate сохраняют только содержимое регистров IFLAGS, CS и IP и сохранение других регистров возлагается на программиста, разрабатывающего соответствующую программу обработки прерывания.
Справедливости ради следует признать, что, несмотря на возможности коммутатора task gate, разработчики современных операционных систем достаточно редко его используют, поскольку переключение на другую задачу требует существенно больших затрат времени, а полное сохранение всех рабочих регистров часто и нс требуется. В основном обработку прерываний осуществляют в контексте текущей задачи, так как это приводит к меньшим накладным расходам и повышает быстродействие системы.
Контрольные вопросы и задачи
Вопросы для проверки
Как в реальном режиме работы микропроцессоров i80x86 осуществляется преобразование виртуального адреса в физический?
Какие механизмы виртуальной памяти используются в защищенном режиме работы микропроцессоров 180x86?
Для чего в микропроцессоры 180x86 введен регистр-указатель задачи TR? Какой он разрядности?
Как в микропроцессорах 180x86 реализована поддержка сегментного способа организации виртуальной памяти?
Что понимается под термином «линейный адрес»? Как осуществляется преобразование линейного адреса в физический? А может ли линейный адрес быть равным физическому?
Что дало введение двухшаговой страничной трансляции в механизме страничного способа реализации виртуальной памяти? Как разработчики микропроцессора i80386 решил» проблему замедления доступа к памяти, которое при двухшаговом преобразовании адресов очень существенно?
Что дало введение виртуального режима? Как в этом режиме осуществляется вычисление физического адреса?
Что имеется в микропроцессорах 180x86 для обеспечения защиты адресного пространства задач?
Что такое «уровень прнвилигий»? Сколько уровней привилегий имеется в микропроцессорах 180x86? Для каких целей введено такое количество уровней привилегий?
Что такое текущий уровень привилегий? Что такое эффективный уровень привилегий?
Объясните правила работы с уровнями привилегий для различных типов сегментов
Поясните механизм шлюзования: для чего он предназначен, как осуществляется передача управления на сегменты кода с другими уровнями привилегий.
Расскажите о работе системы прерываний микропроцессоров 180x86 в реальном режиме.
В чем заключаются основные принципиальные отличия работы системы прерываний микропроцессоров 180x86 в защищенном режиме ио сравнению с реальным режимом?
Как осуществляется переход на программу обработки прерываний, если дескриптор прерываний является коммутатором прерываний?
Как осуществляется переход на программу обработки прерываний, если дескрин гор прерываний является коммутатором перехвата?
Как осуществляется переход на нро> рамму обработки прерываний, если дескриптор прерываний является коммутатором задачи?
ГЛАВА 4 Управление вводом/выводом и файловые системы
Необходимость обеспечить программам возможность осуществлять обмен дан пыми с внешними устройствами и при этом не включать в каждую двоичную программу соответствующий двоичный код, осуществляющий собственно управление устройствами ввода/вывода привела разработчиков к созданию системного программного обеспечения и, в частности, самих операционных систем. Программирование задач управления вводом/выводом является наиболее сложным и трудоемким, требующим очень высокой квалификации Потому код, позволяющий осуществлять операции ввода/вывода, стали оформлять в виде системных библиотечных процедур; потом его стали включать не в системы програм мировапия, а в операционную систему с тем, чтобы в каждую отдельно взятую про! рамму его не вставлять, а только позволить обращаться к такому коду. Системы программирования стали генерировать обращения к этому системному коду ввода/вывода и осуществлять только подготовку к собственно операциям ввода/вывода, то есть автоматизировать преобразование данных к соответствующему формату', понятному устройствам, избавляя прикладных программистов от этой сложной и трудоемкой работы. Другими словами, системы программирования вставляют в машинный код необходимые библиотечные подпрограммы ввода/вывода и обращения к тем системным программным модулям, которые, собственно, и управляют операциями обмена между оперативной памятью и внешними устройствами. Таким образом, управление вводом/выводом — это одна из основных функций любой ОС.
С одной стороны, в организации ввода/вывода в различных ОС мною общего С другой стороны, реализация ввода/вывода в ОС так сильно отличается от системы к системе, что очень нелегко выделить и описат ь именно основные принци пы реализации этих функций. Проблема усугубляется еще тем что в большинстве ныне используемых систем эти моменты вообще, как правило, подробно не описаны, и исключение по этому вопросу касается только системы Linux, для
которой имеются комментированные исходные тексты. Детально описываются функции API, реализующие ввод/вывод. Поэтому в этом разделе, самом небольшом по объему, мы рассмотрим только основные идеи и концепции.
Поскольку внешняя память, как правило, реализуется на таких устройствах ввода/вывода, как накопители на магнитных дисках, мы также рассмотрим логическую структуру диска.
Закончим настоящую главу рассмотрением наиболее распространенных файловых систем.
Основные понятия и концепции организации ввода/вывода в ОС
Как известно, ввод/вывод считается одной из самых сложных областей проекта-' рования операционных систем, в которой сложно применить общий подход из-за изобилия частных методов. Сложность возникает из-за огромного числа устройств ввода/вывода разнообразной природы, которые должна поддерживать ОС. При этом перед создателями ОС встает очень непростая задача — не только обеспечить эффективное управление устройствами ввода/вывода, но и создать удобный и эффективный виртуальный интерфейс устройств ввода/вывода, позволяющий прикладным программистам просто считывать или сохранять данные, не обращая внимание на специфику устройств и проблемы распределения устройств между выполняющимися задачами. Система ввода/вывода, способная объединить в одной модели широкий набор устройств, должна быть универсальной. Опа должна учитывать потребности существующих устройств, от простой мыши до клавиатур, принтеров, графических дисплеев, дисковых накопителей, компакт-дисков и даже сетей. С другой стороны, необходимо обеспечить доступ к устройствам ввода/вывода для множества параллельно выполняющихся задач, причем так, чтобы они как можно меньше мешали друг другу.
Поэтому самым главным является следующий принцип' любые операции по управлению вводом/выводом объявляются привилегированными и могут выполняться только кодом самой ОС. Для обеспечения этого принципа в большинстве процессоров даже вводятся режимы пользователя и супервизора. Как правило, в режиме супервизора выполнение команд ввода/вывода разрешено, а в пользовательском режиме — запрещено. Использование команд ввода/вывода в пользовательском режиме вызывает исключение' и управление через механизм прерываний передается коду ОС. Хотя возможны и более сложные системы, в которых в ряде случаев пользовательским программам разрешено непосредственное выполнение команд ввода/вывода.
Еще раз подчеркнем, что, прежде всего, мы говорим о мультипрограммных ОС, для которых существует проблема разделения ресурсов. Одним из основных ви-
Исключение — это определенный вид внутреннего прерывания. Этим термином, во-первых, обозначают некоторое множество синхронных прерываний, а во-вторых, подчеркивают, что ситуация, вызвавшая запрос на прерывание, является исключительной, то есть отличается от обычной.
дов ресурсов являются устройства ввода/вывода и оответствующее программное обеспечение, с помощью которого осуществляется управление обменом данными между внешними устройствами и оперативной памятью. Помимо разделяемых устройств ввода/вывода (эти устройства допускают разделение посредством механизма доступа) существуют неразделяемые устройства. Примерами разделяемого устройства могут служить накопитель на магнитных дисках, устройство для чтения компакт-дисков. Это устройства с прямым доступом. Примеры неразделяемых устройств — принтер, накопитель на магнитных лентах. Это устройства с последовательным доступом. Операционные системы должны управлять и теми и другими устройствами, предоставляя возможность параллельно выполняющимся задачам использовать различные устройства ввода/вывода.
Можно назвать три основные причины, по которым нельзя разрешать каждой отдельной пользовательской программе обращаться к внешним устройствам непосредственно:
Необходимость разрешать возможные конфликты доступа к устройствам ввода/вывода. Например, две параллельно выполняющиеся программы пытаются вывести на печать результаты своей работы. Если не предусмотреть внешнее управление устройством печати, то в результате мы можем получить абсолютно нечитаемый текст, так как каждая программа будет время от времени выводить свои данные, которые будут перемежаться данными другой программы. Другой пример: ситуация, когда одной программе необходимо прочитать данные с некоторого сектора магнитного диска, а другой — записать результаты в другой сектор того же накопителя. Если операции ввода/вывода не будут отслеживаться каким-то третьим (внешним) процессом- арбитром, то после позиционирования магнитной головки для первого запроса может гуг же появиться команда позиционирования головки для второй задачи, и обе операции ввода/вывода не смогут быть выполнены корректно.
Желание увеличить эффективность использования этих ресурсов. Например, у накопителя на магнитных дисках время подвода головки чтепия/записи к необходимой дорожке и обращение к определенному сектору может значительно (до тысячи раз) превышать время пересылки данных. В результате, если задачи по очереди обращаются к цилиндрам, далеко отстоящим друг от друга, то полезная работа, выполняемая накопителем, может быть существенно снижена.
Ошибки в программах ввода/вывода могут привести к краху всех вычислительных процессов, ибо часть операций ввода/вывода осуществляется тля самой операционной системы. В ряде ОС системный ввод/вывод имеет существенно более высокие привилегии, чем ввод/вывод задач пользователя. Поэтому системный код, управляющий операциями ввода/вывода, очень тщательно отлаживается и оптимизируется для повышения надежности вычислений и эффективности использования оборудования.
Итак, управление вводом/выводом осуществляется операционной системой, компонентом, который чаще .всего называют (.упервизором ввода/вывода. В пере чень основных задач, возлагаемых на супервизор, входят следующие:
супервизор ввода/вывода получает запросы па ввод/вывод от прикладных задач и от программных модулей самой операционной системы. Эти запросы проверяются на корректность, и если запрос выполнен по спецификациям и не содержит ошибок, он обрабатывается дальше, в противном случае пользователю (задаче) выдается соответствующее диагностическое сообщение о пе- действигелыюсти (некорректности) запроса;
супервизор ввода/вывода вызывает соответствующие распределители каналов и контроллеров, планирует ввод/вывод (определяет очередность предоставления устройств ввода/вывода задачам, затребовавшим их). Запрос на ввод/ вывод либо тут же выполняется, либо ставится в очередь на выполнение;
супервизор ввода/вывода инициирует операции ввода/вывода (передает управление соответствующим драйверам) и в случае управления вводом/выводом с использованием прерываний предоставляет процессор диспетчеру задач с тем, чтобы передать его первой задаче, стоящей в очереди на выполнение;
при получении сигналов прерываний от устройств ввода/вывода супервизор идентифицирует их (рис. 4 1) и передает управление соответствующей! про- грамме обработки прерывания (как правило, на секцию продолжения драйвера — см. раздел «Режимы управления вводом/выводом»);
супервизор ввода/вывода осуществляет передачу сообщений об ошибках, если таковые происходят в процессе управления операциями ввода/вывода;
супервизор ввода/вывода посылает сообщения о завершении операции ввода/вывода запросившему эту операцию процессу и снимает его с состояния ожидания ввода/вывода, если процесс ожидал завершения операции.
В случае если устройство ввода/вывода является инициативным, управление со стороны супервизора ввода/вывода будет заключаться в активизации соответсг- вующего вычислительного процесса (перевод его в состояние готовности к выполнению).
Таким образом, прикладные программы (а в общем случае — все обрабатывающие программы) не могут непосредственно связываться с устройствами ввода/ вывода независимо от использования устройств (монопольно или совместно) Установив соответствующие значения параметров в запросе па ввод/вывод, определяющих требуемую операцию и количество потребляемых ресурсов, они могут передать управление супервизору ввода/вывода, который и запускает необходимые логические и физические операции.
Упомянутый выше запрос на виод/вывод должен удовле1ворять требованиям API той операционной системы, в среде которой выполняется приложение. Параметры, указываемые в запросах на ввод/вывод, передаются не только в вызывающих последовательностях, создаваемых по спецификациям API, по и как данные, хранящиеся в соответствующих системных таблицах. Все параметры, которые будут стоять в вызывающей последовательности, поставляются компилятором и отражают требования программиста и постоянные сведения об операционной системе и архитектуре компьютера в целом Переменные сведения о вычислительной системе (се конфигурация, состав оборудования, состав и особенности системного программного обеспечения) содержался в специальных системных таблицах. Процессору, каналам прямого доступа в память, контрол юрам необходимо передавать конкретную двоичную информацию, с помощью которой и осуществляется управление оборудованием. Эта конкретная двоичная информация в виде кодов и данных часто готовится с помощью препроцессоров, но часть се хранится в системных таблицах.
Режимы управления вводом/выводом
Как известно, имеются два основных режима ввода/вывода: режим обмена с опросом готовности устройства ввода/вывода и режим обмена с прерываниями. Рассмотрим рис. 4.1.
готовности
Рис. 4.1. Управление вводом/выводом
Пусть для простоты управление вводом/выводом осуществляет центральный процессор (в этом случае часто говорят о наличии программного канала обмена данными между внешними устройством и оперативной памятью, в отличие от канала прямого доступа к памяти, при котором управление вводом/вывоДом осуществляет специальное дополнительное оборудование; эти вопросы мы обсудим несколько позже). Центральный процессор посылает устррцству управления команду выполнить некоторое действие устройству ввода/вывода Последнее исполняет команду, транслируя сигналы, понятные центральному устройству и
устройству управления в сигналы, понятные устройству ввода/вывода. Но быстродействие устройства ввода/вывода намного меньше быстродействия центрального процессора (порой на несколько порядков). Поэтому сигнал готовности (транслируемый или генерируемый устройством управления и сигнализирующий процессору о том, что команда ввода/вывода выполнена и можно выдать новую команду для продолжения обмена данными) приходится очень долго ожидать, постоянно опрашивая соответствующую линию интерфейса на наличие или отсутствие нужного сигнала. Посылать новую команду, не дождавшись сигнала готовности, сообщающего об исполнении предыдущей команды, бессмысленно. В режиме опроса готовности драйвер, управляющий процессом обмена данными с внешним устройством, как раз и выполняет в цикле команду «проверить наличие сигнала готовности». До тех пор пока сигнал готовности ие появится, драйвер ничего другого не делает. При этом, естественно, нерационально пспользу- ется время центрального процессора. Гораздо выгоднее, выдав команду ввода/ вывода, на время забыть об устройстве ввода/вывода и перейти на выполнение другой программы. А появление сигнала готовности трактовать как запрос на прерывание от устройства ввода/вывода. Именно эти сигналы готовности и являются сигналами запроса на прерывание (см. раздел «Прерывания», глава 1).
Режим обмена с прерываниями по своей сути является режимом асинхронного управления. Для того чтобы не потерять связь с устройством (после того как процессор выдал очередную команду по управлению обменом данными и переключился на выполнение других программ), может быть запущен отсчет времени, в течение которого устройство обязательно должно выполнить команду и выдать таки сигнал запроса на прерывание. Максимальный интервал времени, в течение которого устройство ввода/вывода или его контроллер должны выдать сигнал запроса на прерывание, часто называют уставкой тайм-аута. Если это время истекло после выдачи устройству очередной команды, а устройство так и не ответило, то делается вывод о том, что связь с устройством потеряна и управлять им больше нет возможности. Пользователь и/или задача получают соответствующее диагностическое сообщение.
Драйверы, работающие в режиме прерываний, представляют собой сложный комплекс программных модулей и могут иметь несколько секций: секцию запуска, одну или несколько секций продолжения и секцию завершения.
Секция запуска инициирует операцию ввода/вывода. Эта секция запускается для включения устройства ввода/вывода либо просто для инициации очередной операции ввода/вывода.
Секция продолжения (их может быть несколько, если алгоритм управления обменом данными сложный и требуется несколько прерываний для выполнения одной логической операции) осуществляет основную работу по передаче данных. Секция продолжения, собственно говоря, и является основным обработчиком прерывания. Используемый интерфейс может потребовать для управления вводом/выводом несколько последовательностей управляющих команд, а сигнал прерывания у устройства, как правило, только один. Поэтому после выполнения очередной секции прерывания супервизор прерываний при следующем сигнале готовности должен передать управление другой секции. Это делается за счет изменения адреса обработки прерывания после выполнения очередной секции,
сели же имеется только одна секция прерываний, то она сама передает управление тому или иному модулю обработки.
Секция завершения обычно выключает устройство ввода/вывода либо просто завершает операцию.
Управление операциями ввода/вывода в режиме прерываний требует больших усилий со стороны системных программистов — такие программы создавать сложнее, чем те, что работают в режиме опроса готовности. Примером тому может служить ситуация с драйверами, обеспечивающими печать. Так, в ОС Windows (и Windows 9х, и Windows NT) драйвер печати через параллельный порт работа ст не в режиме с прерываниями, как это сделано в других ОС а в режиме опроса готовности, что приводит к 100%-й загрузке центрального процессора на все время печати При этом, естественно, выполняются и другие задачи, запущенные на исполнение, ио исключительно за счет того, что ОС Windows реализует вытесняющую му штизадачность и время от времени прерывает процесс управления печатью и передает центральный процессор остальным задачам.
Закрепление устройств, общие устройства ввода/вывода
Как известно, многие уст ройства нс допускают совместного использования. Прежде всего, это устройства с последовательным доступом. Такие устройства могут стать закрепленными, то есть быть предоставленными некоторому вычислительному процессу на все время жизни этого процесса. Однако это приводит к тому, что вычислительные процессы часто не могут выполняться параллельно — они ожидают освобождения устройств ввода/вывода Для организации использования многими параллельно выполняющимися задачами устройств ввода/вывода, которые не могут быть разделяемыми, вводится понятие виртуальных устройств. Использование принципа виртуализации позволяет повысить эффективность вы- числ и тельной систомы.
Вообще говоря, понятие виртуального устройства шире, нежели использование этого термина для обозначения спулинга (SPOOLing — simultaneous peripheral operation on-line, то есть имитация работы с устройством в режиме «он-лайн»). Главная задача спулинга — создать видимость параллельного разделения устройства ввода/вывода с последовательным доступом, которое фактически должно использоваться только монопольно и быть закрепленным. Например, мы уже говорили, что в случае, когда несколько приложений должны выводить на печать результаты своей работы если разрешить каждому такому приложению печатать строку но первому же требованию, то это приведет к потоку строк, не представляющих никакой ценности. Однако можно каждому вычислительному процессу предоставлять не реальный, а виртуальный принтер, и поток выводимых символов (или управляющих кодов для их печати) сначала направлять в специальный файл на магнитном диске. Затем, по окончании виртуальной печати, в соответ-
ствии с принятой дисциплиной обслуживания и приоритетами приложений выводить содержимое спул-файла на принтер. Системный процесс, который управляет спул-файлом, называется спулером (spool-reader или spool-writer).
Основные системные таблицы ввода/вывода
Каждая ОС имеет свои таблицы ввода/вывода, их состав (количество и назначение каждой таблицы) может сильно отличаться. В некоторых ОС вместо таблиц создаются списки, хотя использование статических структур данных для организации ввода/вывода, как правило, приводит к большему быстродействию. Здесь очень трудно вычленить общие составляющие, гем более что подробной документации на эту тему крайне мало, только если воспользоваться материалами ныне устаревших ОС. Тем нс менее попыгаемся это сделать, опираясь на идеи семейства простых, ио эффективных ОС реального времени, разработанных фирмой Hewlett-Packard для своих мини-комньютсров.
Исходя из принципа управления вводом/выводом через супервизор ОС и учитывая, что драйверы устройств ввода/вывода используют механизм прерываний для установления обратной связи центральной части с внешними устройствами, можно сделать вывод о необходимости создания по крайней мере трех системных таблиц.
Первая таблица (идй список) содержит информацию обо всех устройствах ввода/вывода, подключенных к вычислительной системе. Назовем ее условно таблицей оборудования (equipment table), а каждый элемент этой таблицы пусть называется UCB (unit control block, блок управления устройством ввода/вывода). Каждый элемент UCB таблицы оборудования, как правило, содержит следующую информацию об устройстве:
тип устройства, его конкретная модель, символическое имя и характеристики устройства;
как это устройство подключено (через какой интерфейс, к какому разъему, какие порты и линия запроса прерывания используются и т. д.);
номер и адрес канала (и подканала), если такие используются для управления устройством;
указание на драйвер, который должен управлять этим устройством, адрес секции запуска и секции продолжения драйвера;
информация о том, используется или нет буферирование при обмене данными с этим устройством, «имя» (или просто адрес) буфера, если такой выделяется из системной области памяти;
уставка тайм-аута и ячейки для счетчика тайм-аута;
состояние устройства;
поле указателя для связи задач, ожидающих устройство, и, возможно, много еще каких сведений.
Поясним перечисленное. Поскольку во многих ОС драйверы могут обладать свойством реентерабельности (напомним, это означает, что один и тот же экземпляр программного модуля может обеспечить параллельное обслуживание сразу нескочьких однотипных устройств), то в элементе UCB должна храниться либо непосредственно сама информация о текущем состоянии устройства и сами переменные для реентерабельной обработки, либо указание на место где такая информация может быть найдена. Наконец, важнейшим компонентом элемента таблицы оборудования является указатель на дескриптор той задачи, которая сейчас использует данное устройство. Если устройство свободно, то поле указателя будет иметь нулевое значение. Если же устройство уже занято и рассматриваемый указатель не нулевой, то новые запросы к устройству фиксируются посредством образования списка из дескрипторов тех задач, которые сейчас ожидают данное устройство.
Вторая таблица предназначена для реализации еще одного принципа виртуализации устройств ввода/вывода — независимости от устройства. Желательно, чтобы программист не был озабочен учетом конкретных параметров (и/или возможностей) того или иного устройства ввода/вывода, которое установлено (или не установлено) в компьютер. Для нею должны быть важны только самые общие возможности, характерные для данного класса устройств ввода/вывода, которыми он желает воспользоваться. Например, принтер должен уметь выводить (печатать) символы или графическое изображение. А накопитель на магнитных дисках — считывать или записывать по указанному адресу (в координатах C-H-S1) порцию данных. Хотя чаще всего программист и не использует прямую адресацию при работе с магнитными дисками, а работает на уровне файловой системы (см. главу 4). Однако в таком случае уже разработчики файловой системы нс должны зависеть от того, накопитель какого конкретного типа и модели, а также какого производителя используется в данном конкретном компьютере (например, HDD IBM DTLA 307030, WDAC 450AA или какой-нибудь еще). Важным должен быть только сам факт существования накопителя, имеющего некоторое количество цилиндров, головок чтения, записи и секторов на дорожке магнитного диска. Упомянутые значения количества цилиндров, головок и секторов должны быть взяты из элемента таблицы оборудования. При этом для программиста также не должно иметь значения каким образом то или иное устройство подключено к вычислительном системе, а не только какая конкретная модель устройства используется. Поэтому в запросе на ввод/вывод программист указывает именно логическое имя устройства. Действительное устройство, которое сопоставляется виртуальному (логическому), выбирается супервизором с помощью таблицы, о которой мы сейчас говорим. Итак, способ подключения устройства его конкретная модель и соответствующий ей драйвер содержатся в уже рассмотренной таблице оборудования. Но для того, чтобы связать некоторое виртуальное устройство, использованное программистом при создании приложения с системной таблицей, отображающей информацию о том, какое конкретно устройство и каким образом подключено к компьютеру, используется вторая системная таблица. Назовем ее условно таблицей описания виртуальных логических устройств
C-H-S (cylinder-head-sector) — номер цилиндра, номер головки и номер сектора.
(DRT, device reference table). Назначение втой второй таблицы — установление связи между виртуальными (логическими) устройствами и реальными устройствами, описанными посредством первой таблицы оборудования. Другими словами, вторая таблица позволяет супервизору перенаправить запрос на ввод/вывод из приложения на те программные модули и структуры данных, которые (или адреса которых) хранятся в соответствующем элементе первой таблицы. Во многих многопользовательских системах такая таблица не одна, а несколько: одна общая и по одной — па каждого пользователя, что позволяет строить необходимые связи между логическими (символьными) именами устройств и реальными физическими устройствами, которые имеются в системе.
Наконец, третья таблица необходима для организации обратной связи между центральной частью и устройствами ввода/вывода. Это таблица прерываний, ко торая указывает для каждого сигнала запроса на прерывание тот элемент UCB, который сопоставлен данному устройству, подключенному так, что оно использует настоящую линию (сигнал) прерывания. Как системная таблица ввода/вывода, таблица прерываний может в явном виде и не присутствовать. В принципе можно сразу из основной таблицы прерываний попадать на программу обработки (драйвер), имеющей связи с элементом UCB. Важно наличие связи между сигналами прерываний и таблицей оборудования.
В современных сложных ОС имеется гораздо больше системных таблиц или списков, используемых для организации процессами управления операциями ввода/вывода. Например, одной из возможных и часто реализуемых информационных структур, сопровождающих практически каждый запрос на ввод/вывод, является блок управления данными (data control block, DCB). Назначение этого DCB — подключение препроцессоров к процессу подготовки данных на ввод/вывод, то есть учет конкретных технических характеристик и используемых преобразований. Это необходимо для того, чтобы имеющееся устройство получало не какие-го непонятные ему коды либо форматы данных, которые не соответствуют режиму его работы, а коды, созданные специально под данное устройство и используемый в настоящий момент формат представления данных. Взаимосвязи между описанными таблицами изображены на рис, 4.2.
ТаблицаТаблицаТаблица
логических именоборудованияпрерываний
Рис. 4 2. Взаимосвязь системных таблиц ввода/вывода
Теперь нам осталось лишь еще раз, с учетом иеложенных принципов и таблиц, рассмотреть процесс управления вводом/вывода с помощью рис 4.3.
Рис. 4.3 Процесс управления вводом/выводом
Запрос на операцию ввода/вывода от выполняющейся профаммы поступает на супервизор (действие 7). Тот проверяет системный вызов на соответствие принятым спецификациям и в случае ошибки возвращает задаче соответствующее сообщение (действие 7-7). Если же запрос корректен, то он перенаправляется в супервизор ввода/вывода (действие 2). Последний по логическому (виртуальному) имени с помощью таблицы DRT находит соответст вующий элемент UCB в таблице оборудования. Если устройство уже занято, то описатель задачи, запрос которой сейчас обрабатывается супервизором ввода/вывода, помещается в список задач, ожидающих настоящее устройство. Если же устройство свободно, то супервизор ввода/вывода определяет из UCB тип устройства и при необходимости запускает препроцессор, позволяющий получить последовательность управляющих кодов и данных, которую сможет правильно понять и отработать устройство (действие 3). Когда «программа» управления операцией ввода/вывода будет готова, супервизор ввода/вывода передаст управление соответствующему драйверу на секцию запуска (действие 4\ Драйвер инициализирует операцию управления, обнуляет счетчик тайм-аута и возвращает управление супервизору (диспетчеру задач) с тем, чтобы он поставил па процессор готовую к исполнению задачу (действие 5). Система работает своим чередом, но когда устройство ввода/вывода отработает посланную ему команду, оно выставляет сигнал запроса на прерывания, по которому через таблицу прерываний управление передается на секцию продолжения (действие 6). Получив новую команду, устройство вновь начинает се обрабатывать, а управление процессором опять передается
диспетчеру задач, и процессор продолжает полезную работу. Таким образом, получается параллельная обработка задач, па фоне которой процессор осуществляет управление операциями ввода/вывода.
Очевидно, что если имеются специальные аппаратные средства для управления вводом/выводом, снимающие эту работу с центрального процессора (речь идет о каналах прямого доступа к памяти), то в функции центрального процессора будут по-прсжпему входить все только что рассмотренные шаги, за исключением последнего — непосредственного управления операциями ввода/вывода. В случае использования каналов прямого доступа к памяти последние исполняют соответствующие канальные программы и разгружают центральный процессор, избавляя его от непосредственного управления обменом данными между памятью и внешними устройствами.
При описании этой схемы мы не стали затрагивать вопросы распределения каналов, контроллеров и собственно самих устройств. Также были опущены детали получения канальных программ. Будем считать, что они выходят за круг вопросов, рассматриваемых в учебнике ио дисциплине «Системное программное обеспечение», тем более что в последнем государственном образовательном стандарте для направления 46 — «Информатика и вычислительная техника» имеется отдельная общепрофессиональная дисциплина «Операционные системы» и специальная дисциплина «Интерфейсы периферийных устройств».
Синхронный и асинхронный ввод/вывод
Задача, выдавшая запрос на операцию ввода/вывода, переводится супервизором в состояние ожидания завершения заказанной операции Когда супервизор получает от секции завершения сообщение о том, что операция завершилась, он переводит задачу в состояние готовности к выполнению, и она продолжает свою работу. Эта ситуация соответствует синхронному вводу/выводу. Синхронный ввод/вывод является стандартным для большинства ОС. Чтобы увеличить скорость выполнения приложений, было предложено при необходимости использовать асинхронный ввод/вывод.
Простейшим вариантом асинхронного вывода является так называемый буферированный вывод данных на внешнее устройство, при котором данные из при ложеийя перелаются нс непосредственно па устройство ввода/вывода, а в специальный системный буфер. В этом случае логически операция вывода для приложения считается выполненной сразу же, и задача может нс ожидать окончания действительного процесса передачи данных на устройство Процессом реального вывода данных из системного буфера занимается супервизор ввода/ вывода. Естественно, что выделением буфера из системной области памяти занимается специальный системный процесс по указанию супервизора ввода/вывода. Итак, для рассмотренного случая вывод будет асинхронным, если, во-первых, в запросе на ввод/вывод было указано па необходимость буферирования данных, а во-вторых, если устройство ввода/вывода допускает такие асинхронные операции и это отмечено в UCB
Можно организовать и асинхронный ввод данных. Однако для этого необходимо не только выделить область памяти для временного храпения считываемых с устройства данных и связать выделенный буфер с задачей, заказавшей операцию, но н сам запрос на операцию ввода/вывода разбить на две части (па два запроса). В первом запросе указывается операция на считывание данных, подобно тому как это делается при синхронном вводе/выводе. Однако тип (код) запроса используется другой, и в запросе указывается еще по крайней мере один дополнительный параметр — имя (код) того системного объекта, которое получает задача в ответ па запрос и которое идентифицирует выделенный буфер. Получив имя буфера (будем этот системный объект условно называть таким образом, хотя в различных ОС для его обозначения используются и другие термины, например — класс), задача продолжает свою работу. Здесь очень важно подчеркнуть, что в результате запроса па асинхронный ввод данных задача не переводится супервизором ввода/вывода в состояние ожидания завершения операции ввода/ вывода, а остается в состоянии выполнения или в состоянии готовности к выполнению. Через некоторое время, выполнив необходимый код, который был определен программистом, задача выдает второй запрос па завершение операции ввода/вывода. В этом втором запросе к тому же устройству, который, естественно, имеет другой код (или имя запроса), задача указывает имя системного объекта (буфера для асинхронного ввода данных) и в случае успешного завершения операции считывания данных тут же получает их из системного буфера. Если же данные еще не успели до конца переписаться с внешнего устройства в системный буфер, супервизор ввода/вывода переводит задачу в состояние ожидания завершения операции ввода/вывода, и далее все напоминает обычный синхронный ввод данных.
Обычно асинхронный ввод/вывод предоставляется в большинстве мультипрограммных ОС, особенно если ОС поддерживает мультизадачное™ с помощью механизма тредов. Однако если асинхронный ввод/вывод в явном виде отсутствует, его идеи можно реализовать самому, организовав для вывода данных само стоятсльный поток.
Аппаратуру ввода/вывода можно рассматривать как совокупность аппаратурных процессоров, которые способны работать параллельно относительно друг друга, а также относительно центрального процессора (процессоров). На таких «процессорах» выполняются так называемые внешние процессы. Например, для внешнего устройства (устройства ввода/вывода) внешний процесс может представлять собой совокупность операций, обеспечивающих перевод печатающей головки, продвижение бумаги на одну позицию, смену цвета чернил или печать каких-то символов. Внешние процессы, используя аппаратуру ввода/вывода, взаимодействуют как между собой, так и с обычными «программными» процессами, выполняющимися па центральном процессоре. Важным при этом является то рб- стоятельство, что скорости выполнения внешних процессов будут существенно (порой, па порядок или больше) отличаться от скорости выполнения обычных {«внутренних») процессов. Для своей нормальной работы внешние и внутренние процессы обязательно должны синхронизироваться. Для сглаживания эф фекта сильного несоответствия скоростей между внутренними и внешними про
цессами используют упомянутое выше буферирование. Таким образом, можно говорить о системе параллельных взаимодействующих процессов (см. главу 6)
Буферы являются критическим ресурсом в отношении внутренних (программных) и внешних процессов, которые при параллельном своем развитии информационно взаимодействуют. Через буфер (буферы) данные либо посылаются от некоторого процесса к адресуемому внешнему (операция вывода данных на внешнее устройство), либо от внешнего процесса передаются некоторому программному процессу (операция считывания данных) Введение буферирования как средства информационного взаимодействия выдвигает проблему управления этими системными буферами, которая решается средствами супервизорной части ОС. При этом на супервизор возлагаются задачи не только по выделению и освобождению буферов в системной области памяти, но и синхронизации процессов в соответствии с состоянием операций по заполнению или освобождению буферов, а также их ожидания, если свободных буферов в наличии пет, а запрос на ввод/вывод требует буферирования. Обычно супервизор ввода/вывода для решения перечисленных задач использует стандартные средства синхронизации, принятые в данной ОС. Поэтому если ОС имеет развитые средства для решения проблем параллельного выполнения взаимодействующих приложений и задач, то, как правило, она реализует и асинхронный ввод/вывод.
Кэширование операций ввода/вывода при работе с накопителями на магнитных дисках
Как известно, накопители на магнитных дисках обладают крайне низкой скоростью по сравнению с быстродействием центральной части компьютера. Разница в быстродействии отличается на несколько порядков. Например, современные процессоры за один такт работы, а они работают уже с частотами в 1 ГГц и более, могут выполнять по две операции. Таким образом, время выполнения операции (с позиции внешнего наблюдателя, не видящего конвейеризации при выполнении машинных команд, благодаря которой производительность возрастает в несколько раз) может составлять 0,5 нс (!). В то же время переход магнитной головки с дорожки на дорожку составляет несколько миллисекунд. Такие же временные интервалы имеют место и при ожидании, пока под головкой чтения/ записи не окажется нужный сектор данных. Как известно, в современных приводах средняя длительность на чтение случайным образом выбранного сектора данных составляет около 20 мс, что существенно медленнее, чем выборка команды или операнда из оперативной памяти и уж тем более из кэша. Правда, после этого данные читаются большим пакетом (сектор, как мы уже говорили, имеет размер в 512 бантов, а при операциях с диском часто читается или записывается сразу несколько секторов). Таким образом, средняя скорость работы процессора с оперативной памятью на 2-3 порядка выше, чем средняя скорость передачи данных из внешней памяти на магнитных дисках в оперативную память.
Для того чтобы сгладить такое сильное несоответствие в производительности основных подсистем, используется буферирование и/или кэширование данных. Простейшим вариантом ускорения дисковых операций чтения данных можно считать использование двойного буферирования. Его суть заключается в том, что пока в один буфер заносятся данные с магнитного диска, из второго буфера ранее считанные данные могут быть прочитаны и переданы запросившей их задаче. Аналогичный процесс происходит и при записи данных. Буферирование используется во всех операционных системах, но помимо буферирования применяется и кэширование. Кэширование исключительно полезно в том случае, когда программа неоднократно читает с диска одни и те же данные. После того как они один раз будут помещены в кэш, обращений к диску больше не потребуется и скорость работы программы значительно возрастет.
Если не вдаваться в подробности, то под кэшем можно понимать некий пул буферов, которыми мы управляем с помощью соответствующего системного процесса. Если мы считываем какое-то множество секторов, содержащих записи того или иного файла, то эти данные, пройдя через кэш, там остаются (до тех пор, пока другие секторы не заменят эти буферы). Если впоследствии потребуется повторное чтение, то данные могут быть извлечены непосредственно из оперативной памяти без фактического обращения к диску. Ускорить можно и операции записи: данные помешаются в кэш, и для запросившей эту операцию задачи можно считать, что они уже фактически и записаны. Задача может продолжить свое выполнение, а системные внешние процессы через некоторое время запишут данные на диск. Это называется операцией отложенной записи (lazy write, «ленивая запись»). Если отложенная запись отключена, только одна задача может записывать на диск свои данные. Остальные приложения должны ждать своей очереди. Это ожидание подвергает информацию риску не меньшему (если не большему), чем отложенная запись, которая к тому же и более эффективна по скорости работы с диском.
Интервал времени, после которого данные будут фактически записываться, с одной стороны, желательно выбрать больше, поскольку если потребуется еще раз прочитать эти данные, то они уже и так фактически находятся в кэше. И после модификации эти данные опять же помещаются в быстродействующий кэш. С другой стороны, для большей надежности данные желательно поскорее отправить во внешнюю память, поскольку она энергонезависима и в случае какой-нибудь аварии (например, нарушения питания) данные в оперативной памяти пропадут, в то время как на магнитном диске они с большой вероятностью останутся в безопасности. Количество буферов, составляющих кэш, ограничено, поэтому возникает ситуация, когда вновь прочитанные или записываемые новые секторы данных должны будут заменить данные в этих буферах. Возможно использование различных дисциплин, в соответствии с которыми будет назначен какой-либо буфер под вновь затребованную операцию кэширования.
Кэширование дисковых операций может быть существенно улучшено за счет введения техники упреждающего чтения (read ahead). Она основана на чтении с диска гораздо большего количества данных, чем на самом деле запросила опера-
ционная система или приложение. Когда некоторой программе требуется считать с диска только один сектор, программа кэширования читает еще и несколько дополнительных блоков данных. А операции последовательного чтения нескольких секторов фактически несущественно замедляют операцию чтения затребованного сектора с данными. Поэтому, если программа вновь обратится к диску, вероятность того, что нужные ей данные уже находятся в кэше, достаточно высока. Поскольку передача данных из одной области памяти в другую происходит во много раз быстрее, чем чтение их с диска, кэширование существенно сокращает время выполнения операций с файлами.
Итак, путь информации от диска к прикладной программе пролегает как через буфер, так и через файловый кэш Когда приложение запрашивает с диска данные, программа кэширования перехватывает этот запрос и читает вместе с необходимыми секторами еще и несколько дополнительных. Затем она помешает в буфер требующуюся задаче информацию и ставит об этом в известность операционную систему. Операционная система сообщает задаче, что ее запрос выполнен и данные с диска находятся в буфере. При следующем обращении приложения к диску программа кэширования прежде всего проверяет, не находятся ли уже в памяти затребованные данные. Если это так, то она копирует их в буфер; если же их в кэше нет, то запрос на чтение диска перелается операционной системе. Когда задача изменяет данные в буфере, они копируются в кэш.
В ряде ОС имеется возможность указать в явном виде параметры кэширования, в то время как в других за эти параметры отвечает сама ОС. Так, например, в системе Windows NT нет возможности в явном виде управлять ни объемом файлового кэша, пи парамеграми кэширования. В системах Windows 95/98 такая возможность уже имеется, но опа представляет не слишком богатый выбор. Фактически мы можем указать только объем памяти, отводимый для кэширования, и объем порции данных (буфер или chunk), из которых набирается кэш. В файле System.ini есть возможность в секции [VCACHE] прописать, например, следующие значения:
[vcache]
MinFi1eCache-4096
MaxFi1eCache-32768
ChunkSize-512
Здесь указано, что минимально под кэширование данных зарезервировано 4 Мбайт оперативной памяти, максимальный объем кэша может достигать 32 Мбайт, а размер данных, которыми манипулирует менеджер кэша, равен одному сектору.
В других ОС можно указывать больше параметров, определяющих работу подсистемы кэширования. Пример, демонстрирующий эти возможности, можно посмотреть в разделе «Файловая система HPFS».
Помимо описанных действий ОС может выполнять и работу по оптимизации перемещения головок чтепия/записи данных, связанного с выполнением запросов от параллельно выполняющихся задач. Время, необходимое на получение данных с магнитного диска, складывается из времени перемещения магнитной
головки на требуемый цилиндр и времени ожидания заданного сектора; временем считывания найденного сектора и затратами на передачу этих данных в оперативную память мы можем пренебречь. Таким образом, основные затраты времени уходят на поиск данных. В мультипрограммных ОС при выполнении многих задач запросы на чтение и запись данных могут идти таким потоком, что при их обслуживании образуется очередь. Если выполнять эти запросы в порядке поступления их в очередь, то вследствие случайного характера обращений к тому или иному сектору магнитного диска мы можем иметь значительные потерн времени на поиск данных. Напрашивается очевидное решение: поскольку выполнение переупорядочивания запросов с целью минимизации затрат времену на поиск данных можно выполнить очень быстро (практически этим временем можно пренебречь, учитывая разницу в быстродействии центральной части и устройств ввода/вывода), то необходимо найти метод, позволяющий перестраивать очередь запросов оптимальным образом. Изучение этой проблемы позволило найти наиболее эффективные дисциплины планирования.
Перечислим известные дисциплины, в соответствии с которыми можно перестраивать очередь запросов на операции чтения/записи данных [28]'
SSTF (shortest seek time — first) — с наименьшим временем поиска — первым. В соответствии с этой дисциплиной при позиционировании магнитных головок следующим выбирается запрос, для которого необходимо минимальное перемещение с цилиндра на цилиндр, даже если этот запрос не был первым в очереди на ввод/вывод. Однако для этой дисциплины характерна резкая дискриминация определенных запросов, а ведь они могут идти от высокоприоритетных задач. Обращения к диску проявляют тенденцию концентрироваться, в результате чего запросы на обращение к самым внешним и самым внутренним дорожкам могут обслуживаться существенно дольше и нет никакой гарантии обслуживания. Достоинством такой дисциплины является максимально возможная пропускная способность дисковой подсистемы.
Scan (сканирование). По этой дисциплине головки перемещаются то в одном, то в другом «привилегированном» направлении, обслуживая «по пути» подходящие запросы. Если при перемещении головок чтения/записи более нет попутных запросов, то движение начинается в обратном направлении
О Next-Step Scan — отличается от предыдущей дисциплины тем, что на каждом проходе обслуживаются только запросы, которые уже существовали па момент начала прохода. Новые запросы, появляющиеся в процессе перемещения головок чтения/записи, формируют новую очередь запросов, причем таким образом, чтобы их можно было оптимально обслужить на обратном ходу.
О C-Scan (циклическое сканирование). По этой дисциплине головки перемещаются циклически с самой наружной дорожки к внутренним, по пути обслуживая имеющиеся запросы, после чего вновь переносятся к наружным цилиндрам. Эту дисциплину иногда реализуют таким образом, чтобы запросы, поступающие во время текущего прямого хода головок, обслуживались не попутно, а при следующем ходе, что позволяет исключить дискриминацию запросов к самым крайним цилиндрам; она характеризуется очень малой дисперсией времени ожидания обслуживания [28]. Эту дисциплину обслуживания часто называют ^элеваторной».
Функции файловой системы ОС и иерархия данных
Напомним, что под файлом обычно понимают набор данных, организованных в виде совокупности записей одинаковой структуры. Для управления этими данными создаются соответствующие системы управления файлами. Возможность иметь дело с логическим уровнем структуры данных и операций, выполняемых над ними в процессе их обработки, предоставляет файловая система. Таким образом, файловая система — это набор спецификаций и соответствующее им программное обеспечение, которые отвечают за создание, уничтожение, организацию, чтение, запись, модификацию и перемещение файловой информации, а также за управление доступом к файлам и за управление ресурсами, которые используются файлами. Именно файловая система определяет способ организации данных на диске или на каком-нибудь ином носителе данных. В качестве примера можно привести файловую систему FAT, реализация для которой имеется в абсолютном большинстве ОС, работающих в современных ПК*.
Как правило, все современные ОС имеют соответствующие системы управления файлами. В дальнейшем постараемся различать файловую систему и систему управления файлами.
Система управления файлами является основной подсистемой в абсолютном большинстве современных операционных систем, хотя в принципе можно обходиться и без нее. Во-первых, через систему управления файлами связываются по данным все системные обрабатывающие программы. Во-вторых, с помощью этой системы решаются проблемы централизованного распределения дискового пространства и управления данными. В-третьих, благодаря использованию той или иной системы управления файлами пользователям предоставляются следующие возможности:
создание, удаление, переименование (и другие операции) именованных наборов данных (именованных файлов) из своих программ или посредством спе- циальных управляющих программ, реализующих функции интерфейса пользователя с его данными и активно использующих систему управления файлами;
работа с не дисковыми периферийными устройствами как с файлами;
обмен данными между файлами, между устройствами, между файлом и устройством (и наоборот);
работа с файлами с помощью обращений к программным модулям системы управления файлами (часть API ориентирована именно на работу с файлами);
зашита файлов от несанкционированного доступа.
1 В MS-DOS, OS/2, Windows 95, Windows NT, Linux имеется поддержка работы с файлами, организованными по принципам FAT. Однако программные модули соответствующих файловых систем не взаимозаменяемы. Кроме этого, все эти файловые системы имеют свои индивидуальные особенности и ограничения. Таким образом, иногда только из контекста ясно, о чем идет речь — о принципах работы файловой системы или о ее конкретной реализации.
В некоторых ОС может быть несколько систем управления файлами, что обеспечивает им возможность работа ь с несколькими файловыми системами. Очевидно, что системы управления файлами, будучи компонентом ОС, не являются независимыми от этой ОС, поскольку они активно используют соответствующие вызовы API (application р ogram nterface, прикладной программный интерфейс). С другой стороны, системы управления файлами сами дополняют API новыми вызовами. Можно сказать, что основное назначение файловой системы и соответствующей ей системы управления файлами — организация удобного доступа к данным, организованным как файлы, то есть вместо низкоуровневого доступа к данным с указанием конкретных физических адресов нужной нам записи используется логический доступ с указанием имени файла и записи в нем.
Другими словами, термин «файловая система» определяет, прежде всего, принципы доступа к данным, организованных в файлы. Этот же термин часто используют и по отношению к конкретным файлам, расположенным на том или ином носителе данных. А термин «система управления файлами» следует употреблять по отношению к конкретной реализации файловой системы, то есть это — комплекс программных модулей, обеспечивающих работу с файлами в конкретной операционной системе.
Следует еще раз заметить, что любая система управления файлами не существует сама по себе — она разработана для работы в конкретной ОС. В качестве примера можно сказать, что всем известная файловая система FAT (file allocation table) имеет множество реализаций как система управления файлами. Так, система, получившая это название и разработанная для первых персональных компьютеров, называлась просто FAT (сейчас ее называют FАТ-12). Ее разрабатывали для работы с дискетами, и некоторое время она использовалась при работе с жесткими дисками. Потом ее усовершенствовали для работы с жесткими дисками большего объема, и эта новая реализация получила название FАТ-16. Это название файловой системы мы используем и по отношению к системе управления файлами самой MS-DOS. Реализацию же системы управления файлами для OS/2, которая использует основные принципы системы FAT, называют super-FAT; основное отличие — возможность поддерживать для каждого файла расширенные атрибуты. Есть версия системы управления файлами с принципами FAT и для Windows 95/98, для Windows NT и т. д. Другими словами, для работы с файлами, организованными в соответствии с некоторой файловой системой, для каждой ОС должна быть разработана соответствующая система управления файлами. Эта система управления файлами будет работать только в той ОС, для которой она и создана; но при этом она позволит работать с файлами, созданными с помощью системы управления файлами другой ОС, работающей по тем же основным принципам файловой системы.
Структура магнитного диска (разбиение дисков на разделы)
Для того чтобы можно было загрузить с магнитного диска собственно саму ОС, а уже с ее помощью и организовать работу той или иной системы управления
файлами, были приняты специальные системные соглашения о структуре диска. Расположение структуры данных, несущее информацию о логической организации диска и простейшую программу, с помощью которой можно находить и загружать программы загрузки гой или иной ОС, очевидно — это самый первый сектор магнитного диска.
Как известно, информация на магнитных дисках размещается и передается блоками. Каждый такой блок называется сектором (sector), сектора расположены па концентрических дорожках поверхности диска. Каждая дорожка (track) образуется при вращении магнитного диска под зафиксированной в некотором предопределенном положении головкой чтения/записи. Накопитель на жестких магнитных дисках (НЖМД) содержит один или более дисков (в современных распространенных НЖМД часто — два или три). Однако обычно под термином «жесткий диск» понимают весь пакет магнитных дисков [24).
Группы дорожек (треков) одного радиуса, расположенных на поверхностях магнитных дисков, образуют так называемые цилиндры (cylinder). Современные жесткие диски ищут иметь по несколько десятков тысяч цилиндров, в то время как на поверхности дискеты число дорожек (число цилиндров) ныне, как правило, составляет всего восемьдесят.
Каждый сектор состоит из поля данных и поля служебной информации, ограничивающей и идентифицирующей его. Размер сектора (точнее — емкость поля данных) устанавливается контроллером или драйвером. Пользовательский интерфейс DOS поддерживает единственный размер сектора — 512 байт. BIOS же непосредственно предоставляет возможности работы с секторами размером 128, 256, 512 или 1024 байт. Если управлять контроллером непосредственно, а не через программный интерфейс более высокого уровня (например, уровень DOS), то можно обрабатывать секторы и с другими размерами. Однако в большинстве современных ОС размер сектора выбирается равным 512 байт.
Физический адрес сектора на диске определяется с помощью трех «координат», то есть представляется триадой [c-h-s], где с — помер цилиндра (дорожки на по- верхпосги диска, cylinder), h — номер рабочей поверхности диска (магнитной головки, head), as — номер сектора на дорожке. Номер цилиндра с лежит в диапазоне 0..С-1, где С — количество цилиндров. Номер рабочей поверхности диска h принадлежит диапазону 0. .Н-1, где Н — число магнитных головок в накопителе. Номер сектора на дорожке s указывается в диапазоне 1..S, где S — количество секторов на дорожке. Например, триада [1 0-2] адресует сектор 2 па дорожке 0 (обычно верхняя рабочая поверхность) цилиндра 1. В дальнейшем мы тоже будем пользоваться именно этими обозначениями.
Напомним, что обмен информацией между ОЗУ и дисками физически осуществляется только секторами. Вся совокупность физических секторов па винчестере представляет его неформатированную емкость.
Жесткий диск может быть разбит на несколько разделов (partition), которые, в принципе затем могут использоваться либо одной ОС, либо различными ОС. Причем самым главным является го, что па каждом разделе может быть организована своя файловая система. Однако для организации даже одпой-едипствен- ной файловой системы необходимо определить, по крайней мерс, один раздел.
Разделы диска могут быть двух типов — primary (обычно этот термин переводят как первичный) и extended (расширенный). Максимальное число primary-разделов равно четырем. При этом на диске обязательно должен быть по крайней мере один primary-раздел. Если primary-разделов несколько, то только один из них может быть активным. Именно загрузчику, расположенному в активном разделе, передается управление при включении компьютера и загрузке операционной системы. Остальные primary-разделы в этом случае считаются' «невидимыми, скрытыми» (hidden).
Согласно спецификациям на одном жестком диске может быть только один сх- tended-раздел, который, в свою очередь, может быть разделен на большое количество подразделов — логических дисков (logical). В этом смысле термин «первичный» следует признать не совсем удачным переводом слова primary; можно это слово перевести и как «простейший, примитивный». В этом случае становится понятным и логичным термин extended.
Один из primary-разделов должен быть активным, именно с пего должна загружаться программа загрузки операционной системы, или так называемый .менеджер загрузки, назначение которого — загрузить программу загрузки ОС из какого-нибудь другого раздела, и уже с ее помощью загружать операционную систему. Поскольку до загрузки ОС система управления файлами работать не может, то следует использовать для указания упомянутых загрузчиков исключительно абсолютные адреса в формате [c-h-s].
По физическому адресу [0-0-1] на винчестере располагается главная загрузочная запись (master boot record, MBR), содержащая внесистемный загрузчик (non-sys- tem bootstrap — NS В), а также таблицу разделов (partition table, PT). Эта запись занимает ровно один сектор, опа размещается в памяти, начиная с адреса O:7C()()h, после чего управление передается коду, содержащемуся в этом самом нервом секторе магнитного диска. Таким образом, в самом первом (стартовом) секторе физического жесткого диска находится нс обычная запись boot record, как на дискете, a master boot record.
MBR является основным средством загрузки с жесткого диска, поддерживаемым BIOS. В MBR находятся три важных элемента:
программа начальной загрузки (пов-systcm bootstrap). Именно она запускается BIOS после успешной загрузки в память первого сектора с MBR. Она, очевидно, не превышает 512 байт и ее хватает только на то, чтобы загрузить
1 Невидимыми они являются 'только для тех систем, которые используют спецификации DOS.
следующую, чуть более сложную программу, обычно — стартовый сектор операционной системы — и передать ему управление;
таблица описания разделов диска (partition table). Располагается в MBR по смещению OxlBE и занимает 64 байта;
сигнатура MBR. Последние два байта MBR должны содержать число АЛ5511. По наличию этой (нгпатуры BIOS проверяет, что первый блок был загружен успению. Сигнатура эта выбрана не случайно. Ее успешная проверка позволяет установить, что все линии передачи данных могут передавать и нули, и единицы.
Таблица partition (able описывает размещение и характеристики имеющихся па винчестере разделов. Можно сказать, что эта таблица разделов — одна из наиболее важных структур данных на жестком диске. Если эта таблица повреждена, то не только нс будет загружаться операционная система (или одна из операционных систем, установленных на винчестере), но перестанут быть доступными и данные, расположенные на винчестере, особенно если жесткий диск был разбит на несколько разделов.
Смещение (Offset)
Размер (Sise) (байт)
Содержимое (Contents)
0
446
Программа анализа Partition Table и загрузки System Bootstrap с активного раздела жесткого диска
+1BEh
16
Partition 1 entry (Описатель раздела)
+1CEh
16
Partition 2 entry
+1DEh
16
Partition 3 entry
+1EEh
16
Pertit on 4 entry
+1FEh
2
Сигнатура (AA55h)
Рис. 4.4 Структура MBR
Упрощенно структура MBR представлена на рис. 4.4. Из нее видно, что в начале этого сектора располагается программа анализа таблицы разделов и чтения первого сектора из активного раздела диска. Сама таблица partition table располагается в конце MBR, и для описания каждого раздела в этой таблице отводится по 16 байтов. Первым байтом в элементе раздела идет флат' активности раздела boot indicator (0 — не активен, 128 (80Н) — активен). Он служит для определения, является ли раздел системным загрузочным и есть ли необходимость производить загрузку операционной системы с него при старте компьютера. Активным может быть только один раздел. За флагом активности раздела следует байт номера головки, с которой начинается раздел. За ним следует два байта, означающие соответс твенно номер сектора и номер цилиндра загрузочного сектора, где
располагается первый сектор загрузчика операционной системы. Затем следует кодовый идентификатор System ID (дчиной в один байт), указывающий на принадлежность данного раздела к той или иной операционной системе и установке на нем соответствующей файловой системы. В табл. 4.1 приведены некоторые (наиболее известные) идентификаторы.
Таблица 4.1, Сигнатуры (типы) разделов
System ID
Тип раздела
System ID
Тип раздела
00
Empty («пустой» раздел)
41
PPC PrcP Bool
01
FAT12
42
SFS
02
XENIX root
4D
QNX 4.x
03
XENIX usr
4E
QNX 4.x 2nd part
04
FAT16 (<32 Мбайт)
4F
QNX 4.x 3nd part
05
Extended
50
OnTrack DM
06
FAT16
51
OnTrack DM6 Aux
07
HPFS/NTFS
52
CP/M
08
A1X
53
OnTrack DM6
09
AIX bootable
54
OnTrack DM6
0А
OS/2 Bool Manager
55
EZ Drive
ОВ
Win95 FAT32
56
Golden Bou
ОС
Win95 FAT32 LBA
5C
Pr am Edisk
0Е
Win95 FAT16 LBA
61
Speed Stor
0F
Win95 Extended
64
Novell Netware
10
OPUS
65
Novel] Netware
И
Hidden FAT12
75
PC/1X
12
Compaq diagnost
80
Old Minix
14
Hidden FAT16 (<32 Мбайт)
82
Li mix swap
16
Hidden FA'l 16
83
Linux native
17
Hidden HPFS/NTFS
84
OS/2 hidden C:
18
AST Windows swap
85
Linux Extended
1В
Hidden Win95 Fat
86
NTFS volume set
1С
Hidden W: n95 Fat
A5
BSD/386
1Е
Hidden Win95 Fat
Л6
Open BSD
24
NEC DOS
Л7
Next Step
ЗС
Partition Magic
EB
Be OS
40
Venix 80286
За байтом кода операционной системы расположен байт номера головки конца раздела, за которым идут два байга — номер сектора и номер цилиндра последнего сектора данного раздела. Ниже представлен формат элемента таблицы разделов.
Таблица 4.2. Формат элемента таблицы разделов
Название записи элемента Partition Table
Длина, байт
Флаг активноеги раздела
1
Номер головки начала раздела
1
Номер сектора и помер цилиндре за1рузочного сектора раздела
2
Кодовый идентификатор операционной системы
1
Номер головки конца раздела
1
Номер сектора в цилиндра последнего сектора раздела
2
Младшее и старшее двухбайтовое слово относительного номера начального сектора
4
Младшее и старшее двухбайтовое слово размера раздела в секторах
4
Номера сектора и номер цилиндра секторов в разделах занимают по би 10 бит соответственно. Ниже представлен формат записи, содержащей номера сектора и цилиндра.
Биты номера цилиндра
Биты номера сектора
15
14
13
12
И
10
9
8
7
6
5
4
3
2
1
0
Как мы уже сказали, загрузчик non-system bootstrap служит для поиска с помощью partition table активного раздела, копирования в оперативную память компьютера загрузчика system bootstrap из выбранного раздела и передачи ему управления, что позволяет осуществить загрузку ОС.
Вслед за сектором MBR размещаются собственно сами разделы (рис. 4.5). В процессе начальной загрузки сектора MBR, содержащего таблицу partition table, работают Программные модули BIOS. Начальная загрузка считается выполненной корректно только в том случае, когда таблица разделов содержит допустимую информацию.
В MS-DOS в первичном разделе может быть сформирован только один логический диск, а в расширенном — любое их количество. Каждый логический диск «управляется» своим логическим приводом. Каждому логическому диску на винчестере соответствует своя (относительная) логическая нумерация. Физическая же адресация жесткого диска сквозная.
Первичный раздел DOS включает только системный логический диск без каких- либо дополни тельных информационных структур.
таблица разделовтаблица разделовРис. 4 5 Разбиение диска на разделыРис. 4 5 Разбиение диска на разделыРасширенный раздел DOS с логическими дисками D: и Е:Расширенный раздел DOS с логическими дисками D: и Е:Первичный раздел DOS (диск С:)Первичный раздел DOS (диск С:)
таблица разделов
таблица разделов
Рис. 4 5 Разбиение диска на разделы
Рис. 4 5 Разбиение диска на разделы
Расширенный раздел DOS с логическими дисками D: и Е:
Расширенный раздел DOS с логическими дисками D: и Е:
Первичный раздел DOS (диск С:)
Первичный раздел DOS (диск С:)
Расширенный раздел DOS содержит вторичную запись MBR (secondary MBR, SMBR), в состав которой вместо part ition table входит таблица логического диска (LDT, logical disk table), ей аналогичная. Таблица LDT описывает размещение и характеристики раздела, содержащего единственный логический диск, а также может специфицировать следующую запись SMBR. Следовательно, если в расширенном разделе DOS создано К логических дисков, то он содержит К экземпляров SMBR, связанных в список. Каждый элемент этого списка описывает соответствующий логический диск и ссылается (кроме последнего) на следующий элемент списка.
Утилиты, позволяющие разбить диск на разделы и тем самым сформировать как partition table, так и, возможно, списковую структуру из LDT (подобно тому, как это изображено на рис. 4.6), обычно называются FDISK (от form disk), хотя есть утилиты и с другим названием, умеющие выполнить разбиение диска. Напомним, что FDISK от MS-DOS позволяет создать только один primary-раздел и один extended, который, в свою очередь, предлагается разделить на несколько логических дисков. Аналогичные утилиты других ОС имеют существенно боль-
ше возможностей. Так, например, FDISK системы OS/2 позволяет создавать несколько primary-разделов, причем их можно выделять не только последовательно, начиная с первых цилиндров, но и с конца свободного дискового пространства. Это удобно, если нужно исключить из работы некоторый диапазон дискового пространства (например, из-за дефектов на поверхности магнитного диска). С помощью этой утилиты можно установить и менеджер загрузки (boot-менеджер). Прежде чем перейти к boot-менеджерам, рассмотрим еще раз процесс загрузки ОС. Процедура начальной загрузки (bootstrap loader) вызывается как программное прерывание (BIOS INT 19h). Эта процедура определяет первое готовое устройство из списка разрешенных и доступных (гибкий или жесткий диск, в современных компьютерах это могут быть также CD-ROM, привод ZIP-drive компании iomega, сетевой адаптер или другое устройство) и пытается загрузить с него в ОЗУ короткую главную программу-загрузчик. Для винчестеров — это загрузчик non-system bootstrap из MBR, и ему передается управление. Главный загрузчик определяет на диске активный раздел, загружает его собственный загрузчик (system bootstrap) и передает управление ему. И, наконец, этот загрузчик загружает необходимые файлы операционной системы и передает ей управление. Далее операционная система выполняет инициализацию подведомственных ей программных и аппаратных средств. Она добавляет новые сервисы, вызываемые, как правило, тоже через механизм программных прерываний, и расширяет (или заменяет) некоторые сервисы BIOS. Необходимо отмстить, что в современных мультипрограммных операционных системах большинство сервисов BIOS, изначально расположенных в ПЗУ, как правило, заменяются собственными драйверами, поскольку они должны работать в режиме прерываний, а не в режиме сканирования готовности.
Соответственно рассмотренному процессу загрузки, мы каждый раз при запуске компьютера будем попадать в одну и ту же ОС. Но иногда это пас не устраивает. Так называемые boot-менеджеры (менеджеры загрузки) предназначены для того, чтобы пользователь мог выбрать среди нескольких установленных на компьютере ОС нужную и передать управление загрузчику этой выбранной ОС. Существует большое количество таких менеджеров, хороший обзор таких программ приведен в работе [781. В ней рекомендуется в качестве менеджера загрузки использовать System commander. Однако этот менеджер должен располагаться в активном разделе с файловой системой FAT, что в наше время нельзя признать хорошим решением. На наш взгляд, одним из лучших долгое время был Boot manager компании IBM, входящий в состав утилит OS/2. Для его размещения создается отдельный primary-раздел, который, естественно, и является активным (он помечается как startable). Раздел для Boot-менеджера OS/2 занимает всего один цилиндр и может размещаться не только на нулевом (начальном) цилиндре, но и на последнем цилиндре. В этом разделе не организуется никакой файловой системы, поскольку обращение к менеджеру идет с использованием абсолютной адресации, а сам Boot manager представляет собой простейшую абсолютную двоичную программу. Установка Boot manager осуществляется из программы FDISK При этом появляется возможность указать, какие разделы могут быть загружаемыми (они помечаются как bootable), то есть в каких разделах на первом логическом секторе будут размещены программы загрузки ОС. Загружаемыми могут
быть как primary-, так и extended-разделы. При этом, естественно, имеется возможность указать как время на принятие решения, так и загрузку некоторой ОС «по умолчанию». Удобным является и то, что при разбиении диска на разделы можно вообще не создавать primary-разделов. Это особенно важно, если в ком пьютере установлено более одного дискового накопителя, либо если мы подготавливаем винчестер, который должен достаточно часто переноситься с одного компьютера на другой. Дело в том, что в большинстве ОС принято следующее правило именования логических дисков: первый логический диск обозначается литерой С:, второй — D: и т. д. При этом литеру С: получает активный primary- раздел. Однако, если к одному винчестеру в персональном компьютере подключить второй винчестер и он тоже имеет активный primary-раздел, то этот primary-раздел второго винчестера получит литеру D:, отодвигая логический диск D: первого винчестера на место диска Е: (и т. д. по цепочке). Если же второй (и последующие) винчестер не имеет primary-раздела с установленной па нем файловой системой, которую данная ОС знает, то «именование» логических дисков первого винчестера не нарушается. Естественно, что логические диски второго винчестера получают литеры логических дисков вслед за дисками первого винчестера. Boot manager OS/2, создавая только логические диски на винчестере, па самом деле создает и «пустой» primary-раздел, однако этот раздел не становится активным и не получает статус логического диска. К сожалению, все более популярная в наши дни ОС Windows 2000 теперь ие только снимает флаг активности с раздела, в котором размещен Boot manager (как это происходило при инсталляции любых ОС от компании Microsoft), но и физически уничтожает его двоичный код. Замена драйвера FASTFAT.SYS системы Windows 2000 на более раннюю версию (в бета-версии ОС Windows 2000 система не уничтожала код Boot manager) помогает лишь до установки Service pack. Поэтому рекомендовать этот менеджер загрузки уже нельзя. Из последних менеджеров загрузки, пожалуй, наиболее мощным является Boot star, но его нельзя рекомендовать неподготовленным пользователям.
Одной из самых известных и до сих пор достаточно часто используемых утилит, с помощью которой можно посмотреть и отредактировать таблицу разделов, а также выполнить и другие действия, связанные с изучением и исправлением данных как па дискете, так и на винчестере, является программа Disk editor из комплекта нортоповских утилит. Работа с ней достаточно подробно изложена в книге [9]. Следует, однако, заметить, что Disk editor, с одной стороны, очень мощное средство, и поэтому его следует использовать с большой осторожностью, а с другой стороны — эта утилита работает только в среде DOS. На сегодняшний день главным недостатком этой утилиты является ограничение на максимальный размер диска в 8 Гбайт.
Надо признать, что в последнее время появилось большое количество утилит, которые предоставляют возможность более наглядно представить разбиение диска на разделы, поскольку в них используется графический интерфейс. Эти про граммы успешно и корректно работают с наиболее распространенными типами разделов (разделы под FAT, FAT32, NTFS) Однако созданы они в основном для работы в среде Win32API, что часто ограничивает возможность их использования. Одной из самых известных и мощных программ для работы с разделами жесткого диска является Partition Magic фирмы Power Quest.
Файловая система FAT
Как мы уже отмечали, аббревиатура FAT (file allocation table) расшифровывается как «таблица размещения файлов». Этот термин Относится к линейной таб- личной структуре со сведениями о файлах — именами файлов, их атрибутами и другими данными, определяющими местонахождение файлов (или их фрагментов) в среде FAT. Элемент FAT определяет фактическую область диска, в которой хранится начало физического файла.
В файловой сис геме FAT логическое дисковое пространство любого логического диска делится па две области (рис. 4.6): системную область и область данных.
BR КеЧ-]ра11 |Secs| __BR КеЧ-]ра11 |Secs| __Fat2 RDirFat2 RDirКаталоги и файлыКаталоги и файлы
BR КеЧ-]ра11 |Secs| __
BR КеЧ-]ра11 |Secs| __
Fat2 RDir
Fat2 RDir
Каталоги и файлы
Каталоги и файлы
Область данныхОбласть данныхСистемная область
Область данных
Область данных
Рис. 4.6. Структура логического диска
Системная область логического диска создается и инициализируется при форма тировании, а впоследствии обновляется при манипулировании файловой структурой. Область данных логического диска содержит файлы и каталоги, подчиненные корневому. Она, в отличие от системной области, доступна через пользовательский интерфейс DOS. Системная область состоит из следующих компонентов, расположенных в логическом адресном пространстве подряд:
загрузочной записи (boot record, BR);
зарезервированных секторов (reserved sector, ResSecs);
таблицы размещения файлов (file allocation table, FAT);
корневого каталога (root directory, RDir).
Таблица размещения файлов
Таблица размещения файлов является очень важной информационной структурой. Можно сказать, что она представляет собой карту (образ) области данных, в которой описывается состояние каждого участка области данных. Область данных разбивают на так называемые кластеры. Кластер представляет собой один или несколько смежных секторов в логическом дисковом адресном пространстве (точнее — только в области данных). В таблице FAT кластеры, принадлежащие одному файлу (некорневому каталогу), связываются в цепочки. Для указания номера кластера в системе управления файлами FAT-16 используется 16-бито- вос слово, следовательно, можно иметь до 2|С - 65536 кластеров (с номерами от О до 65535). .
Кластер — это минимальная адресуемая единица дисковой памяти, выделяемая файлу (или некорневому каталогу). Файл или каталог занимает целое число
кластеров. Последний кластер при этом может быть задействован не полностью, что приведет к заметной потере дискового пространства при большом размере кластера. На дискетах кластер занимает один или два сектора, а па жестких дисках — в зависимости от обт ема раздела (см. табл. 4.3).
Таблица 4.3. Соотношения между размером раздела и размером кластеров в FAT16
Емкость раздела, Мбайт
Количество секторов в кластере
Размер кластеров, Кбайт
16- 127
4
2
128-255
8
4
256-511
16
8
512-1023
32
16
*1024 2047
64
32
Номер кластера всегда относится к области данных диска (пространству, зарезервированному для файлов и подкаталогов). Первый допустимый помер кластера всегда начинается с 2. Номера кластеров соответствуют элементам таблицы размещения файлов.
Логическое разбиение облаете; данных па кластеры как совокупности секторов взамен использования одиночных секторов имеет следующий смысл: прежде всего, уменьшается размер самой таблицы FAT; уменьшается возможная фрагментация файлов; ускоряется доступ к файлу, так как в несколько раз сокращается длина цепочек фрагментов дискового пространства, выделенных для пего.
Однако слишком большой размер кластера ведет к неэффективному использованию области данных, особенно в случае большого количества маленьких файлов. Как мы только что заметили, в среднем на каждый файл теряется около половины кластера. Из табл. 4.3 следует, что при размере кластера в 32 сектора (объем раздела — от 512 Мбайт до 1023 Мбайт), то есть 16 Кбайт, средняя величина потерь на файл составит 8 Кбайт, и при числе файлов в несколько тысяч потери могут составлять более 100 Мбайт. Поэтому в современных файловых системах (к ним, прежде всего, следует отнести HPFS NTFS, FAT32 и некоторые другие, поддерживаемые ОС семейства UNIX) размеры кластеров ограничиваю гея (обычно — от 512 байт до 4 Кбайт). В FAT32 проблема решается за счет того, что собственно сама FAT в этот файловой системе может содержать до 228 кластеров. Наконец, замет им, что системы управления файлами, созданные для
Windows 9х и Windows NT, могут работать с разделами размером до 4 Гбайт, па которых установлена система FAT, тогда как DOS, естественно, с такими разделами работать не сможет.
Достаточно наглядно идея файловой системы с использованием таблицы размещения файлов FAT проиллюстрирована рис. 4.7. Из этого рисунка видно, что файл с именем MYFILE TXT размещается, начиная с восьмого кластера. Всего файл MYFILE.TXT занимает 12 кластеров. Цепочка кластеров (chain) для нашего примера может быть записана следующим образом: 8, 9, ОА, ОВ, 15, 16. 17, 19, 1 А, IB, IC, 1D. Кластер с номером 18 помечен специальным кодом F7 как плохой (bad), он не может быть использован для размещения данных. При форматировании обычно проверяется поверхность магнитного диска, и тс секторы, при контрольном чтении с которых происходили ошибки, помечаются в FAT как плохие Кластер 1D помечен кодом FF как конечный (последний в цепочке) кластер, принадлежащий данному файлу. Свободные (незанятые) кластеры помечаются кодом 00; при выделении нового кластера для записи файла берется первый свободный кластер. Поскольку файлы на диске изменяются — удаляются, перемещаются, увеличиваются или уменьшаются, — то упомянутое правило выделения первого свободного кластера для новой порции данных приводит к фрагментации файлов, то есть данные одного файла могут располагаться не в смежных кластерах, а, порой, в очень удаленных друг от друга, образуя сложные цепочки. Естественно, что это приводит к существенному замедлению работы с файлами.
Рис 4.7. Основная концепция FAT
Так как FAT используется при доступе к диску очень интенсивно, она обычно загружается в ОЗУ (в буфера ввода/вынода или кэш) и остается там настолько [олго, насколько эго возможно
В связи с чрезвычайной важностью FAT она обычно хранится в двух идентичных экземплярах, второй из которых непосредственно следует за первым. Обновляются копии FAT одновременно. Используется же только первый экземпляр. Если он по каким-либо причинам окажется разрушенным, то произойдет обращение ко второму экземпляру. Так, например, утилита проверки и восста повлепия файловой структуры ScanDisk из ОС Windows 9х при обнаружении несоответствия первичной и резервной копии FAT предлагает восстановить главную таблицу, используя данные из копии.
1
Упомянутый корневой каталог отличается от обычного катало! а тем, что он, помимо размещения в фиксированном месте логического диска, еще имеет и фиксированное число элементов. Для каждого файла и каталога в файловой системе хранится информация в соответствии со структурой, изображенной в табл. 4.4.
Таблица 4.4. Элемент каталога
Размер поля данных, байт
Содержание поля
И
Имя файла или каталога
/
1
Атрибуты файла
1
Резервное поле
3
Время создания
2
Дата создания
2
Дата последнего доступа
2
Зарезервировано
2
Время последней модификации
2
Дата последней модификации
2
Помер начального кластера в FAT
4
Размер файла
Структура системы файлов является иерархической. Это иллюстрируется рис. 4.8, из которого видно, что элементом каталога может быть такой файл, который сам в свою очередь, является каталогом.
Рис. 4.8. Структура системы файлов
Структура загрузочной записи DOS
Сектор, содержащий загрузочную запись, является самым первым на логическом диске (на дискете — имеет физический адрес [О 0-Ц). Boot Record состоит, как мы уже знаем, из двух частей — disk parameter block (DPB) и system bootstrap (SB). Структура блока параметров диска (DPB) служит для идентификации физического и логического форматов логического диска, а загрузчик system bootstrap играет существенную роль в процессе загрузки DOS. Эта информационная структура приведена в табл. 4.5.
Таблица 4.5. Структура загрузочной записи Boot Record для FAT16
Смещение поля, байт
Длина поля, байт
Обозначение п^ля
Содержимое поля
ООН (0)
3
JUMP ЗЕН
Безусловный переход на начало SB
ОЗН (3)
8
Систем 11ы й идап ификатор
ОВН (И)
2
SectSize
Размер сектора, байт
ООН (13)
1
ClaslSizc
Число секторов в кластере
ОЕН (14)
2
Res Sees
Число зарезервированных секторов
ЮН (16)
1
FATcnt
Число копий FAT
11И (17)
2
Root Size
Максимальное число элементов Rdir
1311 (19)
2
TotSecs
Число секторов на логическом диске, если ст размер не превышает 32 Мбайт; иначе 0000Н
ЮН (21)
1
Media
Дескриптор носителя
16Н (22)
2
FATsize
Размер FAT, секторов
1811(24)
2
TrkSecs
Число секторов па дорожке
1АН (26)
2
HeadCnt
Число рабочих поверхностей
1СН (28)
4
HidnSecs
Число скрытых секторов
2011 (32)
4
Число секторов на логическом диске, если его размер превышает 32 Мбайт
24Н (36)
1
Тин логическою диска (ООН — гибкий, 80Н — жесткий)
25Н (37)
1
Пусто (резерв)
26Н (38)
1
Маркер с кодом 2911
2711 (39)
4
Серийный помер тома
2ВН (43)
И
Метка тома
Смещение поля, байт
Длина поля, байт
Обозначение поля
Содержимое поля
36Н (э4)
8
Имя файловой системы
ЗЕН (62)
System bootstrap
1FEH (510)
2
Сигнатура (слово АА55Н)
Первые два байта boot record занимает J MP — команда безусловного перехода в программу SB. Третий байт содержит код 90Н (NOP — нет операции) Далее располагается восьмибайтовый системный идентификатор, включающий информацию о фирме-разработчике и версии операционной системы. Затем следует DPB, а после него — SB.
Для работы с загрузочной записью удобно использовать широко известную утилиту D sk Editor из комплекта утилит Питера Нортона. Эта утилита снабжена встроенной системой подсказок и необходимой справочной информацией. Используя ее, можно сохранять, модифицировать и восстанавливать загрузочную запись, а также выполнять много других операций. Достаточно подробно работа с этой утилитой описана в книге [9].
Загрузочные записи других операционных систем отличаются от рассмотренной. Так, например, в загрузочном секторе для тома с FAT32 в блоке DPB содержатся дополнительные поля, а тс поля, что находятся в привычном для системы FAT16 месте, перенесены. Поэтому ОС, в которой имеется возможность работать с файловой системой FAT16, но нет системы управления файлами, понимающей спецификации FAT32, не может читать данные с томов, отформатированных под файловую систему FAT32. В загрузочном секторе для файловой системы FAT32 по-прежнему байты ООН ио ОАН содержат команду перехода и OEM ID, а в байтах ОВП по 59Н содержатся данные блока DPB. Отличие заключается именно в несколько другой структуре блока DBF; его содержимое приведено1 в табл 4.6
Таблица 4.6. Структура загрузочной записи boot record для FAT32
Смещение поля, байт
Длина поля, байт
Обозначение поля
Содержимое поля
ООН (0)
3
JUMP ЗЕН
Безусловный переход на начало SB
ОЗН (3)
8
Системный идентнфикатор
ОВП (И)
2
SectSize
Размер сектора, байт
0DH (13)
1
ClastSize
Число секторов в кластере
ОЕН (14)
2
ResSecs
Число зарезервированных секторов, для FAT32 равно 32
продолжение ^
1 Поскольку файловая система FAT32 нынче имеет очень широкое распространение, мы считаем, что информаця, приведенная в табл. 4.6, может быть полезна.
Таблица 4.6 (продолжение)
Смещение поля, байт
Длина поля, байт
Обозначение поля
Содержимое поля
ЮН (16)
1
FATcnt
Число копий FAT
ПН (17)
2
RootSize
0000Н
13Н (19)
2
TotSccs
ООООН
1511 (21)
1
Media
Дескриптор носителя
16Н (22)
2
FATsize
ООООН
18Н (24)
2
TrkSecs
Число секторов на дорожке
1АН (26)
2
I leadCnt
Число рабочих поверхностей
1СН (28)
4
HidnSecs
Число скрытых секторов (располагаются перед Загрузочным сектором). Используется при загрузке для вычисления абсолютного смещения корневого каталога и данных
20Н (32)
4
Число секторов па логическом диске
24 Н (36)
4
Число секторов в таблице FAT
28Н (37)
2
Расширенные флаги
2AI1 (38)
2
Версия файловой системы
2СН (39)
4
11омср кластера для первого кластера корн енот каталога
34Н (43)
2
Номер сектора с резервной копией загрузочного сектора
36Н (54)
12
Зарезервировано
Файловые системы VFAT и FAT32
Одной из важнейших характеристик исходной FAT было использование имен файлов формата «8.3», в котором 8 символов отводится на указание имени файла и 3 символа — для расширения имени. К стандартной FAT (имеется в виду прежде всего реализация FAT16) добавились еще две разновидности, используемые в широко распространенных операционных системах Microsoft (конкретно — в Windows 95 и Windows NT): VFAT (виртуальная FAT) и FAT32, используемая в одной из редакций ОС Windows 95 и Windows 98. Ныне эта файловая система (FAT32) поддерживается и такими ОС, как Windows Millennium Edition, и всеми ОС семейства Windows 2000. Имеются реализации систем управления файлами FAT32 для Windows NT и ОС Linux.
Файловая система VFAT впервые появилась в Windows for Workgroups 3.11 и была предназначена для выполнения файлового ввода/вывода в защищенном
режиме (см. раздел «Реальный и защищенный режимы работы процессора», глава 3). С выходом Windows 95 в VFAT добавилась поддержка длинных имен файлов (long file name, LFN). Тем не менее, VFAT сохраняет совместимость с исходным вариантом FAT; это означает, что наряду с длинными именами в ней поддерживаются имена формата «8.3», а также существует специальный механизм для преобразования имен «8.3» в длинные имена, и наоборот. Именно файловая система VFAT поддерживается исходными версиями Windows 95, Windows NT 4. При работе с VFAT крайне важно использовать файловые утилиты, поддерживающие VFAT вообще и длинные имена в частности. Дело в том, что более ранние файловые утилиты DOS запросто модифицируют то, что кажется им исходной структурой FAT. Эго может привести к потере или порче длинных имен из таблицы FAT, поддерживаемой VFAT (или FAT32). Следовательно, для томов VFAT необходимо пользоваться файловыми утилитами, которые понимают и сохраняют файловую структуру VFAT.
Б исходной версии Windows 95 основной файловой системой была 32-разрядная VFAT. VFAT может использовать 32-разрядные драйверы защищенного режима или 16-разрядные драйверы реального режима. При этом элементы FAT остаются 12- или 16-разрядиыми, поэтому на диске используется та же структура данных, что и в предыдущих реализациях FAT. VFAT обрабатывает все обращения к жесткому диску и использует 32-разрядный код для всех файловых операций с Дисковыми томами.
Основными недостатками файловых систем Г/ХТ и VFAT являются большие потери на кластеризацию при больших размерах логического диска и ограничения на сам размер логического диска. Эго привело к разработке новой реализации файловой системы с использованием той же идеи использования таблицы FAT. Поэтому в Microsoft Windows 95 OEM Service Release 2 (эта версия Windows 95 часто называется Windows 95 OSR2) на смену системе \ FAT пришла файловая система FAT32. FAT32 является полностью самостоятельной 32-разрядной файловой системой и содержит многочисленные усовершенствования и дополнения по сравнению с предыдущими реализациями FAT.
Принципиальное отличие заключается в том, что FA4'32 намного эффективнее расходует дисковое пространство. Прежде всего, система FAT32 использует кластеры меньшего размера по сравнению с предыдущими версиями, которые ограничивались 65 535 кластерами на том (соответственно, с увеличением размера диска приходилось увеличивать и размер кластеров). Следовательно, даже для дисков размером до 8 Гбайт FAT32 может использовать 4-килобайтные кластеры. В результате по сравнению с дисками FAT16 экономится значительное дисковое пространство (и среднем 10-15 %) [53].
FAT32 также может перемещать корневой каталог и использовать резервную копию FAT вместо стандартной. Расширенная загрузочная запись FAT32 позволяет создавать копии критических структур данных; это повышает устойчивость дисков F/XT32 к нарушениям структуры FAT по сравнению с предыдущими версиями. Корневой каталог в ГАТ32 представлен в виде обычной цепочки кластеров. Следовательно, корневой каталог может находиться в произвольном месте диска, что снимает действовавшее ранее ограничение на размер корневого каталога (512 элементов).
Windows 95 OSR2 и Windows 98 могут работать и с разделами VFAT, создании мн Windows NT. То, что говорилось ранее об использовании файловых утилит VFAT с томами VFAT, относится и к FAT32. Поскольку прежние утилиты FAT (для FAT32 в эту категорию входят обе файловые системы, FAT и VFAT) могут повредить пли уничтожить важную служебную информацию, для томов FAT32 нельзя пользоваться никакими файловыми утилитами, кроме утилит FAT32
Кроме повышения емкости FAT до величины в 4 Тбайт, файловая система FAT32 вносит ряд необходимых усовершенствований в структуру корневого каталога. Предыдущие реализации требовали, чтобы вся информация корневого каталога FAT находилась в одном дисковом кластере. При этом корневой каталог мог содержать не более 512 файлов. Необходимость представлять длинные имена и обеспечить совместимость с прежними версиями FAT привела разработчиков компании Microsoft к компромиссному решению: для представления длинного имени они стали использовать элементы каталога, в том числе и корневого. Рассмотрим способ представления в VFAT длинного имени файла (рис. 4.9)
О 1 2 ТГ4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31О 1 2 ТГ4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
О 1 2 ТГ4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
О 1 2 ТГ4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Элемент каталога для длинного имени файла (FAT32, FAT16 и FAT32)
I S
о.
0) 5 zr
Символы 1—5 имени файла в Unicode
3 £
<
о го m о
X
Ф i го со
5
| £
Символы 6—11 имени файла в Unicode
11
Символы 12—13 имени файла в Unicode
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Рис. 4.9. Элементы каталогов для FAT, VFAT и FAT32
Первые одиннадцать байтов элемента каталога DOS используются для хранения имени файла. Каждое такое имя разделяется на две части: в первых восьми байтах хранятся символы собственно имени, а в последних трех — символы так называемого расширения, с помощью которого реализуются механизмы предопределенных типов. Были введены соответствующие системные соглашения, и файлы определенного типа необходимо (желательно) именовать с оговоренным расширением. Например, исполняемые файлы с расширением СОМ определяют исполняемую двоичную программу с простейшей односегментной структурой. Более сложные программы имеют расширение EXE. Определены расширения для большого количества типов файлов, и эти расширения используются для ассоциированного запуска обрабатывающих файлы программ.
Если имя файла состоит менее чем из восьми символов, то в элементе каталога оно дополняется символами пробела, чтобы полностью заполнить все восемь байтов соответствующего поля. Аналогично и расширение может содержать от нуля до трех символов. Остальные (незаполненные) позиции в элементе каталога, определяющем расширение имени файла, заполняются символами пробела. Поскольку при работе с именем файла учитываются все одиннадцать свободных мест, то необходимость в отображении точки, которая обычно вводится между именем файла и его расширением, отпадает. В элементе каталога она просто подразумевается
В двенадцатом байте элемента каталога хранятся атрибуты файла. Шесть из восьми указанных разрядов используются DOS. К атрибутам DOS относятся следующие:
атрибут «архивный» (A archive). Показывает, что файл был открыт программой таким образом, чтобы у нес была возможность изменить содержимое этого файла. DOS устанавливает этот разряд атрибута в состояние ON (включено) при открытии файла. Программы резервного копирования нередко устанавливают его в OFF (выключено) при выполнении резервного копирования файла. Если применяется подобная методика, то в следующую создаваемую по порядку резервную копию будут добавлены только файлы с данным разрядом, установленным в состояние ON;
атрибут каталога (D — directory). Показывает, что данный элемент каталога указывает на подкаталог, а не на файл;
атрибут тома (V — volume). Применяется только к одному элементу каталога в корневом каталоге. В нем, собственно, и хранится имя дискового тома. Этот атрибут также применяется в случае длинных имен файлов, о чем можно будет узнать из следующего раздела;
атрибут «системный» (S — system). Показывает, что файл является частью операционной системы или специально отмечен подобным образом приклад-
ной программой, что иногда делается в качестве составной части метода защиты от копирования;
атрибут «скрытый» (Н — hidden). Сюда относятся, в частности, файлы с установленным в состояние ON атрибутом «системный» (S), которые не отображаются в обычном списке, выводимом по команде DIR;
атрибут «только для чтения» (R — read only). Показывает, что данный файл не подлежит изменению. Разумеется, поскольку это лишь разряд байта, хранящегося на диске, то любая программа может изменить этот разряд, после чего DOS свободно разрешила бы изменение данного файла. Этот атрибут в основном используется для примитивной защиты от пользовательских ошибок, то есть он позволяет избежать неумышленного удаления или изменения ключевых файлов.
Следует отметить, что наличие файла, помеченного одним пли более из указанных выше атрибугов, может иметь вполне определенный смысл. Например, большинство файлов, отмечаемых в качестве системных, отмечаются также признаками скрытых файлов и «только для чтения».
На дисках FAT12 или FAT16 следующие за именем десять байтов не используются. Обыкновенно они заполняются нулями и считаются резервными значениями. А на диске с файловой системой FAT32 эти 10 байт содержат самую разную информацию о файле. При этом байт, отмеченный как «зарезервировано для NT», представляет собой, как подразумевает его название, поле, не используемое в DOS или Windows 9х, но применяемое в Windows NT
Из соображений совместимости поля, которые встречаются в элементах каталога для коротких имен формата FAT12 и FAT16. находятся на тех же местах и в элементах каталога для коротких имен формата FAT32. Остальные поля, которые встречаются только в элементах каталога для коротких имен формата FAT32, соответствуют зарезервированной области длиной в десять байт в элементах каталога для коротких имен форматов FAT12 и FAT16.
Как видно из рис. 4.9, для длинного имени файла используется несколько элементов каталога. Таким образом, появление длинных имен фактически привело к дальнейшему уменьшению количества файлов, которые могут находиться в корневом каталоге. Поскольку длинное имя может содержать до 256 символов, всего один файл с полным длинным именем занимает до 25 элементов FAT (1 для имени 8.3 и еще 24 для самого длинного имени). Количество элементов корневого каталога VFAT уменьшается до 21. Очевидно, что это не самое изящное решение, поэтому компания Microsoft советует избегать длинных имен в корневых каталогах FAT при отсутствии FAT32, у которой количество элементов каталога соответственно просто увеличено. Помните и о том, что длина полной файловой спецификации, включающей путь и имя файла (длинное или в формате 8.3), тоже ограничивается 260 символами. FAT32 успешно справляется с проблемой длинных имен в корневом каталоге, но проблема с ограничением длины полной файловой спецификации остается. По этой причине Microsoft рекомендует ограничивать длинные имена 75-80 символами, чтобы оставить достаточно места для пути (180-185 символов).
Файловая система HPFS
Сокращение HPFS расшифровывается как «High Performance File System» — высокопроизводительная файловая система. HPFS впервые появилась в OS/2 1.2 и LAN Manager. HPFS была разработана совместными усилиями лучших специалистов компании IBM и Microsoft на основе опыта IBM по созданию файловых систем MVS, VM/CMS и виртуального метода доступа'. Архитектура HPFS начала создаваться как файловая система, которая сможет использовать преимущества многозадачного режима и обеспечит в будущем более эффективную и надежную работу с файлами на дисках большого объема.
HPFS была первой файловой системой для ПК, в которой была реализована поддержка длинных имен [96]. HPFS, как FAT и многие другие файловые системы, обладает структурой каталогов, но в ней также предусмотрены автоматическая сортировка каталогов и специальные расширенные атрибуты, упрощающие реализацию безопасности файлового уровня и создание множественных имен. HPFS поддерживает те же самые атрибуты, что и файловая система FAT, по историческим причинам, но также поддерживает и новую форму file-associated, то есть информацию, называемую расширенными атрибутами (EAs). Каждый ЕА концептуально подобен переменной окружения. Но самым главным отличием все же являются базовые принципы хранения информации о местоположении файлов.
Принципы размещения файлов на диске, положенные в основу HPFS, увеличивают как производительность файловой системы, так и ее надежность и отказоустойчивость. Для достижения этих целей предложено несколько способов: размещение каталогов в середине дискового пространства, использование методов бинарных сбалансированных деревьев для ускорения поиска информации о файле, рассредоточение информации о местоположении записей файлов по всему диску, при том что записи каждого конкретного файла размещаются (по возможности) в смежных секторах и поблизости от данных об их местоположении. Действительно, система HPFS стремится, прежде всего, к тому, чтобы расположить файл в смежных кластерах, или, если такой возможности нет, разместить его на диске таким образом, чтобы экстенты (фрагменты) файла физически были как можно ближе друг к другу. Такой подход существенно уменьшает время позиционирования головок заииси/чтения жесткого диска и время ожидания (rotational latency) — задержка между установкой головки чтения/записи на нужную дорожку
диска и началом чтения данных с диска). Можно сказать, что файловая система HPFS имеет, по сравнению с FAT, следующие основные преимущества
высокая производительность;
надежность,
работа с расширенными атрибутами, что позволяет управлять доступом к файлам и каталогам;
эффективное использование дискового пространства.
Все эти преимущества обусловлены структурой диска HPFS. Рассмотрим ее более подробно (рис. 4.10).
В начале диска расположено несколько управляющих блоков. Все остальное дисковое пространство в HPFS разбито на части («полосы», «ленты» из смежных секторов, в оригинале — band). Каждая такая группа данных занимает на диске пространство в 8 Мбайт и имеет свою собственную битовую карту распределения секторов. Эти битовые карты показывают, какие секторы данной полосы заняты, а какие — свободны. Каждому сектору ленты данных соответствует один бит в ее битовой карте. Если бит имеет значение 1, то соответствующий сектор занят, а если 0 — свободен.
Битовые карты двух полос располагаются на диске рядом, так же располагаются и сами полосы. То есть последовательность полос и карт выглядит следующим образом: битовая карта, битовая карта, лента с данными, лепта с данными, битовая карта, битовая карта и т. д. Такое расположение «лент» позволяет непрерывно разместить на жестком диске файл размером до 16 Мбайт и в то же время не удалять от самих файлов информацию об их местонахождении. Это иллюстрируется рис. 4.10.
Очевидно, что если бы па весь диск была только одна битовая карта, как это сделано в FAT, то для работы с ней приходилось бы перемещать головки чте- ния/записи в среднем через половину диска. Именно для того, чтобы избежать этих потерь, в HPFS и разбит диск на «полосы» Получается своего рода распределенная структура данных об используемых и свободных блоках.
Дисковое пространство в HPFS выделяется не кластерами, как в FAT, а блоками. В современной реализации размер блока взят равным одному сектору, но в принципе он мог бы быть и иного размера. По сути дела, блок — это и есть кла-
стер. Размещение файлом в таких небольших блоках позволяет более эффективно использовать пространство диска, так как непроизводительные потери свободного места составляют в среднем всего 256 байт на каждый файл. Вспомните, что чем больше размер кластера, тем больше места па диске расходуется напрасно. Например, кластер па отформатированном под FAT диске объемом от 512 до 1024 Мбайт имеет размер 16 Кбайт. Следовательно, непродуктивные потери свободного пространства на таком разделе в среднем составляют 8 Кбайт (8192 байт) на один файл, в го время как на разделе HPFS эти потери всегда будут составлять всего 256 байт на файл. Таким образом, на каждый файл экономится почти 8 Кбайт.
На рис. 4.10 показано, что помимо «лент» с записями файлов и битовых карт в том^ с HPFS имеются еще три информационные структуры. Это так называемый загрузочный блок (boot block), дополнительный блок (super block) и запасной (резервный) блок (spare block). Загрузочный блок (hoot block) располагается в секторах с 0 по 15; он содержит имя тома, его серийный номер, блок параметров BIOS и программу начальной загрузки. Программа начальной загрузки на ходит файл OS2LDR. считывает его в память и передает управление этой программе загрузки ОС, которая, в свою очередь, загружает с диска в память ядро OS/2 — OS2KRNL. И уже OS2KRNL с помощью сведений из файла CONFIG.SYS загружает в намять все остальные необходимые программные модули и блоки дан ных.
В блоке (super block) содержится указатель на список битовых карт (bitmap block list). В этом списке перечислены все блоки на диске, в которых расположены битовые карты, используемые для обнаружения свободных секторов. Также в дополнительном блоке хранится указатель па список дефектных блоков (had block list), указатель па группу каталогов (directory band), указатель на файловый узел (F-nodc) корневого каталога, а также дата последней проверки раздела программой CHKDSK В списке дефектных блоков перечислены все поврежденные секторы (блоки) диска. Когда система обнаруживает поврежденный блок, он вносится в этот список и для хранения информации больше не используется. Кроме этого, в структуре super block содержится информация о размере «полосы». Напомним, что в текущей реализации HPFS размер «полосы» взят равным 8 Мбайт. Блок super block размещается в секторе с номером 16 логического диска, па котором установлена файловая система IIPFS.
Резервный блок (spare block) содержит указатель па карту аварийного замещения (hotfix map или hotfix-areas), указатель на список свободных запасных блоков (directory emergency free block list), используемых для операций на почти переполненном диске, и ряд системных флагов и дескрипторов. Этот блок размещается в 17 секторе диска Резервный блок обеспечивает высокую отказоустой-
чивость файловой системы HPFS и позволяет восстанавливать поврежденные данные на диске.
Файлы и каталоги в HPFS базируются на фундаментальном объекте, называемом F-Node. Эта структура характерна для HPFS и аналога в файловой системе ГАТ не имеет. Каждый файл и каталог диска имеет свой файловый узел F-Node. Каждый объект F-Node занимает один сектор и всегда располагается поблизости от своего файла или каталога (обычно — непосредственно перед (файлом или каталогом). Объект F-Node содержит длину и первые 15 символов имени файла, специальную служебную информацию, статистику по доступу к файлу, расширенные атрибуты файла и список прав доступа’ (или только часть этого списка, если он очень большой), ассоциативную нифорк^цию о расположении и подчинении файла и т. д. Структура распределения в F-node может принимать несколько форм в зависимости от размера каталога или файлов. HPFS просматривает файл как совокупность одного или более секторов. Из прикладной программы это не видно; файл появляется как непрерывный поток байтов. Если расширенные атрибуты слишком велики для файлового узла, то в него записывается указатель на них.
Сокращенное имя файла (в формате 8.3) используется, когда файл с длинным именем копируется или перемещается на диск с системой FAT, не допускающей подобных имен. Сокращенное имя образуется из первых 8 символов оригинального имени файла, точки и первых трех символов расширения имени, если расширение имеется. Если в имени файла присутствует несколько точек, что не противоречит правилам именования файлов в HPFS, то для расширения сокращенного имени используются три символа после самой последней из этих точек. Так как IIPFS при размещении файла на диске стреми 1ся ’избежать его (фрагментации. то структура информации, содержащаяся в файловом узле, достаточно проста. Если файл непрерывен, то его размещение па диске описывается двумя 32 битными числами. Первое число представляет собой указатель на первый блок файла, а второе — длину экстента, то есть число следующих друг за другом блоков, принадлежащих файлу. Если файл фрагментирован, то размещение его экстентов описывается в файловом узле дополнительными парами 32-битных чисел. Фрагментация происходит, когда на диске нет непрерывного свободного участка, достаточно большого, чтобы разместить файл целиком. В этом случае файл приходится разбивать на несколько экстентов и располагать их на диске раздельно. Файловая система HPFS старается разместить экстенты фрагментированного (файла как можно ближе друг к другу, чтобы сократить время позиционирования головок чтения/записи жесткого диска. Для этого HPFS использует статистику, а также старается условно резервировать хотя бы 4 килобайта места в конце файлов, которые растут. Еще один способ уменьшения фрагментирования файлов — это расположопие (файлов, растущих навстречу друг другу, или файлов, открытых разными тредами или процессами в разных полосах диска.
В файловом узле можно разместить информацию максимум о восьми экстентах файла. Если файл имеет больше экстентов, то в его файловый узел записывается указатель на блок размещения (allocation block), который может содержать до 40 указателей на экстенты или, по аналогии с блоком дерева каталогов, на другие блоки размещения. Таким образом, двухуровневая структура блоков размещения может хранить информацию о 480 секторах, что позволяет работать с файлами размером до 7,68 Гбайт. На практике размер файла не может превышать 2 Гбайт, по это обусловлено текущей реализацией интерфейса прикладного программирования [96].
«Полоса», находящаяся в центре диска, используется для храпения каталогов. Эта полоса называется directory band. Как и все остальные «полосы», она имеет размер 8 Мбайт. Однако, если она будет полностью заполнена, HPFS начинает располагать каталоги файлов в других полосах. Расположение этой информационной структуры в середине диска значительно сокращает среднее время позиционирования головок чтепия/заннси. Действительно, для перемещения головок чтепия/записи из произвольного места диска в его центр требуется в два раза меньше времени, чем для перемещения к краю диска, где находится корневой каталог в случае файловой системы ЕА'Г. Уже только одно это обеспечивает более высокую производительность файловой системы HPFS по сравнению с FAT. Аналогичное замечание справедливо и для NTFS, которая тоже распил га- ет свой master file table в начале дискового пространства, а не в его середине.
Однако существенно больший (по сравнению с размещением Directory Band в середине логического диска) вклад в производительность HPFS дает использование метода сбалансированных двоичных деревьев для хранения и поиска информации о мес гопахожденин файлов. Как известно, в файловой системе РА'Г каталог имеет линейную структуру, специальным образом не упорядоченную, поэтому при поиске файла требуется последовательно просматривать его с самого начала. В HPFS структура катален а представляет собой сбалансированное дерево с записями, расположенными в алфавитном порядке (рис. 4.11). Каждая запись, входящая в состав В-Tree дерева, содержит атрибуты файла, указатель на соответствующий файловый узел, информацию о времени и дате создания файла, времени и дате последнего обновления и обращения, длине данных, содержащих расширенные атрибуты, счетчик обращений к файлу, длине имени файла и само имя, и другую информацию.
Рис. 4.11. Сбалансированное двоичное дерево
Файловая система HPFS при поиске файла в каталоге просматривает только необходимые ветви двоичного дерева (В-Tree). Такой метод во много раз эффективнее, чем последовательное чтение всех записей в каталоге, что имеет место в системе FAT. Для того чтобы найти искомый файл в каталоге (точнее, указатель на его информационную структуру F-node), организованном на принципах сбалансированных двоичных деревьев, большинство записей вообще читать не нужно. В результате для поиска информации о файле необходимо выполнить существенно меньшее количество операций чтения диска.
Действительно, если, например, каталог содержит 4096 файлов, то файловая система FAT потребует чтения в среднем 64 секторов для поиска нужного файла внутри такого каталога, в то время как HPFS осуществит чтение всего только 2-4 секторов (в среднем) и найдет искомый файл. Несложные расчеты позволяют увидеть явные преимущества HPFS над FAT. Так, например, при использовании 40 входов на блок блоки каталога дерева с двумя уровнями могут содержать 1640 входов, а каталога дерева с тремя уровнями — уже 65 640 входов. Другими словами, некоторый файл может быть найден в типичном каталоге из 65 640 файлов максимум за три обращения. Это намного лучше файловой системы FAT, где для нахождения файла нужно прочитать в худшем случае более 4000 секторов.
Размер каждого из блоков, в терминах которых выделяются каталоги в текущей реализации HPFS, равен 2 Кбайт. Размер записи, описывающей файл, зависит от размера имени файла. Если имя занимает 13 байтов (для формата 8.3), то блок из 2 Кбайт вмещает до 40 описателей файлов. Блоки связаны друг с другом посредством списковой структуры (как и описатели экстентов) для облегчения последовательного обхода.
При переименовании файлов может возникнуть так называемая перебалансировка дерева. Создание файла, переименование или стирание может приводить к каскадированию блоков каталогов. Фактически, переименование может потерпеть неудачу из-за недостатка дискового пространства, даже если файл непосредственно в размерах не увеличился. Во избежание этого «бедствия» HPFS поддерживает небольшой пул свободных блоков, которые могут использоваться при «аварии». Эта операция может потребовать выделения дополнительных блоков на заполненном диске. Указатель на этот пул свободных блоков сохраняется в SpareBlock.
Важное значение для повышения скорости работы с файлами имеет уменьшение их фрагментации. В HPFS считается, что файл является фрагментированным, если он содержит больше одного экстента. Снижение фрагментации файлов сокращает время позиционирования и время ожидания за счет уменьшения количества перемещений головок, необходимого для доступа к данным файла. Алгоритмы работы файловой системы HPFS работают таким образом, чтобы по возможности размещать файлы в последовательных смежных секторах диска, что обеспечивает максимально быстрый доступ к данным впоследствии. В системе FAT, наоборот, запись следующей порции данных в первый же свободный кластер неизбежно приводит к фрагментации файлов. HPFS тоже, если это предоставляется возможным, записывает данные в смежные секторы диска (но не в
первый попавшийся). Это позволяет несколько снизить число перемещений головок чтения/записи от дорожки к дорожке. При этом, когда данные дописываются в существующий файл, HPFS сразу же резервирует как минимум 4 Кбайт непрерывного пространства на диске Если же часть этого пространства не потребовалась, то после закрытия файла она высвобождается для дальнейшего использования. Файловая система HPFS равномерно размещает непрерывные файлы ио всему диску для того, чтобы впоследствии без фрагментации обеспечить их возможное увеличение. Если же файл не может быть увеличен без нарушения его непрерывности, HPFS опять-таки резервирует 4 Кбайт смежных блоков как можно ближе к основной части файла с целью сократить время позиционирования головок чтения/записи и время ожидания соответствующего сектора.
Очевидно, что степень фрагментации файлов па диске зависит как от числа файлов, расположенных на нем, их размеров и размеров самого диска, так и от характера и интенсивности самих дисковых операций. Незначительная фрагментация файлов практически нс сказывается па быстродействии операций с файлами. Файлы, состоящие из двух-трех экстентов, практически не снижают производительность HPFS, так как эта файловая система следит за тем, чтобы области данных, принадлежащие одному и тому же файлу, располагались как можно ближе друг к другу. Файл из трех экстентов имеет только два нарушения непрерывности, и, следовательно, для его чтения потребуется всего лишь два небольших перемещения головки диска. Программы (утилиты) дефрагментации, имеющиеся для этой файловой системы, по умолчанию считают наличие двух-трех экстентов у файла нормой. Например, программа HPFSOPT из набора утилит Gamma- Tech по умолчанию не дефрагментирует файлы, состоящие из трех и менее экс- гептов, а файлы, которые имеют большее количестве экстентов, приводятся к 2 или 3 экстентам, если это возможно (файлы объемом в несколько десятков ме- габайт всегда будут фрагментированы, ибо максимально возможный размер экстента, как вы помните, равен 8 Мбайт). Надо сказать, что практика показывает, что в среднем надиске имеется нс более 2 процен гон файлов, имеющих три и более экстентов [96] Даже общее количество фрагментированных файлов, как правило, не превышает 3 процентов. Такая ничтожная фрагментация оказывает пренебрежимо малое влияние на общую производительность системы
Теперь кратко рассмотрим вопрос надежности храпения данных в HPFS. Любая файловая система должна обладать средствами исправления ошибок, возникающих при записи информации на диск. Система HPFS для этого использует механизм аварийного замещения (hotfix).
Если файловая система HPFS сталкивается с проблемой в процессе записи данных па диск, она выводит па экран соответствующее сообщение об ошибке. Затем HPFS сохраняет информацию, которая должна была быть записана в дефектный сектор, в одном из запасных секторов, заранее зарезервированных на этот случай. Список свободных запасных блоков хранится в резервном блоке HPFS. При обнаружении ошибки во время записи данных в нормальный блок HPFS выбирает один из свободных запасных блоков и сохраняет эти данные в нем. Затем файловая система обновляет карту аварийного замещения в резервном блоке. Эта карта представляет собой просто нары двойных слов, каждое из которых яв-
ляется 32-битным номером сектора. Первый номер указывает на дефектный сектор, а второй — на тот сектор среди имеющихся запасных секторов, который был выбран для его замены. После замены дефектного сектора запасным карта аварийного замещения записывается на диск, и на экране появляется всплывающее окно, информирующее пользователя о произошедшей ошибке записи на диск. Каждый раз, когда система выполняет запись или чтение сектора диска, она просматривает карту' аварийного замещения и подменяет все номера дефектных секторов номерами запасных секторов с соответствующими данными. Следует заметить, что это преобразование номеров существенно не влияет на производительность системы, так как оно выполняется только при физическом обращении к диску, но не при чтении данных из дискового кэша. Очистка карты аварийного замещения автоматически выполняется программой CHKDSK при проверке диска HPFS Для каждого замещенного блока (сектора) программа CHKDSK выделяет новый сектор в наиболее подходящем для файла (которому принадлежат данное) месте жесткого диска Затем программа перемещает данные из запасного блока в этот сектор и обновляет информацию о положении файла, что может потребовать новой балансировки дерева блоков размещения. После этого CHKDSK вносит поврежденный сектор в список дефектных блоков, который хранится в дополнительном блоке HPFS, и возвращает освобожденный сектор в список свободных запасных секторов резервного блока. Затем удаляет запись из карты аварийного замещения и записывает отредактированную карту на диск.
Все основные файловые объекты в HPFS, в том числе файловые узлы, блоки размещения и блоки каталогов, имеют уникальные 32-битпые идентификаторы и указатели на свои родительские и дочерние блоки. Файловые узлы, кроме того, содержат сокращенное имя своего файла или каталога. Избыточность и взаимосвязь файловых структур HPFS позволяют программе CHKDSK полностью восстанавливать файловую структуру диска, последовательно анализируя все файловые узлы, блоки размещения и блоки каталогов. Руководствуясь собранной информацией, CHKDSK реконструирует файлы и каталоги, а затем заново создает битовые карты свободных секторов диска. Запуск программы CHKDSK следует осуществлять с соответствующими ключами. Так, например, один из вариантов работы этой программы позволяет найти и восстановить удаленные файлы.
HPFS относится к так называемым монтируемым файловым системам Это означает, что она не встроена в операционную систему, а добавляется к ней при необходимости. Файловая система HPFS устанавливается оператором IFS* в файле CONFIG.SYS. Этот оператор всегда помещается в первой строке данного конфигурационного файла. В приводимом далее примере оператор IFS устанавливает файловую систему HPFS с кэшем в 2 Мбайт длиной записи кэша в 8 Кбайт и автоматической процедурой проверки дисков С и D:
IFS-E:\0S2\HPFS.IFS /CACHE:2048 /CRECL:4 /AUTOCHECK;CD
Для запуска программы управления процессом кэширования следует прописать в файле CONFIG.SYS еще одну строку
1 IFS (installable file system) — устанавливаемая, монтируемая система управления файлами.
RUN-E:\0S2\CACHE.EXE /Lazy:0n /Bufferldle:2000 /Diskldle:4000 /MaxAge:8000 /DirtyMax:256 /ReadAhead:On
В этой строке включается режим отложенной («ленивой») записи, устанавливаются параметры работы этого режима, а также включается режим упреждающего чтения данных, что в целом позволяет существенно сократить количество обращений к диску и ощутимо повысить быстродействие файловой системы. Так, ключ Lazy с параметром On включает «ленивую запись», а с параметром Off — выключает. Ключ Buffer Idle определяет время в миллисекундах, в течение которого буфер кэша должен оставаться в неактивном состоянии, чтобы стало возможным осуществить запись данных из кэша на диск. По умолчанию (то есть если не прописывать данный ключ явным образом) это время равно 500 мс. Ключ Diskldle задает время (в миллисекундах), в течение которого диск должен оставаться в неактивном состоянии, чтобы стало возможным осуществить запись данных из кэша па диск. По умолчанию это время равно 1 с. Этот параметр позволяет избежать записи из кэша на диск во время выполнения других операций с диском.
Ключ МахАде задает время (тоже в миллисекундах), по истечении которого часто сохраняемые в кэше данные наконец помечаются как «устаревшие» и при переполнении кэша могут быть замещены новыми. По умолчанию это время равно 5 с. Остальные подробности установки параметров и возможные значения ключей имеются в HELP-файлах, устанавливаемых вместе с операционной системой OS/2 Warp.
Наконец, следует сказать и еще об одной системе управления файлами — речь идет о реализации HPFS для работы на серверах, функционирующих под управлением OS/2. Это система управления файлами, получившая название HPFS386.IFS. Ес принципиальное отличие от системы HPFS.IFS заключается в том, что HPFS386.IFS позволяет (посредством более полного использования технологии расширенных атрибутов) организовать ограничения на доступ к файлам и каталогами с помощью соответствующих списков доступа — ACL (access control list). Эта технология, как известно, пспотьзуется в файловой системе NTFS Кроме этого, в системе HPFS386.IFS, в отличие от HPFS.IFS, нет ограничений на объем памяти, выделяемой для кэширования файловых записей. Иными словами, при наличии достаточного объема оперативной памяти объем файлового кэша может быть в несколько десятков мегабайт, в то время как для обычной HPFS.IFS этот объем не может превышать 2 Мбайт, что по сегодняшним меркам безусловно мало. Наконец, при установке режимов работы файлового кэша HPFS386.IFS есть возможность явным образом указать алгоритм кэширования. Наиболее эффективным алгоритмом можно считать так называемый «элеваторный», когда при записи данных из кэша на диск они предварительно упорядочиваются таким образом, чтобы минимизировать время, отводимое на позиционирование головок ч тения/записи. Головки чтения/записи при этом перемещаются от внешних цилиндров к внутренним и по ходу своего движения осуществляют запись и чтение данных в соответствии со специальным образом упорядочиваемым списком запросов па дисковые операции.
Приведем пример записи строк в конфигурационном файле CONFIG.SYS, которые устанавливают систему HPFS386.IFS и определяют параметры раооты ее подсистемы кэширования:
IFS-E:\IBM386FSXHPFS386.IFS /AUTOCHECK:EGH
RUN-EAIBM386FS\CACHE386.EXE /Lauz.y:On /Bufferldle 4000 /МахАде:20000
Эти записи следует понимать следующим образом. При запуске операционной системы в случае обнаружения флага, означающего, что не все файлы были закрыты в процессе предыдущей работы, система управления файлами HPFS386.IFS сначала запустит программу проверки целостности файловой системы для томов Е:, G: и Н: Для кэширования файлов при работе этой системы управления файлами устанавливается режим отложенной записи со временем жизни буферов до 20 с. Остальные параметры, в частности алгоритм обслуживания запросов, устанавливаются в файле HPFS386.INI, который в данном случае располагается в директории E:\IBM386FS.
Опишем кратко некоторые наиболее интересные параметры, управляющие работой кэша в этой системе управления файлами. Прежде всего отметим, чго файл HPFS386.INI разбит на несколько секций. В настоящий момент рассмотрим секцию [ULTIMEDIA],
[ULTIMEDIA]
QUEUESORT-{FIF0|ELEVATOR|DEFAULT|CURRENT}
QUEUEMETHOD-{PR10RITY|NOPRIORITY|DEFAULT|CURRENT)
QUEUEDEPTH-{1. .255|DEFAULT|CURRENT)
Параметр QUEUESORT задает способ ведения очереди запросов к диску. Он может принимать значения FIFO, ELEVATOR, DEFAULT и CURRENT. Если задано значение FIFO, то каждый новый запрос просто добавляется в конец очереди, то есть запросы выполняются в том порядке, в котором они поступают в систему. Однако можно упорядочить некоторое количество запросов по возрастанию номеров дорожек. Если задано значение ELEVATOR, то включается режим поддержки упорядоченной очереди запросов. При этом запросы начинают обрабатываться ио алгоритму ELEVATOR (он же C-SCAN или «режим плавающей головки» [24, 28]). Напомним, этот алгоритм подразумевает, что головка ч гения/записи сканирует диск в выбранном направлении (например, в направлении возрастания номеров дорожек), останавливаясь для выполнения запросов, находящихся на пути следования. Когда она доходит до последнего запроса, головка чтения/записи переносится на начальную дорожку и процесс обслуживания запросов продолжается.
Если для параметра QUEUESORT задано значение DEFAULT, то выбирается алгоритм по умолчанию. Сейчас это ELEVATOR. Если задано значение CURRENT, то остается в силе тот алгоритм, который был выбран DASD Manager при инициализации.
Параметр QUEUEMETHOD определяет, должны ли учитываться приоритеты запросов при построении очереди. Он может принимать значения PRIORITY, NOPRIORITY, DEFAULT и CURRENT. Если задано значение NOPRIORITY, то все запросы включаются в общую очередь, а их приоритеты игнорируются. Если задано значение PRIORITY, то модуль DASD Manager будет поддерживать несколько очередей запросов, по одной на каждый приоритет. Когда DASD Manager передает запросы па исполнение драйверу диска, он сначала выбирает запросы из самой приоритетной очереди, потом из менее приоритетной и г. д. Приоритеты назначает HPFS386, а распределены они следующим образом.
High:
Shutdown или экстренная запись из-за сбоя питания.
Страничный обмен.
Обычные запросы от foreground сессии.
Обычные запросы от background сессии. (Приоритеты 3 и 4 равны, если в файле CONFIG.SYS задан параметр RIORITY_DISK_IO=NO.)
Read-ahead и низкоприоритетные запросы страничного обмена (страничная предвыборка).
Lazy-Write и прочие запросы, не требующие немедленной реакции.
Low:
Предвыборка.
Если для параметра QUEUEMETHOD задано значение DEFAULT, то выбирается метод по умолчанию. Сейчас это PRIORITY. Если задано значение CURRENT, то остается в силе тот метод, который был выбран DASD Manager при инициализации.
Параметр QUEUEDEPTH задает глубину просмотра очереди при выборке запросов. Он может принимать значения из диапазона (1...255), а также DEFAULT и CURRENT. Если в качестве значения параметра QUEUEDEPTH задано число, то оно определяет количество запросов, которые должны находиться в очереди дискового адаптера одновременно. Например, для SCSI-адаптеров имеет смысл поддерживать такую длину очереди, ири которой они смогут загрузить все запросы в свои аппаратные структуры (tagged queue или mailbox). Если очередь запросов к адаптеру будет слишком короткой, то аппаратура будет работать с неполной загрузкой, а если опа будет слишком длинной — драйвер SCSI-адаптера будет перегружен «лишними» запросами. Поэтому разумным значением для QUEUEDEPTH будет число, немного превышающее длину аппаратной очереди команд адаптера. Если для параметра QUEUEDEPTH задано значение DEFAULT, то глубина просмотра очереди определяется автоматически па основании значения которое рекомендовано драйвером дискового адаптера. Если задано значение CURRENT, то глубина просмотра очереди не изменяется. В текущей реализации CURRENT эквивалентно DEFAULT.
Итак, текущие умолчания для HPFS386 имеют вид.
QUEUESORT-FIFO
QUEUEMETHOD-DEFAULT
0UEUEDEPTH-2
А текущие умолчания для DASD Manager таковы:
QUEUESORT-ELEVATOR
QUEUEMETHO -PRIORITY
01НОЕРТН-<эависит от адаптера диска>
Умоччания DASD Manager можно менять с помощью параметра /QF:
BASEDEV-OS2DASD. DMD /QF {1|2|3)
где 1 - QUEUESORT - FIFO; 2 - QUEUEMETHOD - NOPRIORITY 3 - QUEUESORT - FIFO и QUEUEMETHOD = NOPRIORITY.
Наконец, добавим еще несколько слов об устанавливаемых файловых системах (installable file systems — IFS), представляющих собой специальные «драйверы» для доступа к разделам, отформатированным под другую файловую систему. Это очень удобный и мощный механизм добавления в ОС новых файловых систем и замены одной системы управления файлами на другую. Сегодня, например, для OS/2 уже реально существуют IPS модули для файловой системы УРАТ (РАТ с поддержкой длинных имен), FAT32, Ext2FS (файловая система Linux), NTFS (правда, пока только для чтения). Для работы с данными на CD-ROM имеется CDFS.IFS. Есть и FTP.IFS, позволяющая монтировать ftp-архивы как локальные диски. Механизм устанавливаемых файловых систем был перенесен и в систему Windows NT.
Файловая система NTFS (New Technology File System)
В название файловой системы NTFS входят слова «New Technology», то есть «новая технология». Действительно, NTFS содержит ряд значительных усовершенствований и изменений, существенно отличающих ее от других файловых систем. С точки зрения пользователей, файлы по-прежнему хранятся в каталогах (часто называемых «папками» или фоядерами1 в среде Windows). Однако в NTFS, в отличие от FAT, работа на дисках большого объема происходит намного эффективнее; имеются средства для ограничения в доступе к файлам и каталогам, введены механизмы, существенно повышающие надежность файловой системы, сняты многие ограничения на максимальное количество дисковых секторов и/или кластеров.
Основные возможности файловой системы NTFS
При проектировании системы NTFS особое внимание было уделено следующим характеристикам [53]:
надежность Высокопроизводительные компьютеры и системы совместного пользования (серверы) должны обладать повышенной надежностью, которая является ключевым элементом структуры и поведения NTFS. Одним из спо-
собов увеличения надежности является введение механизма транзакций, при котором осуществляется журналирование' файловых операций;
расширенная функциональность. NTFS проектировалась с учетом возможного расширения. В ней были воплощены многие дополнительные возможности — усовершенствованная отказоустойчивость, эмуляция других файловых систем, мощная модель безопасности, параллельная обработка потоков данных и создание файловых атрибутов, определяемых пользователем;
поддержка POSIX. Поскольку правительство США требовало, чтобы все закупаемые им системы хотя бы в минимальной степени соответствовали стандарту POSIX, такая возможность была предусмотрена и в NTFS. К числу базовых средств файловой системы POSIX относится необязательное использование имен файлов с учетом регистра, хранение времени последнего обращения к файлу и механизм так называемых «жестких ссылок» — альтернативных имен, позволяющих ссылаться на один и тот же файл по двум и более именам;
гибкость. Модель распределения дискового пространства в NTFS отличается чрезвычайной гибкостью. Размер кластера может изменяться от 512 байт до 64 Кбайт; он представляет собой число, кратное внутреннему кванту распределения дискового пространства. NTFS также поддерживает длинные имена файлов, набор символов Unicode и альтерна гивные имена формата 8.3 для совместимости с FAT.
NTFS превосходно справляется с обработкой больших массивов данных и достаточно хорошо проявляет себя при работе с томами объемом от 300-400 Мбайт и выше. Максимально возможные размеры тома (и размеры файла) составляют 16 Эбайт. Количество файлов в корневом и некорневом каталогах не ограничено. Поскольку в основу структуры каталогов NTFS заложена эффективная структура данных, называемая «бинарным деревом» (см. раздел «Файловая сис-
тема IIPFS»), время поиска файлов в NTFS (в отличие от систем на базе FAT) не связано линейной зависимостью с их количеством.
Система NTFS также обладает определенными средствами самовосстановления. N'l'FS поддерживает различные механизмы проверки целостности системы, включая ведение журналов транзакций, позволяющих воспроизвес ти файловые операции записи по специальному системному журналу.
Файловая система N ITS поддерживает объектную модель безопасности NT и рассматривает все тома, каталоги и файлы как самостоятельные объекты. NTFS обеспечивает безопасность па уровне файлов; то означает, что права доступа к томам, каталогам и файлам могут зависеть от учетной записи пользователя и тех групп, к которым он принадлежит. Каждый раз, когда пользователь обращается к объекту файловой системы, его права доступа проверяются по списку разрешений данного объекта. Если пользователь обладает достаточным уровнем прав, его запрос удовлетворяется; в противном случае запрос отклоняется. Эта модель безопасности применяется как при локальной регистрации пользователей на компьютерах с NT. так и при удаленных сетевых запросах.
1(аконец, помимо огромных размеров томов и файлов, система NTFS также об- ладает встроенными средствами сжатия. которые можно применять к отдельным файлам, целым каталогам и даже томам (и впоследствии отменять или назначать их по своему усмотрению).
Структура тома с файловой системой NTFS
Рассмотрим теперь структуру файловой системы NTFS. Наиболее полно опа описана в книге |36]. Мы же здесь коснемся только основных моментов.
Одним из основных понятий, используемых при работе с NTFS, является понятие тома (volume). Возможно также создание отказоустойчивого тома, занимающего несколько разделов, то есть использование RAID-технологии. Как и многие другие системы, NTFS делит все полезное дисковое пространство тома па кластеры — блоки данных, адресуемые как единицы данных NTFS поддерживает размеры кластеров от 512 байт до 64 Кбайт; стандартом же считается кластер размером 2 или 4 Кбайт.
Все дисковое пространство в NTFS делится на две неравные части (рис. 4.12). Первые 12% диска отводятся под так называемую MFT-зону — пространство, которое может занимать, увеличиваясь в размере, главный служебный метафайл MFT. Запись каких-либо данных в эту область невозможна. MFT-зона всегда держится пустой — это делается для того, чтобы самый главный, служебный файл (MFT) по возможности не фрагментировался при своем росте. Остальные 88% тома представляют собой обычное пространство для хранения файлов.
5
го X О СО
Зона для размещения файлов и каталогов
2 >s 8
с 3
(D
X S сх ф с
а
Зона для размещения файлов и каталогов
Рис. 4 12. Структура тома NTFS
MFT (master file table, общая таблица файлов) представляет собой централизованный каталог всех остальных файлов диска, в том числе п себя самого. MFT поделен на записи фиксированного размера в 1 Кбайт1, и каждая запись соответствует какому-либо файлу (в общем смысле этого слова). Первые 16 файлов носят служебный характер и недоступны операционной системе — они называются метафайлами, причем самый первый метафайл — сам MFT. Эти первые 16 элементов MFT — единственная часть диска, имеющая строго фиксированное положение. Копия этих же 16 записей хранится в середине тома для надежности, поскольку они очень важны. Остальные части MFT-файла могут располагаться, как и любой другой файл, в произвольных местах диска — вос^ановить его положение можно с помощью его самого, «зацепившись» за самую основу — за первый элемент MFT.
Упомянутые первые 16 файлов NTFS (метафайлы) носят служебный характер; каждый из них отвечает за какой-либо аспект работы системы. Метафайлы находятся в корневом каталоге NTFS-тома. Все они начинаются с символа имени «$», хотя получить какую-либо информацию о них стандартными средствами сложно. В табл. 4.7 приведены основные известные метафайлы и их назначение. Таким образом, можно узнать, например, сколько операционная ст гема тратит на каталогизацию тома, посмотрев размер файла $MFT
Таблица 4.7. Метафайлы NTFS
Имя метафайла
Назначение метафайла
$MFT
Сам Master File Table
SMFTmirr
Копия первых 16 записей MFT, размещенная посередине тома
$LogFile
Файл поддержки операций журналирования
$ Volume
С |ужсбная информация — метка тома, версия файловой системы и т. д.
SAttrDcf
Список стандартных атрибутов файлов на томе
продолжение ^
1 Размер файловых записей MFT для тома — минимум 1 Кбайт и максимум 4 Кбайт — определяется во время форматирования тома.
Таблица 4 7 (продолжение)
Имя метафайла
Назначение метафайла
$
Корневой каталог
SBitmap
Карта свободного места тома
SBoot
Загрузочный сектор (если раздел загрузочный)
SQnota
Файл, в котором записаны права пользователей па использование дискового пространства (этот файл начал работать лишь в Windows 2000 с системой NTFS 5.0)
SUpcase
Файл — таблица соответствия заглавных и прописных букв в именах файлов. В NTFS имена файлов записываются в Unicode (что составляет 65 тысяч различных символов) и искать большие и малые эквиваленты в данном случае — петриттльпая задача
Итак, все файлы тома упоминаются в MFT В этой структуре хранится вся информация о файлах, за исключением собственно данных. Имя файла, размер, положение на диске отдельных фрагментов и т. д. — все это хранится в соответствующей записи. Если для информации нс хватает одной записи MFT, то используется несколько записей, причем не обязательно идущих подряд. Файлы могут иметь не очень большой размер. Тогда применяется довольно удачное решение: данные файла хранятся прямо в MFT, в оставшемся от основных данных месте в пределах одной записи MFT. Файлы, занимающие сотни байт, обычно не имеют своего «физического» воплощения в основной файловой области — все данные такого файла хранятся в одном месте, в МГТ.
Файл в томе с NTFS идентифицируется так называемой файловой ссылкой (File Reference), которая представляется как 64-разрядное число. Файловая ссылка состоит из номера файла, который соответствует позиции его файловой записи в MFT, и номера последовательности. Последний увеличивается всякий раз, когда данная позиция в MFT используется повторно, что позволяет файловой системе NTFS выполнять внутренние проверки целостности.
Каждый файл в NTFS представлен с помощью потоков (streams), то есть у него нет как таковых «просто данных», а есть «потоки». Для правильного понимания потока достаточно указать, что один из потоков и носит привычный нам смысл — данные файла. Но большинство атрибутов файла — это тоже потоки. Таким образом, получается, что базовая сущность у файла только одна — номер в MFT, а все остальное, включая и его потоки, — опционально. Данный подход может эффективно использоваться — например, файлу можно «прилепить» еще один поток, записав в него любые данные. В Windows 2000 таким образом записана информация об авторе и содержании файла (одна из закладок в свойствах фай- ла, просматриваемых, например, из проводника). Интересно, что эти дополнительные потоки не видны стандартными средствами работы с файлами: наблюдаемый размер файла — это лишь размер основного потока, который содержит традиционные данные. Можно, к примеру, иметь файл нулевой длины, при стирании которого освободится 1 Гбайт свободного места — просто потому, что ка-
кая-нибудь хитрая программа или технология «прилепила» к нему дополнительный поток (альтернативные данные) такого большого размера. Но па самом деле в настоящее время потоки практически не используются так чго опасаться подобных ситуаций не следует, хотя гипотетически они возможны. Просто необходимо иметь в виду, что файл в NTFS — это более глубокое понятие, чем можно себе представить, просматривая каталоги диска.
Стандартные же атрибуты для файлов и каталогов в томе NTFS имеют фиксированные имена и коды типа, они перечислены в табл. 4.8.
Таблица 4.8. Атрибуты файлов в системе NTFS
Системный атрибут
Описание атрибута
Стандартная информация о файле
Традиционные атрибуты Read Only, Hidden, Archive, System, отметки времени, включая время создания или последней модификации, число каталогов, ссылающихся па файл
Список атрибутов
Список атрибутов, из которых состоит файл, и файловая ссылка на файловую запись и MFT, в которой расположен каждый из атрибутов. Последний используется, если файлу необходимо более одной записи в MFT
Имя файла
Имя файла в символах Unicode. Файл может иметь несколько атрибутов — имен файла, подобно тому как это имеет место в UNIX-системах. Это случается, когда имеется связь POSIX с данным файлом или если у файла сеть автоматически сгенерированное имя в формате 8.3
Дескриптор защиты
Структура данных защиты (ACL), предохраняющая файл от несанкционированного доступа. Атрибут «деофинтор защиты» определяет, кто владелец файла в кто имеет дот туп к нему
Данные
Собственно данные файла, его содержимое. В NTFS у файла ио умолчанию есть один безымянный атрибут данных, и он может иметь дополнительные именованные атрибуты данных. У каталога пет атрибута данных ио умолчанию, но oil может иметь необязательные именованные атрибуты данных
Корень индекса, размещение индекса, битовая карта (только для каталогов)
Атрибуты, используемые для индексов имен файлов в больших каталогах
Расширенные атрибуты HPFS
Атрибуты, используемые для реализации расширенных атрибутов IIPFS для подсистемы OS/2 и OS/2-клиспгов файл-серверов Windows NT
Атрибуты файла в записях MFT расположены в порядке возрастания числовых значений кодов типа, причем некоторые типы атрибутов могут встречаться в записи более одного раза: например, если у файла есть несколько атрибутов данных или несколько имен. Обязательными для каждого файла в томе NTFS являются атрибут стандартной информации, атрибут имени файла, атрибут дескриптора защиты и атрибут данных. Остальные атрибуты могут встречаться при необходимости.
Имя файла в NTFS, в отличие от файловых систем FAT и HPFS, может содержать любые символы, включая полный набор национальных алфавитов, так как данные представлены в Unicode — 16 битном представлении, которое дает 65 535 разных символов. Максимальная длина имени файла в NTFS — 255 символов.
Большой вклад в эффективность файловой системы вносит организация каталога. Каталог; в NTFS представляет собой специальный файл, храпящий ссылки на другие файлы и каталоги, создавая иерархическое строение данных на диске. Файл каталога поделен па блоки, каждый из которых содержит имя файла, базовые атрибуты и ссылку на элемент MFT, который уже предоставляет полную информацию об элементе каталога. Главный каталог диска — корневой — ничем не отличается от обычных каталогов, кроме специальной ссылки на него из начала метафайла MFT.
Внутренняя структура каталога представляет собой бинарное дерево, подобно тому как это организовано в HPFS. Кстати, при создании файловой системы NTFS разработчики решили использовать максимально возможное количество эффективных решений из HPFS. К сожалению, не было взято па вооружение разбиение всего дискового пространства па зоны, в каждой из которых хранилась бы информация об имеющихся свободных кластерах. В результате отказа от этого подхода и введения механизма транзакций скорость работы файловой системы NTFS существенно ниже скорости работы системы HPFS.
Итак, как нам теперь известно, бинарное дерево каталога располагает имена файлов таким образом, чтобы поиск файла осуществлялся с помощью получения двухзначных ответов на вопросы о положении файла. Бинарное дерево способно дать ответ па вопрос: в какой группе, относительно данного элемента, находится искомое имя — выше или ниже? Мы начинаем с такого вопроса к среднему элементу, и каждый ответ сужает зову поиска в среднем в два раза. Если представить, что файлы отсортированы по алфавиту, то ответ на вопрос осуществляется очевидным способом — сравнением начальных букв. Область поиска, суженная в два раза, начинает исследоваться аналогичным образом, начиная опять же со среднего элемента.
Заметим, что добавлять файл в каталог в виде дерева не намного труднее, чем в линейный каталог системы FAT. Это сопоставимые по времени операции. Для того чтобы добавить новый файл в каталог, нужно сначала убедиться, что файла с таким именем там еще нет. Поэтому в системе FAT с линейной организацией записей каталога у нас появляются трудности не только с поиском файла. И это с лихвой компенсирует саму простоту добавления файла в каталог.
Возможности файловой системы NTFS по ограничению доступа к файлам и каталогам
Благодаря наличию механизма расширенных атрибутов, в NTFS именно с их помощью реализованы ограничения в доступе к файлам и каталогам. Эти дополнительные атрибуты, использованные для ограничения в доступе к файловым объектам, назвали атрибутами безопасное! и. При каждом обращении к такому объекту сравнивается специальный список дискреционных прав доступа, приписанный ему, со специальным системным идентификатором, несущим информацию о том, от имени кого осуществляется текущий запрос к файлу пли каталогу. Если имеется в списке необходимое разрешение, то действие выполняется, в противном случае система сообщает об отказе.
Фантовая система NTFS имеет следующие разрешения, которые могут быть приписаны любому файлу и/пли каталогу, и которые называются индивидуальными разрешениями. Это разрешения Read {прочитать), Write {записать), eXecute {выполнить), Delete {удалить), Change Permissions {изме нить разрешения), и Take Ownership {стать владельцем). Соответствующие этим разрешениям действия можно выполнять, только если для данного пользователя плп для группы (к которой он принадлежит) имеется одиоимшпюс разрешение. Другими словами, если для некоторого файла указано, что псе могут его читать и исполнять, то только эти действия и можно с ним сделать, если при этом не указано, что для какой-нибудь /фугой группы (или отдельного пользователя) имеются другие разрешения. Комбинации этих индивидуальных разрешений и определяют те действия, которые могут быть выполнены с файлом или каталогом.
Изначально всему диску, а значит, и файлам, которые па нем создаются, при своены все индивидуальные разрешения для группы Everyone (все). Эго означает, что любой пользователь, имея полный набор индивидуальных разрешений на файлы и каталоги, может изменять их по своему усмотрению, т.е. ограничивать других пользователей в правах доступа к тому или иному объек- гу. Если изменить разрешения на каталог, то новые файлы, создаваемые в нем, будут получать и соответствующие разрешения: они будут наследовать разрешения своего родительского каталога.
Имеются так называемые стандартные разрешения, которые, ио замыслу разработчиков, следует использовать для указания наиболее распространенных комбинаций индивидуальных разрешений. Поясним эти стандартные разрешения с помощью таблицы, расположенной на следующей странице.
Помимо этих стандартных разрешений можно использовать специальные. Они определяются как явная комбинация индивидуальных разрешений.
Фактические разрешения для пользователя, которые он будет иметь на файл или каталог, определяются как сумма разрешений, которые он получает как член нескольких групп. У этого общего правила есть исключение. Разрешение No Access (нет доступа) имеет приоритет над остальными. Оно запрещает любой доступ к файлу или каталогу, даже если пользователю, как члену другой группы, дано необходимое разрешение. Можно сказать, что это стандартное разрешение означает не отсутствие разрешений, а наложение явного запрета, и что оно отме-
няет для пользователя или группы все разрешения, установленные в остальных строках дискреционного списка прав доступа.
Стандартные разрешения NTFS
Соотве ствующие им комбинации индивидуальных разрешений NTFS
Для каталогов
Для файлов
No access (нет доступа)
Нет никаких разрешений
Her никаких разрешений
List (просмотр)
Read, eXecute
Нет никаких разрешений
Read (чтение)
Read, eXecute
Read. eXecute
Add (добавление)
Write, eXecute
Нет никаких разрешений
Add & Read (чтение и добавление)
Read, Write, eXecute
Read, eXecute
Change (изменение)
Read, Write, eXecute, Delete
Read, Write, eXecute, Delete
Full Control (полный доступ)
Roc разрешения
Все разрешения
Рассмотрим теперь, что происходит с правами па защищенные файлы в NTFS при их перемещении. Каталоги обычно обладают теми же разрешениями, что и находящиеся в них файлы и папки, хотя у каждого файла могут быть свои разрешения. Разрешения, которые имеются у файла, имеют приоритет над разрешениями, которые установлены па каталог, в котором находится этот файл. Например, если вы создаете каталог внутри другого каталога, для которого администраторы обладают правом полного доступа, а пользователи — правом чтения, то новый каталог унаследует эти права. То же относится и к файлам, копируемым из другого каталога или перемещаемым из другого раздела NTFS.
Если каталог или файл перемещается в другой каталог того же раздела NTFS, то атрибуты безопасности не наследуются от нового каталога. Дело в том, что при перемещении файлов в границах одного раздела NTFS изменяется только указатель местонахождения объекта, а все остальные атрибуты (включая атрибуты безопасности) остаются без изменений.
Три следующих важных правила помогут определить состояние прав доступа при перемещении или копировании объектов NTFS:
при перемещении файлов в границах раздела NTFS сохраняются исходные права доступа.
при выполнении других операций (создании или копировании файлов, а также их перемещении между разделами NTFS) наследуются права доступа родительского каталога.
при перемещении файлов из раздела NTFS в раздел FAT все права NTFS теряются.
Основные отличия FAT и NTFS
Если говорить о накладных расходах на хранение служебной информации, FAT отличается от NTFS большей компактностью и меньшей сложностью. В большинстве томов FAT на хранение таблицы размещения, содержащей информацию обо всех файлах тома, расходуется менее 1 Мбайт. Столь низкие накладные расходы позволяют форматировать в FAT жесткие диски малого объема и флоппи-диски. В NTFS служебные данные занимают больше места, чем в FAT. Так, каждый элемент каталога занимает 2 Кбайт. Однако это имеет и свои преимущества, гак как содержимое файлов объемом 1500 байт и менее может полностью храниться в элемыпе каталога.
Система NTFS не может использоваться для форматирования флоппи-дисков. Не стоит пользоваться ею для форматирования разделов объемом менее 50-100 Мбайт. Относительно высокие накладные расходы приводят к тому, чго для малых разделов служебные данные могут занимать до 25 % объема носителя. Корпорация Microsoft рекомендует использовать FAT для разделов объемом 256 Мбайт и менее, a NTFS — для разделов объемом 400 Мбайт и более.
Следующий критерий сравнения — размер файлов. Разделы FAT имеют объем до 2 Гбайт, VFAT — до 4 Гбайт и FAT32 — до 4 Тбайт. Тем не менее из-за особенностей своего внутреннего строения разделы FAT лучше всего работают для разделов объемом 200 Мбайт и менее. Разделы NTFS могут достигать 16 Эбайт, однако в настоящее время из-за аппаратных и других системных причин размер файлов ограничивается 2 Тбай г.
Разделы FAT могут использоваться практически во всех операционных системах. За редкими исключениями, с разделами NTFS можно работать напрямую только из Windows NT, хотя и имеются для ряда ОС соответствующие реализации систем управления файлами для чтения файлов из томов NTFS. Так, например, утилита (драйвер) NTFSDOS позволяет читать данные NTFS па компьютере, загруженном в режиме MS-DOS. Однако полноценных реализаций для работы с NTFS вне системы Windows NT пока нет.
Разделы FAT не обеспечивают локальной безопасности. С другой стороны, разделы NTFS обеспечивают локальную безопасность как файлов, так и каталогов. Для разделов FAT могут устанавливаться общие права, связанные с общим доступом к каталогам в сети Однако такая защита не помешает пользователю с локальным входом получить доступ к файлам своего компьютера. В отношении безопасности NTFS оказывается предпочтительным вариантом. Разделы NTFS могут запрещать или ограничивать доступ как удаленных, так и локальных пользователей. Следовательно, к защищенным файлам смогут обратиться лишь те пользователи, которым были предоставлены соответствующие права.
Напомним, что Windows NT содержит специальную утилиту CONVERT.EXE которая преобразует тома FAT в эквивалентные тома NTFS, однако для обратного
преобразования (из NTFS в FAT) подобных утилит не существует. Чтобы выполнить такое обратное преобразование, необходимо создать раздел FAT, скопировать в него файлы из раздела NTFS и затем удалить оригиналы. Важно при этом не забывать и о том, что при копировании файлов из NTFS в FAT теряются все атрибуты безопасности NTFS (напомним, что в FAT не предусмотрены средства для определения и последующего храпения этих атрибутов).
В последнее время появилось еще одно очень важное обстоятельство, связанное с тем, что объемы дисковых механизмов намного превысили максимально допустимый размер, приемлемый для FAT, — 8,4 Гбайт. Этот предел объясняется максимально возможными значениями в адресе сектора, для которого, как мы уже знаем, отводится всего 3 байта. Поэтому в подавляющем большинстве случаев при работе в среде Windows-систем используют либо FAT32, либо NTFS Последняя, безусловно, лучше, по она не поддерживается в широко распространенных ОС Windows 98 и ныне все более часто встречающейся Windows Millen nium Edition.
Контрольные вопросы и задачи
Вопросы для проверки
Почему создание подсистемы ввода/вывода считается одной из самых сложных областей проектирования операционных систем?
Почему операции ввода/вывода в ОС объявляются привилегированными?
Перечислите основные задачи, возлагаемые па супервизор ввода/вывода.
В каких случаях устройство ввода/вывода называется инициативным?
Какие режимы управления вводом/выводом вы знаете? Опишите каждый из них.
Что означает термин «spooling» и что означает термин «swapping»?
Чем обеспечивается независимость пользовательских программ от устройств ввода/вывода, подключенных к компьютеру?
Что такое синхронный и асинхронный ввод/вывод?
Расскажите о кэшировании операций ввода/вывода при работе с накопителями на магнитных дисках.
Что такое «файловая система»? Что обеспечивает использование тон или иной файловой системы? Какие файловые системы, используемые в ОС для ПК, вы знаете?
Опишите структуру магнитного диска (разбиение дисков на разделы). Сколько (и каких) разделов может быть па магнитном диске?
Как в общем случае осуществляется загрузка ОС после включения компьютера? Что такое системный и внесистемный загрузчики? Где они располагаются?
Объясните общие принципы файловой системы FAT. Что такое кластер, от чего зависит его размер?
Сравните файловые системы FAT16 и FAT32. В чем заключаются их достоинства п недостатки?
За счет чего в файловой системе HPFS обеспечена высокая производительность?
Что означает «журналирование» файловых операций? Что это даст?
Расскажите о правилах, которые определяют состояние прав доступа при перемещении или копировании объектов, если используется NTFS.
Проведите сравнительный анализ файловых систем HPFS и NTFS; перечислите достоинства и недостатки каждой.
Задания
Используя ПК с установленной на нем ОС Windows N'F 4.0. исследуйте ограничения доступа к файлам и каталогам, которыми обладает файловая сис тема N ITS. Расскажиге о результатах.
Используя ПК с установленной на нем ОС Windows N'F 4.0, проверьте правила, которые определяют состояние прав доступа при перемещении или копировании обьектов при использовании NTFS. Расскажите о результатах.
Используя специально выделенный для этих целей ИК, изучите структуру диска и освойте работу' с программой Disk Editor. Выполните следующее задание:
О Включите компьютер. Во время выполнения программы самотестирования войдите в BIOS и установите возможность загрузки с дискеты.
О Загрузите операционную систему, расположенную на магнитном диске. Покажите преподавателю, что она работает.
О Загрузитесь с системной дискеты (на пей должна быть MS-DOS 6.2 или какая-нибудь другая версия DOS).
О Запустите программу Disk Editor из комплекта утилит от Питера Нортона. С помощью встроенных подсказок изучите основные возможности этой утилиты.
О Посмотрите структуру диска. Сохраните MBR и загрузочный сектор на свою диске гу.
О Найдите таблицы размещения файлов для двух разделов магнитного диска. Сохраните их па дискете. Выйдите из программы Disk Editor.
О Запустите программу FD1SK. Посмотрите структуру диска — сколько и каких разделов па нем расположено?
О Удалите все логические диски с помощью программы FDISK.
О Перезапустите компьютер и убедитесь, что операционная система, расположенная раньше на магнитном диске, больше не функционирует. Покажите преподавателю.
О Восстановите операционную систему и файлы, расположенные на магнитном диске, используя программу Disk Editor и файлы, которые вы ранее создали с ее помощью. Покажите преподавателю.
ГЛАВА 5 Архитектура операционных систем и интерфейсы прикладного программирования
Несмотря на тот факт, что в паши дни уже практически никто не разрабатывает операционные системы (естественно, за исключением нескольких известных компаний, специализирующихся на этом направлении, кстати, одном из сложнейших) и все являются пользователями наиболее распространенных систем, мы все-таки рассмотрим кратко вопросы архитектуры ОС. Сделать это необходимо потому, что многие возможности и характеристики ОС определяются в значительной мере ее архитектурой.
Основные принципы построения операционных систем
Среди множества принципов, которые используются при построении ОС, перечислим несколько наиболее важных (на наш взгляд, так как в соответствующих публикациях на эту тему перечисляется существенно большее их количество).
Принцип модульности
Под модулем в общем случае понимают функционально законченный элемент системы, выполненный в соответствии с принятыми межмодульными интерфейсами. По своему определению модуль предполагает возможность без труда заменить его на другой при наличии заданных интерфейсов. Способы обособления составных частей ОС в отдельные модули могут существенно различаться, но
чаще всего разделение происходит именно по функциональному признаку. В значительной степени разделение системы на модули определяется используемым методом проектирования ОС (снизу вверх или наоборот).
Особо важное значение при построении ОС имеют привилегированные, повторно входгемые и реентерабельные модули, гак как они позволяют более эффективно использовать ресурсы вычислительной системы. Как мы уже знаем (см. раздел «Основные виды ресурсов», глава 1), достижение реентерабельности реализуется различными способами. В некоторых системах реентерабельность программы получают автоматически, благодаря неизменяемости кодовых частей программ при исполнении (из-за особенностей системы команд машины), а также автоматическому распределению регистров, автоматическому отделению кодовых частей программ от данных и помещению последних в системную область памяти. Естественно, что для этого необходима соответствующая аппаратная поддержка. В других случаях это достигается программистами за счет использования специальных системных модулей.
Принцип модульности отражает технологические и эксплуатационные свойства системы. Наибольший эффект от его использования достижим в случае, когда принцип распространен одновременно па операционную систему, прикладные программы и аппаратуру.
Принцип функциональной избирательности
В ОС выделяется некоторая часть важных модулей, которые должны постоянно находиться в оперативной памяти для более эффективной организации вычислительного процесса. Эту часть в ОС называют ядром, так как это действительно основа системы. При формировании состава,ядра требуется учитывать два противоречивых требования. В состав ядра должны войти наиболее часто используемые системные модули. Количество модулей должно быть таковым, чтобы объем памяти, занимаемый ядром, был бы нс слишком большим. В состав ядра, как правило, входят модули по управлению системой прерываний, средства по переводу программ из состояния счета в состояние ожидания, готовности и обратно, средства по распределению таких основных ресурсов, как оперативная память и процессор. Помимо программных модулей, входящих в состав ядра и постоянно располагающихся в оперативной памяти, может быть много других системных программных модулей, которые получают название транзитных. Транзитные программные модули загружаются в оперативную память только при необходимости и в случае отсутствия свободного пространства могут быть замещены другими транзитными модулями. В качестве синонима к термину «транзитный» можно использовать термин «диск-резидентный».
Принцип генерируемое™ ОС
Основное положение этого принципа определяет такой способ исходного представления цен тральной системной управляющей программы ОС (ее ядра и основных компонентов, которые должны постоянно находиться в оперативной памяти), который позволял бы настраивать эту системную супсрвизорпую часть,
исходя из конкретной конфигурации конкретного вычислительного комплекса и круга решаемых задач. Эта процедура проводится редко, перед достаточно протяженным периодом эксплуатации ОС. Процесс генерации осуществляется с помощью специальной программы-генератора и соответствующего входного языка для этой программы, позволяющего описывать программные возможности системы и конфигурацию машины. В результате генерации получается полная версия ОС. Сгенерированная версия ОС представляет собой совокупность системных наборов модулей п данных.
Упомянутый раньше принцип модульности положительно проявляется при генерации ОС. Он существенно упрощает настройку ОС на требуемую конфигурацию вычислительной системы. В наши дни при использовании персональных компьютеров с принципом гснерируемости ОС можно столкнуться разве что только при работе с Linux. В этой UNIX-системе имеется возможность не только использовать какое-либо готовое ядро ОС, по и самому сгенерировать (скомпилировать) такое ядро, которое будет оптимальным для данного конкретного персонального компьютера и решаемых па нем задач. Кроме генерации ядра в Linux имеется возможность указать и набор подгружаемых драйверов и служб, го есть часть функций может реализовываться модулями, непосредственно входящими в ядро системы, а часть — модулями, имеющими статус подгружаемых, транзитных.
В остальных современных распространенных ОС для персональных компьютеров конфигурирование ОС под соответствующий состав оборудования осуществляется на этапе инсталляции, а йо гом состав драйверов и изменение некоторых параметров ОС может быть осуществлено посредством редактирования конфигурационного файла
Принцип функциональной избыточности
Этот принцип учитывает возможность проведения одной и топ же работы различными средствами. В состав ОС может входить несколько типов мониторов (модулей супервизора, управляющих гем или другим видом ресурса), различные средства организации коммуникаций между вычислительными процессами. Наличие нескольких типов мониторов, нескольких систем управления файлами позволяет пользователям быстро и наиболее адекватно адаптировать ОС к определенной конфигурации вычислительной системы, обеспечить максимально эффективную загрузку технических средств при решении конкретного класса задач, получить максимальную производительность при решении заданного класса задач.
Принцип виртуализации
Построение виртуальных ресурсов, их распределение и использование теперь используется практически в любой ОС Этот принцип позволяет представить структуру системы в виде определенного набора планировщиков процессов и распределителей ресурсов (мониторов) и использовать единую централизованную схему распределения ресурсов.
Наиболее естественным и закопченным проявлением концепции виртуальности является понятие виртуальной машины. По сути, любая операционная система, являясь средством распределения ресурсов и организуя по определенным правилам управление процессами, скрывает от пользователя и его приложений реальные аппаратные и иные ресурсы, заменяя их некоторой абстракцией В результате пользователи видят и используют виртуальную машину как некое устройство, способное воспринимать их программы, написанные па определенном языке программирования, выполнять их и выдавать результаты. При таком языковом представлении пользователя совершенно не интересует реальная конфигурация вычислительной системы, способы эффективного использования ее компонентов и подсистем. Он мыслит и работает с машиной в терминах используемого им языка и тех ресурсов, которые ему предоставляются в рамках виртуальной машины.
Чаще виртуальная машина, предоставляемая пользователю, воспроизводит архитектуру реальной машины, но архитектурные элементы в таком представлении выступают с новыми или улучшенными характеристиками, часто упрощающими работу с системой. Характеристики могут быть произвольными, но чаще всего пользователи желают иметь собственную «идеальную» по архитектурным характеристикам машину в следующем составе:
единообразная по логике работы память (виртуальная) практически неограниченного объема. Среднее время доступа соизмеримо со значением этого параметра оперативной памяти. Организация работы с информацией в такой памяти производится в терминах обработки данных — в терминах работы с сегментами данных на уровне выбранного пользователем языка программирования;
произвольное количество процессоров (виртуальных), способных работать параллельно и взаимодействовать во время работы. Способы управления процессорами, в том числе синхронизация и информационные взаимодействия, реализованы и доступны пользователям па уровне используемого языка в терминах управления процессами;
U произвольное количество внешних устройств (виртуальных), способных работать с памятью виртуальной машины параллельно пли последовательно, асинхронно или синхронно по отношению к работе того или иного виртуального процессора, которые инициируют работу этих устройств. Информация передаваемая или хранимая на виртуальных устройствах, не ограничена допустимыми размерами. Доступ к такой информации осуществляется па основе либо последовательного, либо прямого способа доступа в терминах соответствующей системы управления файлами. Предусмотрено расширение информационных структур данных, хранимых на виртуальных устройствах.
Степень приближения к «идеальной» виртуальной машине может быть большей или меньшей в каждом конкретном случае. Чем больше виртуальная машина, реализуемая средствами ОС на базе конкретной аппаратуры, приближена к «идеальной» по характеристикам машине и, следовательно, чем больше ее архитектурно-логические характеристики отличны от реально существующих, тем больше степень виртуальности у полученной пользователем машины
Одним из аспектов виртуализации является организация возможности выполнения в данной ОС приложений, которые разрабатывались для других ОС. Другими словами, речь идет об организации нескольких операционных сред, о чем мы уже говорили (см. главу 1). Реализация этого принципа позволяет такой ОС иметь очень сильное преимущество перед аналогичными ОС, нс имеющими такой возможности. Примером реализации принципа виртуализации может служить VDM- машила (virtual DOS machine) — защищенная подсистема, предоставляющая полную среду MS-DOS и консоль для выполнения MS-DOS приложений. Одновременно может выполняться практически произвольное число VDM-сессий. Такие VDM-машины имеются и в системах Microsoft Windows, и в OS/2.
Принцип независимости программ от внешних устройств
Этот принцип реализуется сейчас в подавляющем большинстве ОС общего применения. Мы уже говорили о нем, рассматривая принципы организации ввода/ вывода. Пожалуй, впервые наиболее последовательно данный принцип был реализован в ОС UNIX. Реализован он и в большинстве современных ОС для ПК. Напомним, этот принцип заключается в том, что связь программ с конкретными устройствами производится нс на уровне трансляции программы, а в период планирования ее исполнения. В результате перекомпиляция при работе программы с новым устройством, на котором раснола! аются данные, не требуется.
Принцип позволяет одинаково осуществлять операции управления внешними устройствами независимо от их конкретных физических характеристик. Например, программе, содержащей операции обработки последовательного набора данных, безразлично, на каком носителе эти данные будут располагаться. Смена носителя и данных, размещаемых на них (при неизменности структурных характеристик данных), нс принесет каких-либо изменений в программу, если в системе реализован принцип независимости.
Принцип совместимости
Одним из аспектов совместимости является способность ОС выполнять программы, написанные для других ОС или для более ранних версий данной операционной системы, а также для другой аппаратной платформы.
Необходимо разделять вопросы двоичной совместимости и совместимости на уровне исходных текстов приложений. Двоичная совместимость достигается в том случае, когда можно взять исполняемую программу и запустить ее па выполнение па другой ОС. Для этого необходимы: совместимость па уровне команд процессора, совместимость на уровне системных вызовов и даже на уровне библиотечных вызовов, если они являются динамически связываемыми.
Совместимость на уровне исходных текстов требует наличия соответствующего транслятора в составе системного программного обеспечения, а также совместимости на уровне библиотек и системных вызовов. При этом необходима перекомпиляция имеющихся исходных текстов в новый выполняемый модуль.
Гораздо сложнее достичь двоичной совместимости между процессорами, основанными на разных архитектурах. Для того чтобы один компьютер выполнял программы другого (например, программу для ПК типа IBM PC желательно выполнить на ПК типа Macintosh фирмы Apple), этот компьютер должен работать с машинными командами, которые ему изначально непонятны. В таком случае процессор типа 680x0 (или PowerPC) на Мас должен исполнять двоичный код, предназначенный для процессора i80x86. Процессор 80x86 имеет свои собственные дешифратор команд, регистры и внутреннюю архитектуру. Процессор 680x0 не понимает двоичный код 80x86, поэтому он должен выбрать каждую команду, декодировать ее, чтобы определить, для чего она предназначена, а затем выполнить эквивалентную подпрограмму, написанную для 680x0. Так как к тому же у 680x0 пет в точности таких же регистров, флагов и внутреннего арифметико- логического устройства, как в 80x86, он должен имитировать все втн элементы с использованием своих регистров или намял и. И он должен тщательно воспроизводить результаты каждой команды, что требует специально написанных подпрограмм для 680x0, гарантирующих, что состояние эмулируемых регистров и флагов после выполнения каждой команды будет в точности таким же, как и на реальном 80x86 Выходом в таких случаях является использование так называемых прикладных сред или эмуляторов. Учитывая, что основную часть программы, как правило, составляют вызовы библиотечных функций, прикладная среда имитирует библиотечные функции целиком, используя заранее написанную библиотеку функций аналогичного назначения, а остальные команды эмулирует каждую по отдельности.
Одним из средств обеспечения совместимости программных и пользовательских интерфейсов является соответствие стандартам POSIX. Использование стандарта POSIX позволяет создавать программы в стиле UNIX, которые впоследствии могут легко переноситься из одной системы в другую.
Принцип открытой и наращиваемой ОС
Открытая ОС доступна для анализа как пользователям, гак и системным специалистам, обслуживающим вычислительную систему. Наращиваемая (модифицируемая, развиваемая) ОС позволяет не только использовать возможности генерации, по и вводить в ее состав новые модули, совершенствовать существующие и т. д. Другими словами, необходимо, чтобы можно было легко внести дополнения и изменения, если это потребуется, и нс нарушить целостность системы. Прекрасные возможности для расширения предоставляет подход к структурированию ОС по типу клиент—сервер с использованием мпкроядерпой технологии. В соответствии с этим подходом ОС строится как совокупность привилегпро- ванпой управляющей программы и набора непривилегированных услуг — «серверов». Основная часть ОС остается неизменной и в то же время могут быть добавлены новые серверы или улучшены старые.
Этот принцип иногда трактуют как расширяемость системы.
К открытым ОС, прежде всего, следует отнести UNIX-системы и, естественно, ОС Linux.
Принцип мобильности (переносимости)
Операционная система отноыггелыю легко должна переноситься с процессора одного типа на процессор другого типа и с аппаратной платформы (которая включает наряду с типом процессора н способ организации всей аппаратуры компьютера, иначе говоря, архитектуру вычислительной системы) одного типа на аппаратную платформу другого типа. Заметим, что принцип переносимости очень близок принципу совместимости, хотя это и не одно и то же.
Написание переносимой ОС аналогично написанию любого переносимого кода — нужно следовать некоторым правилам. Во-первых, большая часть ОС должна быть написана на языке, который имеется на всех системах, па которые планируется в дальнейшем ее переносить. Это, прежде всего, означает, что ОС должна быть написана на языке высокого уровня, предпочтительно стандартизованном, например на языке С. Программа, написанная на ассемблере, нс является в общем случае переносимой. Во-вторых, важно минимизировать или, если возможно, исключить те части кода, которые непосредственно взаимодействуют с аппаратными средствами. Зависимость от аппаратуры может иметь много форм. Некоторые очевидные формы зависимости включают прямое манипулирование регистрами и другими аппаратными средствами. Наконец, если аппаратно-зависимый код не может быть полностью исключен, то он должен быть изолирован в нескольких хорошо локализуемых модулях. Аппаратно-зависимый код не должен быть распределен по всей системе. Например, можно спрятать аппаратно-зависимую структуру в программно задаваемые данные абстрактного типа. Другие модули системы будут работать с этими данными, а не с аппаратурой, используя набор некоторых функций. Когда ОС переносится, то изменяются только эти данные и функции, которые ими манипулируют.
Введение стандартов POSIX преследовало цель обеспечить переносимость создаваемого программного обеспечения.
Принцип обеспечения безопасности вычислений
Обеспечение безопасности при выполнении вычислений является желательным свойством для любой многопользовательской системы Правила безопасности определяют такие свойства, как защита ресурсов одного пользователя от других и установление квот по ресурсам для предотвращения захвата одним пользователем всех системных ресурсов (таких, как память).
Обеспечение защиты информации от несанкционированного доступа является обязательной функцией сетевых операционных систем. Во многих современных ОС гарантируется степень безопасности данных, соответствующая уровню С2 в системе стандартов США. Основы стандартов в области безопасности были заложены в документе «Критерии оценки надежных компьютерных систем». Этот документ, изданный Национальным центром компьютерной безопасности (NCSC — National Computer Security Center) в США в 1983 году, часто называют Оранжевой книгой.
В соответствии с требованиями Оранжевой книги безопасной считается система, которая «посредством специальных механизмов защиты контролирует дос-
туп к ип(1юрмации таким образом, что только имеющие соответствующие полномочия лица или процессы, выполняющиеся от их имени, могут получить доступ на чтение, запись, создание или удаление информации».
Иерархия уровней безопасности, приведенная в Оранжевой книге, помечает низший уровень безопасности как D, а высший — как Л.
В класс D понадают системы, оценка которых выявила их несоответствие требованиям всех других классов.
Основными свойствами, характерными для систем класса С, являются наличие подсистемы учета событий, связанных с безопасностью, и избирательный контроль доступа. Класс (уровень) С делится на 2 подуровня: уровень С1, обеспечивающий защиту данных от ошибок пользователей, но не от действий злоумышленников; и более строгий уровень С2. На уровне С2 должны присутствовать:
средства секретного входа, обеспечивающие идентификацию пользователей путем ввода уникального имени и пароля перед тем, как им будет разрешен доступ к системе;
избирательный контроль доступа, позволяющий владельцу ресурса определить. кто имеет доступ к ресурсу и что он может с ним делать. Владелец делает это путем предоставляемых прав доступа пользователю или группе пользователей;
средства учета и наблюдения (auditing), обеспечивающие возможность обнаружить и зафиксировать важные события, связанные с безопасностью, или любые попытки создать, получить доступ или удалить системные ресурсы;
защита памяти, заключающаяся в том, что память инициализируется перед тем, как повторно используется.
На этом уровне система не защищена от ошибок пользователя, но поведение его может быть проконтролировано по записям в журнале, оставленным средствами наблюдения и аудита.
Системы уровня В основаны на помеченных данных и распределении пользователей но категориям, то есть реализуют мандатный контроль доступа. Каждому пользователю присваивается рейтинг защиты, и он может получать доступ к данным только в соответствии с этим рейтингом. Этот уровень в отличие от уровня С защищает систему от ошибочного поведения пользователя.
Уровень А является самым высоким уровнем безопасности, он требует в дополнение ко всем требованиям уровня В выполнения формального, математически обоснованного доказательства соответствия системы требованиям безопасности.
Различные коммерческие структуры (например, банки) особо выделяют необходимость учетной службы, аналогичной той, что предлагают государственные рекомендации С2. Любая деятельность, связанная с безопасностью, может быть отслежена и тем самым учтена. Это как раз то, чего требует стандарт для систем класса С2, и что обычно нужно банкам. Однако коммерческие пользователи, как правило, не хотят расплачиваться производительностью за повышенный уровень безопасности. А-уровеиь безопасности занимает своими управляющими механизмами до 90 % процессорного времени, что, безусловно, в большинстве случаев уже неприемлемо. Более безопасные системы не только снижают эффектив-
иость по и существенно ограничивают число доступных прикладных пакетов, которые соответствующим образом могут выполняться в подобной системе. Например, для ОС Solaris (версия UNIX) есть несколько тысяч приложений, а для ее аналога В-уровия — только около era.
Микроядерные операционные системы
Микроядро — это минимальная стержневая часть операционной системы, служащая основой модульных и переносимых расширений. Существует мнение, что большинство операционных систем следующих поколений будут обладать микроядрами, Однако имеется масса разных мнений по поводу того, как следует организовывать службы операционной системы по отношению к микроядру: как проектировать драйверы устройств, чтобы добиться наибольшей эффективности, по сохрани гь функции драйверов мйксимально независимыми от аппаратуры; следует ли выполнять операции, не относящиеся к ядру, в пространстве ядра или в пространстве пользователя; стоит ли сохранять программы имеющихся подсистем (например, UNIX) или лучше отбросить все и начать с нуля.
Основная идея, заложенная в технологию микроядра, будь то собственно ОС или се графический интерфейс, заключается в том, чтобы конструировать необходимую среду верхнего уровня, из которой можно легко получить доступ ко всем функциональным возможностям уровня аппаратного обеспечения. При такой структуре ядро служит стартовой точкой для создания системы. Искусство разработки микроядра заключается в выборе базовых примитивов, которые должны в нем находиться для обеспечения необходимого и достаточного сервиса. В микроядре содержится и исполняется минимальное количество кода, необходимое для реализации основных сис темных вызовов. В число этих вызовов входят передача сообщений и организация другого общения между внешними ио отношению к микроядру процессами, поддержка управления прерываниями, а также ряд некоторых других функции. Остальные функции, характерные для «обычных» (не микроядерных) ОС, обеспечиваются как модульные дополнения-процессы, взаимодействующие главным образом между собой и осуществляющие взаимодействие посредством передачи сообщений.
Микроядро является маленьким, передающим сообщения модулем системного программного обеспечения, работающим в наиболее приоритетном состоянии компьютера и поддерживающим остальную часть операционной системы, рассматриваемую как набор серверных приложений. Интерес к микроядрам возрастал по мере того, как системные разработчики реагировали на сложность современных реализаций операционных систем, и поддержан тем, что исследовательское сообщество успешно продемонстрировало реализуемость концепции микроядра.
Созданная в университете Карнеги Меллон технология микроядра Mach служит основой для многих ОС.
Исполняемые микроядром функции ограничены в целях сокращения его размеров и максимизации количества кода, работающего как прикладная программа.
Микроядро включает только тс функции, которые требуются для определения набора абстрактных сред обработки для прикладных программ и для ортниза- ции совместной работы приложений в обеспечении сервисов и в действии клиентами и серверами. В результате микроядро обеспечивает только пять различных типов сервисов:
управление виртуальной памятью:
задания и потоки;
межпроцессные коммуникации (IPC),
U управление поддержкой ввода/вывода и прерываниями;
сервисы набора хоста и процессора.
Другие подсистемы и функции операционной системы, такие как системы управления файлами, поддержка внешних устройств и традиционные программные интерфейсы, размешаются в одном или более системных сервисах либо в задаче операционной системы. Эти программы работают как приложения микроядра.
Следуя концепции множественных потоков па одно задание, микроядро создает прикладную среду, обеспечивающую использование мультипроцессоров без требования, чтобы какая-то конкретная машина была мультипроцессорной: па однопроцессорной различные потоки просто выполняются в разные времена. Вся поддержка, требуемая для мультипроцессорных машин, сконцентрирована в сравнительно малом и простом микроядрс.
Благодаря слоим размерам и способности поддерживать стандартные сервисы программирования и характеристики в виде прикладных программ сами микро- ядра проще, чем ядра монолитных или модульных операционных систем. С микроядром функция операционной системы разбивается на модульные части, которые могут быть сконфигурированы целым рядом способов, позволяя строить большие системы добавлением частей к меньшим. Например, каждый аппаратно-независимый нейтральный сервис логически отделен и может быть сконфигурирован в широком диапазоне способов. Микроядра также облегчают поддержку мультипроцессоров созданием стандартной программной среды, которая может использовать множественные процессоры в случае их наличия, однако не требует их, если их нет. Специализированный код для мультипроцессоров ограничен самим микроядром. Более того, сети из общающихся между собой микроядер могут быть использованы для обеспечения операционной системной поддержки возникающего класса массивно параллельных машин. В некоторых случаях определенной сложностью использования микроядерного подхода на практике является замедление скорости выполнения системных вызовов при передаче сообщений через микроядро по сравнению с классическим подходом. С другой стороны, можно констатировать и обратное. Поскольку микроядра малы и имеют сравнительно мало требуемого к исполнению кода уровня ядра, они обеспечивают удобный способ поддержки характеристик реального време-
ни, требующихся для мультимедиа, управления устройствами и высокоскорос тных коммуникации. Наконец, хорошо структурированные микроядра обеспечивают изолирующий слоте для аппаратных различий, которые не маскируются применением я паков программирования высокого уровня Таким образом, oi и упрощают перенесение кода и увеличивают уровень его повторного испо. ьзоиа пня.
Наиболее ярким представителем микроядерных ОС является ОС рентного врс мепп QNX. Микроядро QNX поддерживает готько птшпронание и дпспстчери зацию процессов, взаимодействие процессов, обработку прерывании и сетевые службы нижнего уровня (более подробно об ОС QNX см в i лаве 8) Микро? ipo обеспечивает всего лишь iapv десятков системных вызовов но благодаря этому оно может быть целиком размещено во внутреннем каше даже таких троцсссе ров, как Intel 486. Как известно, разные версии этой ОС i мели и различные объ емы ядер — от 8 до 46 Кбайт.
Чтобы построить минимальную систему QNX, Требуется добавить к микроя еру менеджер протесов. который создает процессы управляет процессам i и амя тыо процессов. Чтобы ОС QNX была применима ле только во ветрен иных и б 8 дисковых системах, нужно добавить <|xiti юную систему п мене ежер устройств. Эт менеджеры испо.' няются вне прострапсувр ядра так что ядро ос гается небольшим
Монолитные операционные системы
Монолитные ОС являются прямой противоположностью мпк|юяд рным О( II >п этом можно coi таспгься с тем, как трактуется архи сктура монолитных О( в работе |56] В монолитной ОС, несмотря па ее возможную сильную структуризацию, о юнь трудно удалить один из уровне и многоуровневой моду ыи i е рук- туры. Добавление новых функций и изменение cyiдестующих д я монолитных ОС требует очень хорошего знания всей архитектуры ОС и чре вычайно больших усилий Поэтому более современный подход к проектированию ОС который может быть условно назван как «клиент-серверная» технология, позволяет в большей мере* и с меньшими трудозатратами реализовать перечисленные ш и с принципы проектирования ОС
Модель клиент сервер вреднолагает наличие программного компонента, являющегося потребителем какого-либо сервиса юшвшпа, и программного компонента, служащего поставщиком этого с ервиса«рвера. Взаимодействие м< кд
клиентом п сервсдюм стандартизируе и я, так чго сервер может об л живать клиентов, pea in ювапных различными способами и может быть раЗшimii ] «работниками. При этом главным требованием является использование едипообр; ш гэ 1пнер(|>епса Иппипат >ром обмена обычно является клиент который посылает запрос па обслуживание серверу, находящемуся в состоянии ожидания запроса Один и тот же программный компонент может быть клиентом по отношению к одному внчу услуг и сервером для другого вида услуг. Модель кл еит— сервер является скорее удобным концептуальным средством ясного представлен! я функций то о п in иного программного элемента в какой шио сипуаши неже ли
технологией. Эта модель успешно применяется не только при построении ОС, по и на всех уровнях программного обеспечения и имеет в некоторых случаях более узкий, специфический смысл, сохраняя, естественно, при этом все своп об щне черты.
При поддержке монолитных ОС возникает ряд проблем, связанных с тем, что все функции макроядра работают в едином адресном пространстве. Во-первых, это опасность возникновения конфликта между различными частями ядра; во- вторых — сложность подключения к ядру новых драйверов. Преимущество мик- роядериой архитектуры перед мошыштной заключается в том, чго каждый ком иопент системы представляет собой самостоятельный процесс, запуск или остановка которого нс отражается на работоспособности остальных процессов.
Микроядерные ОС в настоящее время разрабатываются чаще монолитных. Однако следует заметить, что использование технологии клиент—сервер — это еще ис гарантия того, что ОС станет микроядерпой. В качестве подтверждения можно привести пример с ОС Windows NT, которая построена па идеологии клиент- сервер, по которую тем не менее трудно назвать микроядерпой. Для того чтобы согласиться с таким высказыванием, достаточно сравнить ОС QNX и ОС Windows NT.
Требования, предъявляемые к ОС реального времени
Как известно, система реального времени (СРВ) должна давать от клик на любые непредсказуемые внешние воздействия в течение предсказуемого инт ервала времени. Для #гого должны быть обеспечены следующие свойства:
Ограничение врвЛ1ени отклика, то есть после наступления события реакция на пего гарантированно последует до предустановленного крайнего срока. Отсутствие такого ограничения рассматривается как серьезный недостаток программного обеспечения.
Одновременность обработки: даже если наступает более одного события одновременно, все временные ограничения для всех событий должны быть выдержаны. Это означает, что системе реального времени должен быть присущ парад челизм. Параллелизм достигается использованием нескольких процессоров в системе п/или многозадачного подхода.
Примерами сис тем реального времени являются системы управления атомными электростанциями или какими-нибудь технологическими процессами, медицинского мониторинга, управления вооружением, космической навигации, разведки, управления лабораторными эксперимен тами, у правления автомобильными двигателями, робототехника, телеметрические системы управления, системы анти- блокировки тормозов, системы сигнализации — список в принципе бесконечен.
Иногда можно услышать из разговоров спеййалистов, что различают системы «мягкого» и «жесткого» реального времени. Различие между жесткой и мягкой СРВ зависит от требований к системе — система считается жесткой, если «пару-
шенис временных ограничений не допустимо», и мягкой, если «нарушение временных ограничений нежелательно». Ведется множество дискуссий о точном смысле терминов «жесткая» и «мягкая» СРВ. Можно даже аргументировать, что мя!кая СРВ не является СРВ вовсе, ибо основное требование соблюдения временных ограничений нс выполнено. В действительности термин СРВ часто неправомерно применяют по отношению к быстрым системам.
Часто путают понятия СРВ и ОСРВ, а также неправильно используют атрибу ты «мягкая» и «жесткая». Иногда говорят, что та или иная ОСРВ мят кая или жесткая Hei мягких или жестких ОСРВ. ОСРВ может только служить основой для построения мягкой или жесткой СРВ. Сама ио себе ОСРВ не препятствует тому, что ваша СРВ будет мягкой. Например, вы решили создать СРВ, которая должна работать через Ethernet по протоколу TCP/IP. Такая система не может быть жесткой СРВ, поскольку сама сеть Ethernet в принципе непредсказуема вследствие использования случайного метода доступа к среде передачи данных, в отличие, например, от IBM Token Ring или ARC-Net, в которых используются детерминированные меч оды доступа.
Итак, перечислим основные требования к ОСРВ.
Мультипрограммность и многозадачность
Требование 1. ОС должна быть мультипрограммной и многозадачной ^многопоточной — multi-threaded) и активно использовать прерывания для диспетчеризации.
Как указывалось выше, ОСРВ должна быть предсказуемой. Это означает не то, что ОСРВ должна быть быстрой, а то, что максимальное время выполнения того или иного действия должно быть известно заранее и должно cooi ветствовать требованиям приложения. Так, например, ОС Windows 3.11 даже па Pentium III 1000 MHz бесполезна для ОСРВ, ибо одно приложение может захватить управление и заблокировать систему для остальных.
Первое требование состоит в том, что ОС должна быть многопоточной по принципу абсолютного приоритета (прерываемой). То есть планировщик должен иметь возможность прервать любой поток и предоставить ресурс тому потоку, которому он более необходим ОС (и аппаратура) должны также обеспечивать прерывания на уровне обработки прерываний.
Приоритеты задач (потоков)
Требование 2 В ОС должно существовать понятие приоритета потока.
Проблема в том, чтобы определить, какой задаче ресурс требуется более всего В идеальной ситуации ОСРВ отдаст ресурс потоку или драйверу с ближайшим крайним сроком (эго называется управлением временным ограничением, deadline driven OS). Чтобы реализовать это временное ограничение, ОС должна знать, сколько времени требуется каждому из выполняющихся потоков для завершения.
ОС, построенных по этому принципу, практически пет, так как он слишком сложен для реализации. Поэтому разработчики ОС принимают иную точку зрения: вводится понятие уровня приоритета для задачи, и временные ограничения сводят к приоритетам. Так как умозрительные решения чреваты ошибками, показатели СРВ при этом снижаются. Чтобы более эффективно осуществить указанное преобразование ограничений, проектировщик может воспользоваться теорией расписаний или имитационным! моделированием, хотя и это может оказаться бесполезным. Тем не менее, так как па сегодняшний день не имеется иного решения, понятие приоритета потока неизбежно.
Наследование приоритетов
Требование 3. В ОС должна существовать система наследования приоритетов.
Па самом деле именно этот механизм синхронизации и тот факт, что различные треды используют одно и то же пространство памяти, отличают их от процессов. Как мы уже знаем, процессы почти не разделяют одно и то же пространство памяти, а в основном работают в своих локальных адресных пространствах. Так, например, старые версии UNIX не являются мультитредовыми (multi-threaded). «Старый» UNIX — многозадачная ОС, где задачами являются процессы (а нс треды), которые сообщаются через потоки (pipes) и разделяемую память Оба эти механизма используют файловую систему, а ее поведение — непредсказуемо. Комбинация приоритетов тредов и разделения ресурсов между ними приводит к другому явлению — классической проблеме инверсии приоритетов. Это можно проиллюстрировать на примере, когда есть как минимум три треда. Когда тред низшего приоритета захватил ресурс, разделяемый с тредом высшего приоритета, и начал выполняться поток среднего приоритета, выполнение треда высшего приоритета будет приостановлено, пока не освободится ресурс и не отработает тред среднего приоритета. В этой ситуации время, необходимое для завершения треда высшего приоритета, зависит от нижних приоритетных уровней, — это и есть инверсия приоритетов. Ясно, что в такой ситуации трудно выдержать ограничение на время исполнения.
Чтобы устранить такие инверсии, ОСРВ должна допускать наследование приоритета, то еегь повышение уровня приоритета треда до уровня треда, который его вызывает. Наследование означает, что блокирующий ресурс тред наследует приоритет треда, который он блокирует (разумеется, это справедливо лишь в том случае, если блокируемый тред имеет более высокий приоритет).
Иногда можно услышать утверждение, что в грамотно спроектированной системе такая проблема не возникает. В случае сложных систем с этим нельзя согласиться. Единственный способ решения этой проблемы состоит в увеличении приоритета треда «вручную» прежде, чем ресурс окажется заблокированным. Разумеется, это возможно в случае, когда два треда разных приоритетов претендуют па один ресурс. В общем случае решения нс существует.
Синхронизация процессов и задач
Требование 4. ОС должна обеспечивать мощные, надежные и удобные механизмы синхронизации задач.
Так как задачи разделяют данные (ресурсы) и должны сообщаться друг с другом, представ пястся логичным, что должны существовать механизмы блокирования и коммуникации. Необходимы механизмы, гарантированно предоставляющие возможность параллетыю выполняющимся задачам и процессам оперативно обмениваться сообщениями и синхросигналами Эти системные механизмы должны быть всегда доступны процессам, требующим реального времени. СледоваСелЬ- по, системные ресурсы для их функционирования должны быть распределены заранее.
Предсказуемость
Требование 5 Поведение ОС должно быть известно и достаточно точно прогнозируемо.
Времена выполнения системных вызовов и временные характеристики поведения системы в различных обстоятельствах должны быть известны разработчику. Поэтому создатель ОСРВ должен приводить следующие характеристики:
латентную задержку прерывания (то есть время от момента прерывания до момента запуска задачи): опа должна быть предсказуема и согласована с требованиями приложения. Эта величина зависит от числа одновременно «висящих» прерываний;
максимальное время выполнения каждого системного вызова. Оно должно быть предсказуемо и не зависимо от числа обьектов в системе;
максимальное время маскирования прерываний драйверами и ОС.
Принципы построения интерфейсов операционных систем
Напомним, что ОС всегда выступает как интерфейс между аппаратурой компьютера и пользователем с его задачами. Под интерфейсами операционных систем здесь и далее следует понимать специальные интерфе псы (истемпого и прикладного программирования, иредпазпачеипые для выполнения следующих задач:
U Управление процессами, которое включает в себя следующий набор основных функций.
О запуск, приостапов и снятие задачи с выполнении;
О задание пли изменение приоритета задачи;
О взаимодействие задач между собой (механизмы сигналов, семафорные примитивы, очереди, конвейеры, почтовые ящики);
О RPC (remote procedure call) — удаленный вызов подпрограмм.
Управление памятью:
О запрос на выделение блока памяти;
О освобождение памяти
О изменение параметров блока памяти (например, память может быть заблокирована процессом либо предоставлена в общий доступ);
О отображение файлов на память (имеется не во всех системах).
Управление вводом/выводом:
О запрос на управление виртуальными устройствами (напомним, что управление вводом/выводом является привилегированной функцией самой ОС, и никакая из пользовательских задач не должна иметь возможности непосредственно управлять устройствами);
О файловые операции (запросы к системе управления файлами на создание, изменение и удаление данных, организованных в файлы).
Здесь мы перечислили основные наборы функций, которые выполняются ОС по соответствующим запросам от задач. Что касается пользовательского интерфейса операционной системы, то он реализуется с помощью специальных программных модулей, которые принимают его команды на соответствующем языке (возможно, с использованием графического интерфейса) и транслируют их в обычные вызовы в соответствии с основным интерфейсом системы. Обычно эти модули называют интерпретатором команд. Так, например, функции такого интерпретатора в MS-DOS выполняет модуль COMMAND СОМ. Получив от пользователя команду, такой модуль после лексического и синтаксического анализа либо сам выполняет действие, либо, что случается чаще, обращается к другим модулям ОС, используя механизм API. Надо заметить, что в последние годы большую популярность получили графические интерфейсы (GUI), в которых задействованы соответствующие манипуляторы тина «мышь» или «трекбол»1. Указание курсором на объекты и щелчок (клик) или двойной щелчок но соответствующим клавишам приводит к каким-либо действиям — запуску программы, ассоциированной с указываемым объектом, выбору и/или активизации пунктов меню и т д. Можно сказать, чго такая интерфейсная подсистема транслирует «команды» пользователя в обращения к ОС.
Поясним также, что управление GUI — частный случай задачи управления вводом/выводом, не являющийся частью ядра операционной системы, хотя в ряде случаев разработчики ОС относят функции GUI к основному системному API. Следует отметить, что имеются два основных подхода к управлению задачами Так, в одних системах порождаемая задача наследует все ресурсы задачи-родителя, тогда как в других системах существуют равноправные отношения, п при порождении нового процесса ресурсы для него запрашиваются у операционной системы.
Обращения к операционной системе, в соответствии с имеющимся API, может осуществляться как посредством вызова подпрограммы с передачей ей необходимых параметров, так и через механизм программных прерываний. Выбор метода реализации вызовов функций API должен определяться архитектурой платформы.
Trackball — в переносных компьютерах очень часто используется для управления перемещением курсора специальный шарик, который равмещасТся рядом с i гавцатурой и прокручивается пальцами.
Так, например, в операционной системе MS-DOS, которая разрабатывалась для однозадачного режима (поскольку процессор 18O86 не поддерживал мультипрограммирование), использовался механизм программных прерываний. При этом основной набор функций API был доступен через точку входа обработчика 1 nt 21h.
В более сложных системах имеется не одна точка входа, а множество — по количеству функций API. Так, в большинстве операционных систем используется метод вызова подпрограмм. В этом случае вызов сначала передается в модуль API (например, это библиотека времени выполнения'), который и перенаправляет вызов соответствующим обработчикам программных прерываний, входящим в состав операционной системы. Использование механизма прерываний вызвано, главным образом, тем, что при этом процессор переводится в режим супервизора.
Интерфейс прикладного программирования
Прежде всего необходимо однозначно разделить общий термин API (application program interface, интерфейс прикладного программирования) на следующие направления:
API как интерфейс высокого уровня, принадлежащий к библиотекам RTL;
API прикладных и системных программ, входящих в поставку операционной системы;
прочие API.
Интерфейс прикладного программирования, как это и следует из названия, предназначен для использования прикладными программами системных ресурсов ОС и реализуемых сю функции. API описывает совокупность функций и процедур, принадлежащих ядру или надстройкам ОС.
Итак, API представляет собой набор функций, предоставляемых системой программирования разработчику прикладной программы и ориентированных на организацию взаимодействия результирующей прикладной программы с целевой вычислительной системой. Целевая вычислительная система представляет собой совокупность программных и аппаратных средств, в окружении которых выполняется результирующая программа. Сама результирующая программа порождается системой программирования на основании кода исходной программы, созданного разработчиком, а также объектных модулей и библиотек, входящих в состав системы программирования.
В принципе API используется нс только прикладными, но и многими системными программами как в составе ОС, так и в составе системы программирования.
RTL (run time library) библиотека времени выполнения; она включает в себя те стандартные полпрограммы, которые система программирования подставляет на этапе компиляции. В общем случае RTL включает в себя не только модули из системы программирования. ио и модули самой ОС.
Но дальше речь пойдет только о функциях API с точки зрения разработчика прикладной программы. Для системной программы существуют некоторые дополнительные ограничения на возможные реализации ЛР1.
Функции API позволяют разработчику строить результирующую прикладную программу так, чтобы использовать средства целевой вычислительной системы для выполнения типовых операций. При этом разработчик программы избавлен от необходимости создавать исходный код для выполнения этих операций.
Программный интерфейс API включает в себя не только сами функции, но и соглашения об их использовании, которые регламентируются операционной системой (ОС), архитектурой целевой вычислительной системы и системой программирования.
Существует несколько вариантов реализации API:
реализация на уровне ОС;
реализация на уровне системы программирования;
реализация на уровне внешней библиотеки процедур и функций.
Система программирования в каждом из этих вариантов предоставляет разработчику средства для подключения функций API к исходному коду программы и организации их вызовов. Объектный код функций API подключается к результирующей программе компоновщиком при необходимости.
Вовможнусти API можно оценивать со следующих позиций:
эффективность выполнения функций API — включает в себя скорость выполнения функций и объем вычислительных ресурсов, погребных для их выполнения;
широта предоставляемых возможностей;
зависимость прикладной программы от архитектуры целевой вычислительной системы.
В идеале хотелось бы иметь набор функций API, выполняющихся с наивысшей эффективностью, предоставляющих пользователю все возможности современных ОС и имеющих минимальную зависимость от архитектуры вычислительной системы (еще лучше — лишенных такой зависимости).
Добиться наивысшей эффективности выполнения функций API практически трудно но тем же причинам, ио которым невозможно добиться наивысшей эффективности выполнения для любой результирующей программы. Поэтому об эффективности API можно говорить только в сравнении его характеристик с другим API.
Что касается двух других показателей, то в принципе нет никаких технических ограничений па их реализацию. Однако существуют организационные проблемы и узкие корпоративные интересы, тормозящие создание такого рода библиотек.
Реализация функций API на уровне ОС
При реализации функций API на уровне ОС за их выполнение ответственность несет ОС. Объектный код, выполняющий функции, либо непосредственно вхо-
дит в состав ОС (или даже ядра ОС), либо поставляется в составе динамически загружаемых библиотек, разработанных для данной ОС. Система программирования ответственна только за го, чтобы организовать интерфейс для вызова этого кода.
В таком варианте результирующая программа обращается непосредственно к ОС. Полому достигается наибольшая эффективность выполнения функций API по сравнению со всеми другими вариантами реализации API.
Недостатком организации API по такой схеме является практически полное отсутствие переносимости нс только кода результирующей программы, ио и кода исходной программы. Программа, созданная для одной архитектуры вычислительной системы, не сможет исполняться на вычислительной системе другой архитектуры даже после того, как ее объектный код будет полностью перестроен. Чаще всего система программирования нс сможет выполнить перестроение исходного кода для новой архитектуры вычислительной системы, поскольку многие функции API, ориентированные на определенную ОС, будут, в повой архитектуре просто отсутствовать.
Таким образом, в данной схеме для переноса прикладной программы с одной целевой вычисли 1елыюй системы на другую будет требоваться изменение исходного кода программы.
Переносимости можно было бы добиться, если унифицировать функции API в различных ОС. С учетом корпоративных интересов производителей ОС такое направление их развития представляется практически невозможным. В лучшем случае при приложении определенных организационных усилий удастся добиться стандартизации смыслового (семантического) наполнения основных функции API, по не их программного интерфейса.
Хорошо известным примером API такого рода может служить набор функций, предоставляемых пользователю со стороны ОС типа Microsoft Windows — W 1API (Windows API). Надо сказать, что даже внутри этого корпоративного API существует определенная несогласованность, которая несколько ограничивает переносимость программ между различными ОС тина Windows. Еще одним примером такого API можно считать набор сервисных функций ОС тина MS-DOS, реализованный в виде набора подпрограмм обслуживания программных прерываний.
Реализация функций API на уровне системы программирования
Если функции API реализуются на уровне системы программирования, они предоставляются пользователю в виде библиотеки функций соответствующего языка нрот раммирования. Обычно речь идет о библиотеке времени исполнения — RTL (run time library). Система программирования предоставляет пользователю библиотеку соответствующего языка программирования и обеспечивает подключение к результирующей программе объектного кода, ответственного за выполнение этих функций.
Очевидно, что эффективность функций API в таком варианте будет несколько ниже, чем при непосредственном обращении к функциям ОС. Так происходит, поскольку для выполнения многих функций API библиотека RTL языка программирования должна все равно выполнять обращения к функциям ОС. Наличие всех необходимых вызовов и обращений к функциям ОС в объектном коде RTL обеспечивает система программирования.
Однако переносимость исходного кода программы в таком варианте будет самой высокой, поскольку синтаксис и семантика всех функций будут строго рыла- мептированы в стандарте соответствующего языка программирования. Опи зависят от языка и не зависят от архитектуры целевой вычислительной системы. Поэтому для выполнения прикладной программы на новой архитектуре вычислительной системы достаточно заново построить код результирующей программы с помощью соответствующей системы программирования.
Единообразное выполнение функций языка обеспечивается системой программирования. При ориентации на различные архитектуры целевой вычислительной системы в системе программирования могут потребоваться различные комбинации вызовов функций ОС для выполнения одних и тех же функций исходного языка. Это должно быть учтено в коде RTL. В общем случае для каждой архитектуры целевой вычислительной системы будет требоваться свой код RTL языка программирования. Выбор того или иного объектного кода RTL для подключения к результирующей программе система программирования обеспечивает автоматически
Например, рассмотрим функции динамического выделения памяти в языках С и Pascal В С это функции malloc, realloc и free (функции new и delete в C++), в Pascal — функции new и dispose. Если использовать эти функции в исходном тексте программы, то с точки зрения исходной программы они будут действовать одинаковым образом в зависимости только от семантики исходного кода. При этом для разработчика исходной программы не имеет значения, па какую архитектуру ориентирована его программа. Э го имеет значение для системы программирования, которая для каждой из этих функций должна подключить к результирующей программе объектный код библиотеки. Этот код будет выполнять обращение к соответствующим функциям ОС. Не исключено даже, что для однотипных но смыслу функций в разных языках (например, malloc в С и new в языке Pascal выполняют схожие по смыслу действия) этот код будет выполнять схожие обращения к ОС. Однако для различных вариантов ОС этот код будет различен даже при использовании одного н того же исходного языка.
Проблема, главным образом, заключается в том, что большинство языков про- фаммировапня предоставляют пользователю не очень широкий набор стандартизованных функций. Поэтому разработчик исходного кода существенно ограничен в выборе доступных функций API. Как правило, набора стандартных функций оказывается недостаточно для создания полноценной прикладной программы. Тогда разработчик может обратиться к функциям других библиотек, имеющихся в составе системы программирования. В этом случае нет гарантии, чго функции, включенные в состав данной системы программирования, по не входящие в стандарт языка программирования, будут доступны в другой системе программирования. Особенно если она ориентирована на другую архитектуру
целевой вычислительной системы. Такая си туация уже ближе к третьему варианту реализации API.
Например, те же функции malloc, realloc и free в языке С фактически не входя! в стандарт языка Опи входят в состав стандартной библиотеки, которая «де- факто» входит во все системы программирования, построенные на основе язы ка С. Общепринятые стандарты существуют для многих часто используемых функций языка. Если же взять более специфические функции, такие как функции порождения новых процессов, то для них пи в С, ни в языке Pascal не окажется общепринятого стандарта.
Реализация функций API с помощью внешних библиотек
При реализации функций API с помощью внешних библиотек они предоставляются пользователю в виде библиотеки процедур и функций, созданной сторонним разработчиком. Причем разработчиком такой библиотеки может выступать тот же самый производитель.
Система программирования ответственна только за то, чтобы подключить объектный код библиотеки к результирующей программе. Причем внешняя библиотека может быть и динамически загружаемой (загружаемой во время выполнения программы).
С точки зрения эффективности выполнения этот метод реа шзации API имеет самые низкие результаты, поскольку внешняя библиотека обращается как к функциям ОС, так и к функциям RTL языка программирования. Только при очень высоком качестве внешней библиотеки се эффективность становится сравнимой с библиотекой RTL.
Если говорить о переносимости исходного кода, то здесь требование только одно — используемая внешняя библиотека должна быть доступна в любой из архитектур вычислительных систем, на которые ориентирована прикладная программа. Тогда удается достигнуть переносимости. Это возможно, если используемая библиотека удовлетворяет какому-то принятому стандарту, а система программирования поддерживает этот стандарт.
Например, библиотеки, удовлетворяющие стандарту POSIX (см. следующий подраздел), доступны в большинстве систем программирования для языка С. И если прикладная программа использует только библиотеки этого стандарта, то ее исходный код будет переносимым. Еще одним примером является широко известная бпбтиотека графического интерфейса XLib, поддерживающая стандарт графической среды X Window.
Для б< плпипегва специфических библиотек отдельных разработчиков это нс так. Если но.н юватель использует какую-то библиотеку, то опа ориентирована па ограниченный набор доступных архитектур целевой вычислительной системь Примерами могут служить библиотеки MI С (Microsoft foundation classes) фирмы Microsoft и VOL (visual controls library) фирмы Borland, жестко ориентированные па архитектуру ОС типа Windows.
Тем не менее, многие фирмы-разработчики сейчас стремятся создать библиотеки, которые бы обеспечивали переносимость исходного кода приложений между различными архитектурами целевой вычислительной системы Пока еще такие библиотеки не получили широкого распространения, поесть несколько попыток их реализации — например, библиотека СЕХ производства фирмы Borland ориентирована па архитектуру ОС типа Linux и ОС тина Windows.
В целом развитие функций прикладного API идет в направлении попытки создать библио теки API, обеспечивающие широкую переносимость исходного кода. Однако, учитывая корпоративные интересы различных производителей и сложившуюся ситуацию на рынке системного программного обеспечения, в ближайшее время вряд ли удастся достичь значительных успехов в этом направлении. Разработка широко применимого стандарта АР! пока еще остается «ц чом будущего.
Поэтому разработчики системных программ вынуждены оставаться в довольно узких рамках ограничений стандартных библиотек языков программирования.
Что касается прикладных программ, то гораздо большую перспективу для них предоставляют технологии, связанные с разработками в рамках архитектуры «клиент сервер» пли трехуровневой архитектуры создания приложений. В этом направлении ведущие производители ОС, СУБД и систем программирования скорее достигнут соглашений, чем в направлении стандартизации АРЕ
Итак, нами были рассмотрены основные принципы, цели и подходы к реализации системных АРЕ Отметим еще один очень важный момент: желательно, что бы интерфейс прикладного программирования не зависел от системы програм мпроваппя Конечно, были одно время персональные компьютеры, у которых базовой системой программирования выступал интерпретатор с языка Basic, но это скорее исключении. Обычно базовые функции API не зависят от системы программирования и могут использоваться 1гз любой системы программирования, хотя и с применением соответствующих правил нос троения вызывающих последовате плюс гей В то же время в раде случаев система программирования может сама генерировать обращения к функциям АРЕ Например, мы можем написать в протрамме вызов функции ио запросу 256 байт памяти
unsigned char * ptr - malloc (256):
LPVOID ПеарАГосС HANDLE hHeap, DWORD dwFlags DWORD dwBytes );LPVOID ПеарАГосС HANDLE hHeap, DWORD dwFlags DWORD dwBytes );С истома программирования языка С сгенерирует целую последовательность об ращений. Из кода пользовательской программы будет осуществлен вызов библиотечной функции malloc, код которой расположен в RTLязыка С. Библиотека времени выполнения в данном случае реализует вызов malloc уже как вызов системной функции API HeapAlloc
LPVOID ПеарАГосС HANDLE hHeap, DWORD dwFlags DWORD dwBytes );
LPVOID ПеарАГосС HANDLE hHeap, DWORD dwFlags DWORD dwBytes );
// handle to the private heap block - указатель на блок
П heap allocation control flags - свойства блока
// number of bytes to allocate - размер блока
Параметры выделяемого блока памяти в таком случае задаются системой программирования, и пользователь лишен возможности задавать их напрямую. С дру-
гой стороны, если это необходимо, возможно использование функций API прямо в тексте программы.
unsigned char * ptr - (LPVOID) HeapAlloct GetProcess eap(), 0 256)
В этом случае программирование вызова немного усложняется, но получаемый конечный результат будет, как правило, короче и, что самое важное, будет работать эффективнее. Следует отметить, что далеко не все возможности API доступны через обращения к функциям системы программирования. Непосредственное обращение к функциям API позволяет пользователю обращаться к системным ресурсам более эффективным способом. Однако это требует знания функций API, количество которых нередко достигает нескольких сотен.
Как правило, API не стандартизированы. В каждом конкретном случае набор вызовов API определяется, прежде всего, архитектурой ОС и се назначением. В то же время принимаются попытки стандартизировать некоторый базовый набор функций, поскольку это существенно облегчает перенос приложений с одной ОС в другую. Таким примером может служить очень известный и, пожалуй, одни из самых распространенных с тандартов — стандарт POSIX. В этом стандарте перечислен большой набор функций, их параметров и возвращаемых значений Стандартизированными, согласно POSIX, являются не только обращения к API, но и файловая система, организация доступа к внешним устройствам, набор системных команд1. Использование в приложениях этого стандарта позволяет в дальнейшем легко переносить такие программы с одной ОС в другую путем простейшей перекомпиляции исходного текста
Частным случаем попытки стандартизации API является внутренний корпоративный стандарт компании Microsoft, известный как WinAPI. Он включает в себя следующие реализации: Win-16, Win32s. Win32, WinCE. С точки зрения WinAPI (в силу ряда идеологических причин обязательный графический «окопный» интерфейс пользователя), базовой задачей является окно. Таким образом, W тАР1 изначально ориентирован па работу в 1рафической среде. Однако базовые понятия дополнены традиционными функциями, в том числе частично поддержива ется стандарт POSIX
Платформенно-независимый интерфейс POSIX
POSIX (Portable Operating System Interface for Computer Environments) — плат- форменпо независимый системный интерфейс для компьютерного окружения Это стандарт IEEE, описывающий системные интерфейсы для открытых операционных систем, в том числе оболочки, утилиты и инструментарии Помимо этого, согласно POSIX, стандартизированными являются задачи обеспечения безопасности, задачи реального времени, процессы администрирования, сетевые
В данном контексте под системными командами следует понимать некий набор про грамм позволяющих управлять вычислительными процессами Например, pstat, ki I, dir и др.
функции и обработка транзакций. Стандарт базируется па UNIX-снс гемах, но допускает реализацию и в других ОС.
POSIX возник как попытка всемирно известной организации IEEE пропагандировать переносимость приложений в UNIX-средах путем разработки абстрактного, платформенно-независимого стандарта. Однако POSIX не ограничивается только UNIX-системами; существуют различные реализации этого стандарта в системах, которые соответствуют требованиям, предъявляемым стандартом IEEE Standard 1003.1-1990 (POSIX 1) Например, известная ОС реального времени QNX соответствует спецификациям этого стандарта, что облегчает перенос приложений в эту систему, по UNIX-системой не является пи в каком виде, ибо ее архитектура использует абсолютно иные принципы
Этот стандарт подробно опис ывает VMS (virtual memory system, систему виртуальной памяти), многозадачность (МРЕ, multi process executing) и технологию переноса операционных систем (CTOS). Таким образом, на самом деле POSIX представляет собой множество стандартов, именуемых POSIX 1 — POSIX 12.
В табл 5.1 приведены основные направления, описываемые данными стандартами Следует также особо отметить, что POSIX.1 предполагает язык С как основной язык описания системных функций API.
Таблица 5 1. Семейство стандартов POSIX
Стандарт
Стандарт ISO
Краткое описание
POSIX.0
Пет
Введение в стандарт открытых систем Данный документ не является стандартом в чистом виде, а представляет собой рекомендации и крат кий об тор технологий
POSIX 1
Да
Системный API (язык С)
POSIX.2
Пет
Оболочки и утилиты (одобренные IEEE)
POSIX.3
11гт
Тестирование и верификация
POSIX Л
Нет
Задачи реального времени и инти
POS1X5
Да
Использование языка ADA применительно к стандарту' POSIX.1
POSIX.6
Пет
Сисгемная безопасность
POSIX 7
Нет
Администркрование системы
POS1X.8
Нет
Сеги
«Прозрачный» доступ к файлам
Абстрактные сетевые интерфейсы, не зависящие от физических протоколов
RPC (remote procedure calls, вызовы удаленных процедур)
Связь г истомы с протоколо-зависимыми приложениями
IEEE (Institute of Eluctrical and Electronical Engineers) — американский Институт инженеров ио элекгрспехиикс и радиоэлектронике.
Стандарт
Стандарт ISO
Краткое описание
POSIX.9
Да
Использование языка FORTRAN применительно к стандарту POSIX.1
POSIX.10
11ет
Super-computing Application Environment Profile (AEP)
POSIX. И
Ист
Обработка транзакции AEP
POSIX.12
Нет
Графический интерфейс пользователя (GUI)
Таким образом, программы, написанные с соблюдением данных стандартов, будут одинаково выполняться на всех POSIX-совместимых системах. Однако стандарт в некоторых случаях носит лишь рекомендательный характер. Часть стандартов описана очень строго, тогда как другая часть только поверхностно раскрывает основные требования. Нередко программные системы заявляются как POSIX- совместимые, хотя таковыми их назвать нельзя. Причины кроются в формальности подхода к реализации POSIX-интерфейса в различных ОС. На рис 5.1 изображена типовая схема реализации строго соответствующего POSIX приложения.
Рис. 5.1. Приложения, строго соответствующие стандарту POSIX
Из рис. 5.1 видно, что для взаимодействия с операционной системой программа использует только библиотеки POSIX.1 и стандартную библиотеку 1ГГЕ языка С, в которой возможно использование лишь 110 различных функций, также описанных стандартом’ POSIX I
К сожалению, достаточно часто с целью увеличить производительность той или иной подсистемы либо из соображений введения фирменных технологий, которые ограничив нот использование приложения соответствующей операционной средой, при программировании используются другие функции, не отвечающие стандарту POSIX.
Реализации POSIX API па уровне операционной системы различны. Если UNIX- системы в своем абсолютном большинстве изначально соответствуют спецификациям IEEE Standard 1003.1-1990, то WinAPI не является POSIX-совмсстнмым. Однако для поддержки данного стандарта в операционной системе MS Win dows NT введен специальный модуль поддержки POSIX API, работающий на
уровне привилегий пользовательских процессов. Данный модуль обеспечивает конвертацию и передачу вызовов из пользовательской программы к ядру системы и обратно, работая с ядром через W AIL Прочие приложения, написанные с использованием WinAPI, могут передавать информацию POSIX-прпложениям через стандартные механизмы потоков ввода/вывода (stdin, stdout) [97]
Пример программирования в различных API ОС
Для наглядной демонстрации принципиальных различ! й API наиболее популярных современных операционных систем для ПК ]*ссзуотрпм простейший при- v. ер, в котором реализуется следующая задача.
Постановка задачи, необходимо подсчитать количестве пробелов в текстовых (файлах, имена которых должны указываться в командной строке. Рассмотрим два варианта программы, решающей эту задачу, — для Windows (с использованием WinAPI) и для Linux (POSIX API). ’
Поскольку пас интересует работа с параллельными задачами, пусть при вынол- п№и программы для каждого перечисленного в командной строке файла создается свой процесс или задача (тред), который параллельно с другими процессами (тредами) ирон води работу по подсчету пробелов в «своем» файле Результатом работы программы будет являться список файлов с подсчитанным количеством пробелов для каждого.
Сл< дует обратить особое вннмапИ на io, что приведенная ниже конкретная реализация данной задачи не является единственно возможной. В обеих рассматриваемых операционных системах существуют различные методы как работы с файловой системой, так и управления процессами. В данном случае рассматривается только один, но наиболее характерны для соответсiвующего API вариант.
Текст программы для Windows (WinAPI)
Для того чтобы было удобней сравнивать эту и следующую программы, а также из-за того, что настоящая задача не требует для своего решения окопного интерфейса, в нижеприведенном Тексте использованы только те вызовы API, которые не затрагивают графическим интерфейс. Конечно, нынче редко какое приложение не использует возможностей GUI, по в пашем случае зато сразу можно увидеть разницу в организации параллельной работы запускаемых вычислений.
#include «windows.h>
#include <stdio.h>
^include «stdllb h>
// Название: processFile
// Описание: исполняемый код треда
// Входные параметры: IpFileName - имя файла для обработки
// Выходные парамет с нет
//
DWORD processFile(LPVOID IpFileName) (
HANDLE handle; // описатель файла
DWORD numRead, total - 0: char buf:
// запрос к ОС на открытие файла (только для чтения) handle - CreateFileC (LPCTSTR)lpFileNaine, GENERIC READ.
FILE SHARE READ. NULL. OPEN_EXISTING. FILE_ATTRIBUTE_NORMAL. NULL):
// цикл чтения до конца файла do {
// чтение одного символа из файла
ReadFilet handle. (LPVDID) &buf. 1. SnumRead. NULL): if (buf -- 0x20) total+
} while ( numRead > 0):
fprintf( stderr. "(ThreadID: XLu). File Xs. spaces = Xd\r" GetCurrentThreadldO. IpFileName total)
// закрытие файла CloseHandle( handle):
return(O): }
// Название: main
// Описание главная программа
// Входные параметры: список имен файлов для обработки
// Выходные параметры: нет //
1 nt ma ndnt argc. char *argv[]) {
int 1:
DWORD pid
HANDLE hThrd[255]:// массив ссылок на треды
// для всех файлов, перечисленных в командной строке for (1 - 0: 1< (argc-1): 1++) (
// запуск треда - обработка одного файла
hThrd[i] - CreateThread( NULL. 0x4000.
(LPTHREAD_START_ROUTINE) processFile
(LPVDID) argv[i+l], 0. &pid);
fprintf( stdout. "processFile started (HND-Xd)\n". hThrd[1]): )
// ожидание окончания выполнения всех запушенных тредов
WaitForMultipleObjects( argc-1. hThrd. true. INFINITE)
return(O):
}
Обратите внимание, что основная программа запускает треды и ждет окончания их выполнения.
Текст программы для Linux (POSIX API)
^include <sys/types.h>
#include <sys/stat.h>
#mclude <wait.h>
#include <fcntl h> ^include <stdio.h>
// Название processFile
// Описание, обработка файла, подсчет кол-ва пробелов
// Входные параметры fileName - имя файла для обработки
// Выходные параметры: кол-во пробелов в файле
//
mt processFile( char *fileName) {
int handle. numRead. total - 0
Char buf:
// запрос к ОС на открытие файла (только для чтения) handle - open! fileName, O_RDONLY):
// цикл чтения до конца файла
do {
// чтение одного символа из файла
numRead - read( handle. &buf. 1): if (buf ~ 0x20) total++-
} while (numRead > 0):
// закрытие файла closet handle); return( total);
}
// Название: main
// Описание; главная программа
// Входные параметры, список имен файлов для обработки
// Выходные параметры: нет
//
int maindnt argc. char *argv[]) {
int 1. pid. status
// для всех файлов, перечисленных в командной строке
for (i - 1: i< argc: i**) (
// запускаем дочерний процесс
pid - forkO
if (pid -- 0) (
// если выполняется дочерний процесс
// вызов функции счета количества пробелов в файле
printf( "(PID: $d). File Is. spaces - ld\n”. getpidO. argv[ i], processFile( argv[ i])):
// выход из процесса
exitO:
}
// если выполняется родительский процесс
el se
printf( "processFile started (pid=ld)\n“. pid)
}
// ожидание окончания выполнения всех запущенных процессов
if (pid !“ 0) while (wait(&sratus)>0):
return;
)
Из этого текста видно, что в этом случае все вычисления принимают статус процессов, а не тредов.
В заключение можно заметить, что очень трудно сравнивать API При их разработке создатели, как правило, стараются реализовать полный набор основных функций, используя которые можно решать различные задачи, хотя, порой, и различными способами. Один набор будет хорош для одного набора задач, другой — для иного набора задач. Тем более что фактически у нас сейчас существенно ограниченное множество API Причина в том, что доминируют наиболее распространенные ОС, на распространение которых в большей степени оказали влияние не достоинства или недостатки этих ОС и их API, а правильная маркетинговая политика фирм, их создавших.
Контрольные вопросы и задачи
Вопросы для проверки
Перечислите и поясните основные принципы построения операционных систем.
Расскажите об основных моментах характерных для микроядерных ОС. Какие основные функции должно выполнять микроядро ОС?
Перечислите основные требования, предъявляемые к операционным системам реального времени.
Какие задачи возлагаются на интерфейс прикладного программирования (API)?
Какими могут быть варианты реализации API? В чем заключаются достоинства и недостатки каждого варианта?
Что такое библиотека времени выполнения (RTL)9
Что такое POSIX? Какими преимуществами обладают программы, созданные с использованием только стандартных функций, предусмотренных POSIX?
ГЛАВА 6 Проектирование параллельных взаимодействующих вычислительных процессов
При создании современных приложений, позволяющих использовать все возможности операционных систем и плане организации параллельных и распределенных вычислений, одной из важнейших проблем является проблема синхронизации взаимодействия параллельных вычислительных процессов, обмена между ними данными. Существующие методы синхронизации и обмена сообщениями различаются по таким параметрам, как удобство использования при программировании параллельных процессов, стоимость реализации, эффективность функционирования созданных приложений и всей вычислительной системы в целом.
Операционные системы имеют в своем составе различные средства синхронизации. Знание этих средств и их правильное использование позволяет создавать программы, которые при своей работе осуществляют корректный обмен информацией, а также исключают возможность возникновения тупиковых ситуаций.
В настоящем разделе рассматриваются основные понятия и проблемы, характерные для параллельных процессов. Описываются основные механизмы синхронизации, дается их сравнительный анализ, приводятся примеры характерных программ, использующих данные механизмы.
Независимые и взаимодействующие вычислительные процессы
Основной особенностью мультипро^хишных операционных систем является то, что в их среде параллельно развивается несколько (последовательных) вычислительных процессов. С точки зрения внешнего наблюдателя эти последователь-
ные вычислительные процессы выполняются одновременно, мы будем использовать термин «параллельно». При этом под параллельными понимаются не только процессы, одновременно развивающиеся на различных процессорах, каналах и устройствах ввода/вывода, но и те последовательные процессы, которые разделяют центральный процессор и хотя бы частично перекрываются во времени. Любая мультипрограммная операционная система вместе с параллельно выполняющимися в пей задачами пользователей может быть логически описана как совокупность последовательных процессов, которые, с одной стороны, состязаются за использование ресурсов, переходя из одного состояния в другое, а с другой — действуют почти независимо друг от друга, но образуют систему вследствие установления всевозможного рода связей между ними (путем пересылки сообщений и синхронизирующих сигналов).
Итак, параллельными мы будем называть такие последовательные вычислительные процессы, которые одновременно находятся в каком-либо активном состоянии. Два параллельных процесса могут быть пезаеисимыми (independing processes) либо озаимодвйстоуюиуаш (cooperating processes).
Нввависимыми являются процессы, множества переменных которых не пересекаются. Под переменными в этом случае понимают файлы данных, а также области оперативной памяти, сопоставленные определенным в программе и промежуточным переменным. Независимые процессы не влияют на результаты работы друг друга, так как не могут изменить значения переменных другого независимого процесса. Опи могут только явиться причиной задержек исполнения других процессов, так как вынуждены разделять ресурсы системы.
Взаимодсйствукпцие процессы совместно используют некоторые (общие) переменные, и выполнение одного процесса может повлиять на выполнение другого. Как мы уже говорили, при выполнении вычислительные процессы разделяют ресурсы системы. Подчеркнем, что при рассмотрении вопросов синхронизации вычислительных процессов из числа разделяемых ими ресурсов исключаются: центральный процессор и программы, реализующие эти процессы; то есть с логической точки зрения каждому процессу соответствуют свои процессор и программа, хотя в реальных системах обычно несколько процессов разделяют один процессор и одну пли несколько программ. Многие ресурсы вычислительной системы могут совместно использоваться несколькими процессами, но в каждый момент времени к разделяемому ресурсу может иметь доступ только один процесс. Ресурсы, которые нс допускают одновременного использования несколькими процессами, называются критическими.
Если нескольким вычислительным процессам необходимо пользоваться критическим ресурсом в режиме разделения, им следует синхронизировать свои действия таким образом, чтобы ресурс всегда находился в распоряжении не более чем одного из процессов. Если один процесс пользуется в данный момент критическим ресурсом, то все остальные процессы, которым нужен этот ресурс, должны получить отказ и ждать, пока он не освободится. Если в операционной системе нс предусмотрена защита от одновременного доступа процессов к критическим ресурсам, в пей могут возникать ошибки, которые трудно обнаружить и исправить. Основной причиной возникновения этих ошибок является то, это процессы в мультипрограммных операционных системах развиваются с различными
скоростями, а относительные скорости развития каждого из взаимодействующих процессов ие известны и не подвластны ни одному из них. Более того, па их скорости могут влиять решения планировщиков, касающиеся других процессов, с которыми ни одна из этих программ не взаимодействует. Кроме гого, содержание одного процесса и скорость его исполнения обычно неизвестны другому процессу. Поэтому влияние, которое оказывают друг на друга взаимодействующие процессы, ие всегда предсказуемо и воспроизводимо.
Взаимодействовать могут либо конкурирующие процессы, либо процессы, совместно выполняющие общую работу. Конкурирующие процессы, на первый взгляд, действуют относительно независимо, но они имеют доступ к общим переменным. Процессы, выполняющие общую совместную работу таким образом, что результаты вычислений одного процесса в явном виде передаются другому, то есть их работа построена именно на обмене данными, называются сотрудничающими. Взаимодействие сотрудничающих процессов удобно всего рассматривать в схеме «производитель — потребитель» (produces — consumer) или, как часто говорят — «поставщик — потребитель».
В качестве первого примера рассмотрим работу двух процессов Р1 и Р2 с общей переменной X. Пусть оба процесса асинхронно, независимо один от другого, изменяют (например, увеличивают) значение переменной X, считывая ее значение в локальную область памяти Rj, при этом каждый процесс выполняет некоторые последовательности операций во времени (рис. 6.1). Здесь мы рассмотрим нс все операторы каждого из процессов, а только ге, в которых осуществляется работа с общей переменной X. Каждому из операторов мы присвоили некоторый условный номер.
№
Процесс Р1
№
Процесс Р2
оператора
оператора
1
R1:=X
4
R2:=X
2
R1-R1+X
5
R2:=R2+1
3
X:=R1
6
X:=R2
Рис. 6.1. Пример конкурирующих процессов
Поскольку при мультипрограммировании процессы могут иметь различные ско роста исполнения, то может иметь место любая последовательность выполнения операций во времени. Если сначала будут выполнены все операции процесса Р1, а уже потом — все операции процесса Р2 (или, наоборот, сначала операции 4-6, а затем — операции 1-3), то в итоге переменная X получит значение, равное Х+2 (рис. 6.2).
Однако, если в промежуток времени между выполнением операций 1 и 3 будет выполнена хотя бы одна из операций 4-6 (рис. 6.3), то значение переменной X после выполнения всех операций будет нс (Х+2), а (Х+1)
Р1: (1):=Х; (2) R1 =R1+1; (3)X:=R1;
Р2:Р2:(4) R2:=X; (5) R2:=R+1; (6) X:=R2;
Р2:
Р2:
Рис. 6.2. Первый вариант развития событий при выполнении процессов
Р1: (1)R1:=X;(2) R1:=R1+1(3)X=R1;
Р2(4) R2:=X(5) R2:=R2+1(6)X=R2
Рис. 6.3. Второй вариант развития событий при выполнении процессов
Понятно, что это очень серьезная (и, к сожалению, неисправимая, так как се нельзя проконтролировать) ошибка. Например, если бы процессы Р1 и Р2 осуществляли продажу билетов и переменная X фиксировала количество уже проданных, то в результате некорректного взаимодействия было бы продано несколько билетов на одно и то же место.
В качестве второго примера рассмотрим ситуацию, которая еще совсем недавно была достаточно актуальной для первых персональных компьютеров. Пусть па ПК с простейшей одноирограммной операционной системой (тина MS-DOS) установлена некоторая резидентная программа с условным названием TIME, которая по нажатию на комбинацию клавиш (например, Ctrl+T) воспроизводит на экране дисплея время. Допустим, что значения переменных, указывающих час, минуты и секунды, равны 18:20:59, причем вывод па дисплей осуществляется справа налево (то есть в порядке: секунды, минуты, часы). Пусть сразу же после передачи программой TIME на дисплей информации «59 секунд» генерируется прерывание от таймера и значение времени обновляется: 18.21 00.
После этого программа TIME, прерванная таймером, продолжит свое выполнение, и на дисплей будут выданы значения: минуты = 21, часы - 18. В итоге на экране мы увидим: 18:21:59.
Рассмотрим теперь несколько иной случай развития событий обновления значений времени по сигналу таймера. Если программа ведения системных часов после вычислений количества секунд 59 + 1 - 60 и замены его на 00 прерывается от нажатия клавиш Ctrl+T, то есть программа не успевает осуществить пересчет количества минут, то время, индицируемое па дисплее, станет равным 18:20:00. И в этом случае мы получим неверное значение времени.
Наконец, в качестве третьего примера приведем пару процессов, которые изменяют различные поля записей служащих какого-либо предприятия [37J Пусть процесс АДРЕС изменяет домашний адрес служащего, а процесс СТАТУС — его должность и зарплату. Пусть каждый процесс копирует всю запись СЛУЖАЩИЙ в свою рабочую область. Предположим, что каждый процесс должен обработать
некоторую запись ИВАНОВ. Предположим также, что после того, как процесс АДРЕС скопировал запись ИВАНОВ в свою рабочую область, но до того, как он записал скорректированную запись обратно, процесс СТАТУС скопировал первоначальную запись ИВАНОВ в свою рабочую область. Изменения, выполненные тем из процессов, который первым запишет скорректированную запись назад в файл СЛУЖАЩИЕ, будут утеряны и, вовможно, никто не будет знать об этом.
Чтобы предотвратить некорректное исполнение конкурирующих процессов вследствие нерегламентированиого доступа к разделяемым переменным, необходимо ввести механизм взаимного исключения, который нс позволит двум процессам одновременно обращаться к разделяемым переменным.
Кроме реализации в операционной системе средств, организующих взаимное исключение и тем самым регулирующих доступ процессов к критическим ресурсам, в ней должны быть предусмотрены средства, синхронизирующие работу взаимодействующих процессов. Другими словами, процессы должны обращаться к неким средствам не только ради синхронизации с целью взаимного исключения, ио и чтобы обмениваться данными.
Допустим, что «поставщик» — жо процесс, который отправляет порции информации (сообщения) другому процессу, имя которого «потребитель». Например, процесс пользователя, порождающий строки для вывода, может выступать как «поставщик», а процесс, который выводит эти строки на печать, — как «потреби тель». Один из методов, применяемых при реализации передачи сообщений, состоит в том, что заводится пул свободных буферов, каждый из которых может содержать одно сообщение (длина сообщения может быть произвольной, по ограниченной).
В этом случае между процессами «поставщик» и «потребитель» будем иметь очередь заполненных буферов, содержащих сообщения. Когда «поставщик» хочет послать очередное сообщение, он добавляет в конец этой очереди еще один буфер. «Потребитель», чтобы получить сообщение, забирает из очереди буфер, который стоит в ее начале. Такое решение, хотя и кажется тривиальным, требует, чтобы «поставщик» и «потребитель» синхронизировали свои действия. Например, они должны следить за количеством свободных и заполненных буферов. «Поставщик» может передавать сообщения только до тех пор, пока имеются свободные буферы. Аналогично, «потребитель» может получать сообщения только если очередь не пуста. Ясно, что для учета заполненных и свободных буферов нужны разделяемые переменные, поэтому для сотрудничающих процессов, как и для конкурирующих, тоже возникает необходимость во взаимном исключении.
Таким образом, до окончания обращения одной задачи к общим переменным следует исключить возможность обращения к ним другой задачи. Эта ситуация и называется взаимным исключением. Другими словами, при организации различного рода взаимодействующих процессов приходится организовывать взаимное исключение и решать проблему корректного доступа к общим переменным (критическим ресурсам). Те места в программах, в которых происходит обраще-
пие к критическим ресурсам, называются критическими секциями или критическими интервалами (Critical Section — CS). Решение этой проблемы заключается в организации такого доступа к критическому ресурсу, когда только одному процессу разрешается входить в критическую секцию. Данная задача только на первый взгляд кажется простой, ибо критическая секция, вообще говоря, не является последовательностью операторов программы, а является процессом, то есть последовательностью действий, которые выполняются этими операторами. Другими словами, несколько процессов, которые выполняются по одной и гой же программе, могут выполнять критические интервалы, базирующиеся на одной и той же последовательности операторов программы.
Когда какой-либо процесс находится в своем критическом интервале, другие процессы могут, конечно, продолжать свое исполнение, но без входа в их критические секции. Взаимное исключение необходимо только в том случае, когда процессы обращаются к разделяемым, общим данным. Если же они выполняют операции, которые не приводят к конфликтным ситуациям, они должны иметь возможность работать параллельно. Когда процесс выходит из своего критического интервала, то одному из остальных процессов, ожидающих входа в свои критические секции, должно быть разрешено продолжить работу (если в этот момент действительно есть процесс в состоянии ожидания входа в свой критический интервал).
Обеспечение взаимоисключения является одной из ключевых проблем параллельного программирования. При этом можно перечислить следующие требования к критическим секциям [37, 92р
в любой момент времени только одни процесс должен находиться в своей критической секции;г
пи один процесс не должен находиться в своей критической секции бесконечно долго;
ни один процесс не должен ждать бесконечно долго входа в свой кригический интервал. В частности:
О никакой процесс, бесконечно долго находящийся вне своей критической секции (что допустимо), не должен задерживать выполнение других процессов, ожидающих входа в свои критические секции. Другими словами, процесс, работающий вне своей критической секции, нс должен блокировать критическую секцию другого процесса;
О если два процесса хотят войти в свои критические интервалы, то принятие решения о том, кто первым войдет в критическую секцию, не должно откладываться бесконечно долго;
если процесс, находящийся в своем критическом интервале, завершается либо естественным, либо аварийным путем, то режим взаимоисключения должен быть отменен, с тем чтобы другие процессы получили возможность входить в свои критические секции.
Было предложено несколько способов решения этой проблемы — программные и аппаратные; частные, низкоуровневые и глобальные, высокоуровневые; предусматривающие свободное взаимодействие между процессами и требующие строгого соблюдения жестких протоколов.
Средства синхронизации и связи при проектировании взаимодействующих вычислительных процессов
Все известные средства для решения проблемы взаимного исключения основаны на использовании специально введенных аппаратных возможностей, к которым относятся блокировка памяти, специальные команды типа «проверка и установка» и управление системой прерываний, позволяющее организовать такие механизмы, как семафорные операции, мониторы, почтовые ящики и др. С помощью перечисленных средств можно разрабатывать взаимодействующие процессы, при исполнении которых будут корректно решаться все задачи, связанные с проблемой критических интервалов Рассмотрим эти средства в порядке их появления, а значит, по мере их усложнения, перехода к функциям операционной системы и увеличения предоставляемых ими удобств для пользователя При этом будем опираться на далеко не новую, ио все же еще достаточно актуальную работу Дейкстры [27].
Использование блокировки памяти при синхронизации параллельных процессов
Все вычислительные машины и системы (в том числе и с многопортовыми блоками оперативной памяти) имеют такое средство для организации взаимного исключения, как блокировка пальяти. Это средство запрещает одновременное исполнение двух (и более) команд, которые обращаются к одной и той же ячейке памяти. Поскольку в некоторой ячейке памяти хранится значение разделяемой переменной, то получит ь доступ к ней может только один процесс, несмотря на возможное совмещение выполнения команд во времени на различных процессорах (или па одном процессоре, но с конвейерной организацией параллельного выполнения команд).
Механизм блокировки памяти предотвращает одновременный доступ к разделяемой переменной, но не предотвращает чередование доступа. Таким образом если критические интервалы исчерпываются одной командой обращения к памяти, данного средства может быть достаточно для непосредственной реализации взаимного исключения. Если же критические секции требуют более одного обращения к памяти, то задача становится сложной, но алгоритмически разрешимой. Рассмотрим различные попытки использования механизма блокировки памяти для организации взаимного исключения при выполнении критических интервалов и покажем некоторые важные моменты, пренебрежение которыми приводит к неприемлемым или даже ошибочным решениям.
Возможные проблемы при организации взаимного исключения посредством использования только блокировки памяти
Пусть имеются два (или более) циклических процесса, в которых есть абстрактные критические секции, то есть каждый из процессов состоит из двух частей: некоторого критического интервала и оставшейся части кода, в которой пет работы с общими (критическими) переменными. Пусть эти два процесса асинхронно разделяют во времени единственный процессор либо выполняются на отдельных процессорах, каждый из которых имеет доступ к некоторой общей области памяти, с которой и работают критические секции. Проиллюстрируем эту ситуацию с помощью рис. 6.4.
Рис. 6.4. Модель взаимодействующих процессов
Проблема кажется легко решаемой, если потребовать, чтобы процессы ПР1 и ПР2 входили в свои критические интервалы попеременно. Для этого одна общая переменная может хранить указатель того, чья очередь войти в критическую секцию Текст такого решения на языке, близком к Pascal, приведен в листинге 6.1.
Листинг 6.1. Первый вариант попытки реализации взаимного исключения
Var перекп : integer;
Begin перекл ;- 1; { при перекл=1 в CS находится процесс ПР1 } Parbegin
While true do
Begin
while перекл = 2 do begin end;
CS1: { Критическая секция процесса ПР1 }
перекл :- 2:
PR1;{ оставшаяся часть процесса ПР1 }
• End
And
While true do
Beg п
while перекл = 1 do begin end;
CS2: { Критическая секция процесса ПР2 } перекл :- 1:
PR2: { оставшаяся часть процесса ПР2 }
End
Раrend
End.
Здесь и далее конструкция следующего типа
parbegin...511: 512. ... SIN]
and... 521: 522: - : $2Мг
and... SKI: SK2: ; SKNlk
parend
означает параллельность К описываемых последовательных процессов. Конструкция из операторов Sil; S12: ... : S1N1 выполняется оператор за оператором, о чем свидетельствует наличие точки с запятой между операторами
Конструкция
while true do
begin 51: 52: SN end
означает, что каждый процесс может выполняться неопределенное время. Конструкция типа begin end означает «пустой» оператор.
Итак, решение, представленное в листинге 6.1, обеспечивает взаимное исключение в работе критических интервалов. Однако если бы часть программы PR1 была намного длиннее, чем программа PR2, или если бы процесс ПР1 был заблокирован в секции PR1, или если бы процессор для ПР2 был с бойыпим быстродействием, то процесс. IIP2 вскоре вынужден был бы ждать (и может быть, чрезвычайно долго) входа в свою критическую секцию CS2, хотя процесс ПР1 и был бы вне CS1 То есть при таком решении один процесс вне своей критической секции может помешать вхождению другого в свою критическую секцию.
Попробуем устранить это блокирование с помощью использования двух общих переключателей, которые используются как флаги для указания, находится ли соотвс ствующий процесс вне своей критической секции.
Пусть с каждым из процессов ПР1 и ПР2 будет связана переменная, по смыслу являющаяся переключателем, которая принимает значение true, когда процесс находится в своем критическом интервале, и false - в противном случае. Прежде чем войти в свой критический интервал, процесс проверяет значение переключателя другого процесса. Если это значение равно true, процессу не разрешается входить в свой критический интервал. В противном случае процесс «включает» свой собственный переключатель и входил в критический интервал. Этот алгоритм взаимного исключения представлен в листинге 6 2.
Данный алгоритм нс гарантирует полного выполнения условия нахождения только одного процесса внутри критического интервала. Отсутсгвие гарантий связано с различными, в общем случае, скоростями развития процессов. Поэтому,
например, между проверкой значения переменной перекл2 процессом ПР1 и последующей установкой им значения переменной перекл! параллельно выполняющийся процесс ПР2 может установить перекл2 в значение true, так как перекл! еще не успел установиться в значение true. Отсюда следует, чго оба процесса могут пойти одновременно в свои критические интервалы.
Листинг 6.2. Второй вариант попытки реализации взаимного исключения
Var перекл 1,перекл2.: boolean;
begin перекл1:“Га15е;
перекл2:-Га15е;
parbegin
while true do
begin
while перекл2 do
begin
end:
перекл!:-true:
CS1 (* Критический интервал процесса ПР! *)
перекл1:-Га15е:
PR! (* процесс ПР1 после критического интервала *) end
and
while true do
begi n
v/hile перекл! do
begin
end;
перекл2:-1гие:
(* Критический интервал процесса ПР2 *) перекл2:-false;
(* процесс ПР2 после критического интервала *) end
parend
end.
Следующий (третий) вариант решения этой задачи (листинг 6.3) усиливает взаимное исключение, так как в процессе ПР1 проверка значения переменной перекл2 выполняется после установки переменной перекл! в значение true (аналогично для ПР2).
Листинг 6.3. Третий вариант попытки реализации взаимного исключения
var перекл!. лерекл2 boolean
begin перекл!;-false: nepeKn2;-false:
parbegin
ПР1: while true do
begin
перекл!:-true;
while перекл2 do begin end;
CS1 { Критический интервал процесса ПР! }
перекл!:“false:
PR1 { ПР1 после критического интервала } end
and
ПР2. while true do
begin
перекл2:=1гие
while перекл! do
begin end:
CS2.{ Критический интервал процесса ПР2 }
nepeKn2:-false:
PR2 { ПР2 после критического интервала } end parend end.
Алгоритм, приведенный в листинге 6.3, также имеет свои недостатки. Действительно, возможна ситуация, когда оба процесса одновременно установят свои переключатели в значение true и войдут в бесконечный цикл. В этом случае будет нарушено требование отсутствия бесконечного ожидания входа в свой критический интервал. Предположив, чю скорости исполнения процессов произвольны, мы получили такую последовательность событий, при которой процессы вообще перестанут нормально выполняться.
Рассмотренные попытки решить проблему критических интервалов иллюстри руют некоторые тонкости, лежащие в основе этой проблемы.
Последний вариант решения задачи взаимного исключения, и пользующий только блокировку памяти, который мы рассмотрим, — это известный алгоритм, предложенный математиком Деккером.
Алгоритм Деккера
Алгоритм Деккера основан на использовании трех переменных (листинг 6.4): перекл!, перекл2 и ОЧЕРЕДЬ. Пусть по-прежнему переменная nepeKnl=true тогда, когда процесс ПР1 хочет войти в свой критический интервал (для ПР2 аналогично), а значение переменной ОЧЕРЕДЬ указывает, чье сейчас право сделать попытку входа, при условии, что оба процесса хотят выполнить свои критические интервалы.
Листинг 6 4. Алгоритм Деккера
label 1. 2:
var перекл!. перекл2: boolean:
ОЧЕРЕДЬ : integer;
Begin перекл!:=false; перекл2:-false:
ОЧЕРЕДЬ:-!продолжение »
Листинг 6.4 (продолже ие)
parbegin
while true do
begin перекл!:-true:
- if перекл2Чгие then
f ОнГРЕДЬ-! then go to 1
else begin перекл! -false:
while ОЧЕРЕДЬ-2 do beg n end end
else begin
CS1 { Критический интервал ПР1 }
ОЧЕРЕДЬ.-2: перекл!.-faIse end end and
while true do
begin neper л2 -1
if перекл!-ргие tnen
if ОЧЕРЕДЬ-2 then go to 2
else begin перекл2 -false.
while ОЧЕРЕДЬ-1 do begin end end
else begin
CS2 { Критический ишервал ПР2 }
ОЧЕРЕДЬ =1; перекл2 -false end end parend end.
Если nepeKn2=true и перекл!=Тalse, то выполняется критический интервал процесса ПР2 независимо от значения переменной ОЧЕРЕДЬ. Аналогично для случая перекл2=Еalse и nepeKnl=true.
Если же оба процесса хотят выполнить свои критические интервалы, то есть перекл2=Ьгие и nepeKnl=true, го выполняется критический интервал того процесса, на который указывало значение переменной ОЧЕРЕДЬ, независимо от скоростей развития обоих процессов. Использование переменной ОЧЕРЕДЬ совместно с перекл! и перекл2 в алгоритме Деккера позволяет гараптированпо'решать проблему критических интервалов. Переменные перекл! и перекл2 обеспечивают, что взаимное выполнение не может иметь места; переменная ОЧЕРЕДЬ гарантирует от взаимного блокирования. Взаимное блокирование невозможно, так как переменная не изменяет своего значения во время выполнения программы принятия решения о том, кому же сейчас проходить свои критический интервал.
Тем не менее, реализация критических интервалов па основе описанного алгоритма практически не используется из-за чрезмерной сложности, особенно в случаях, когда алгоритм Деккера обобщается с двух до N процессов
Синхронизация процессов посредством операции «ПРОВЕРКА И УСТАНОВКА»
Операция «ПРОВЕРКА И УСТАНОВКА» является, как и блокировка памяти, одним из аппаратных средств решения задачи критического интервала. Данная операция реализована па многих компьютерах. Так, в знаменитой IBM 360 (370) эта команда называлась TS (test and set). Команда TS является двухадресной (двухоперандной). Ее действие заключается в том, что процессор присваивает значение второго операнда первому, после чего второму операнду присваивается значение, равное единице. Команда TS является неделимой операцией, то есть между ее началом и концом не могу г выполняться никакие другие команды.
Чтобы использовать команду TS для решения проблемы критического интервала, свяжем с пей переменную common, которая будет общей для всех процессов, использующих некоторый критический ресурс. Данная переменная будет принимать единичное значение, если какой-либо из взаимодействующих процессов находится в своем критическом интервале. С каждым процессом связана своя локальная переменная которая принимает значение, равное единице, если данный процесс, хочет войти в свой критический интервал. Операция TS будет при- епшгвагь значение common локальной переменной и устанавливать common в единицу. Программа решения проб темы критического интервала на примере двух параллельных процессов приведена в листинге 6.5.
Листинг 6.5. Взаимное исключение с помощью операции «ПРОВЕРКА И УСТАНОВКА»
var common local 1. 1оса12 integer: begin
common.-0 parbegin ПР1 while true do begin local 1:-l: while locall=l do TSClocall. common);
CS1; { Критический интервал процеса ПР1 } common:=0;
PR1:{ ПР1 после критического интервала }
end and ПР2 while true do begin local2 -1 while local2-l do TS(locai2. common).
CS2:{ Критический интервал процеса ПР2 }
common -0: PR2:{ ПР2 после критического интервала }
end parend end
Предположим, что первым захочет войти в свой критический интервал процесс ПР1. В атом случае значение local 1 сначала установится в единицу, а после цикла проверки с помощью команды TSGocall, common) — в ноль. При этом значение common станет равным единице. Процесс ПР1 войдет в свой критический интервал. После выполнения этого критического интервала common примет значение, равное нулю, что даст возможность второму процессу ПР2 войти в своп критический интервал.
Безусловно, мы предполагаем, что в компьютере предусмотрена блокировка памяти, то есть операция common:=0 неделима. Команда «ПРОВЕРК/Х И УСТАНОВКА» значительно упрощает решение проблемы критических интервалов. Главное свойство этой команды — ее неделимость.
Основной недостаток использования операций типа «ПРОВЕРКА И УСТАНОВКА» состоит в следующем: находясь в цикле проверки переменной common, процессы впустую потребляют время центрального процессора и другие ресурсы. Действительно, если предположить, что произошло прерывание процесса ПР1 во время выполнения своего критического интервала в соответствии с некоторой дисциплиной обслуживания и начал выполняться процесс ПР2, то он войдет в цикл проверки, впустую тратя процессорное время. В этом случае до тех нор, пока диспетчер супервизора нс поставил на выполнение процесс ПР1 и не даст ему закончиться, процесс ПР2 не сможет войти в свой критический интервал.
В микропроцессорах i80386 и старше, с которыми мы теперь сталкиваемся постоянно, есть специальные команды: BTC, BTS, ВТК, которые как раз и являются вариантами реализации команды типа «ПРОВЕРКА И УСТАНОВКА». Рассмотрим одну из них — BTS.
Команда BTS (bit test and reset — проверка бита и установка) является двухадресной [48]. По этой команде процессор сохраняет значение бита из первого операнда со смещением, указанным вторым операндом, во флаге CF регистра флагов, а затем устанавливает указанный бит в 1 Значение индекса выбираемого бита может быть представлено постоянным непосредственным значением в команде RTS или значением в общем регистре. В этой команде используется только 8-битное непосредственное значение. Значение этого операнда берется по модулю 32, таким образом, смещение битов находится в дианавоне от 0 до 31. Это позволяет выбирать любой бит внутри регистра. Для битовых строк в памяти это иоле непосредственного значения дает только смещение внутри слова или двойного слова.
С учетом изложенного можно привести фрагмент текста, в котором используется данная команда для решения проблемы взаимного исключения (листинг 6.6)
Однако здесь следует заметить, что некот орые асссмб теры поддерживают значения битовых смещений больше 31, используя поле непосредственного значения в комбинации с полем смещения операнда в памяти. В «том случае ассемблером младшие 3 или 5 бигов (3 — для 16-битных операндов, 5 —- для 32-битных операндов) смещения бига (второй операнд команды) сохраняются в поле испо-
средственного операнда, а старшие биты сдвигаются и комбинируются с полем смешения. Процессор же игнорирует ненулевые значения старших битов поля второго операнда [48]. При доступе к памяти процессор обращается к четырем байтам, начинающимся по полученному следующим образом адресу
Effective Address + (4 * (BitOffset DIV 32))
для размера операнда 32 бита, или к двум байтам, начинающимся по адресу
Effective Address + (2 * (BitOffset DIV 16))
для 16-бптпого размера операнда Такое обращение происходит, даже если необходим достуш только к одиночному байту. Поэтому избегайте ссылок к областям памяти, близким к «пустым» адресным пространствам. В частности, избегайте ссылок на распределенные в памяти регистры ввода/вывода. Вместо этого используйте команду MOV для загрузки и сохранения значений по таким адресам и регистровую форму команды BTS для работы с данными.
Листинг 6.6. Фрагмент программы на ассемблере с критическим интервалом
L: BTC М.1 JC L
критическая секция
AND М. ОВ
Несмотря па то что и алгоритм Деккера, основанный только па блокировке памяти, и операция «ПРОВЕРКА И УСТАНОВКА» пригодны для реализации взаимного исключения, оба эти приема очень неэффективны. Всякий раз, когда один из процессов выполняет свой критический интервал, любой другой процесс, который пытается войти в свою критическую секцию, понадает в цикл проверки соответствующих переменных-флагов, регламентирующих доступ к критическим переменным. При таком неопределенном пребывании в цикле, которое называется активным ожиданием, напрасно расходуется процессорное время, поскольку процесс имеет доступ к тем общим переменным, которые и определяют возможность или невозможность входа в критическую секцию. При этом процесс занимает ценное время центрального процессора, на самом деле ничего реально не выполняя. Как результат, мы имеем общее замедление вычислительной системы процессами, которые реально не выполняют никакой полезной работы. .
До тех пор, пока процесс, занимающий в данный момент критический ресурс, не решит его уступить, все другие процессы, ожидающие этого ресурса, могли бы вообще не конкурировать за процессорное время. Для этого их нужно перевести в состояние ожидания (заблокировать их выполнение). Когда вход в критическую секцию снова будет свободен, можно будет опять перевести заблокирован-
ный процесс в состояние готовности к выполнению и дать ему возможность получить процессорное время. Самый простой способ предоставить процессорное время только одному вычислительному процессу — это отключить систему прерываний, поскольку тогда никакое внешнее событие не сможет прервать выполняющийся процесс. Однако это, как мы уже знаем, приведет к тому, что система не сможет реагировать на внешние события.
Вместо того чтобы связывать с каждым процессом свою собственную переменную, как это было в рассмотренных нами решениях, можно со всем множеством конкурирующих критических секций связать одну переменную, которую и рас- смагривать как некоторый ключ. Вначале доступ к критической секции открыт. Однако перед входом в свой критический интервал процесс забирает «ключ» и тем самым блокирует другие процессы. Покидая критическую секцию, процесс открывает доступ, возвращая «ключ» на место. Если процесс, который хочет войти в свою критическую секцию, обнаруживает, что ключ «отсутствует», то он додже и быть переведен в состояние блокирования до тех пор, пока процесс, имеющий ключ, не вернет его. Таким образом, каждый процесс, входящий в критический интервал, должен вначале проверить, доступен ли ключ, и если это так, го сделать его недоступным для других процессов. Причем самым главным является то, что эти два действия должны быть неделимыми, чтобы два или более процессов не могли одновременно получить доступ к кеючу Более того, проверку того, можно ли войти в критический интервал, лучше всего выполнять нс самим конкурирующим процессам, так как это приводит к активному ожиданию, а возложить эту функцию па операционную систему. Таким образом, мы подошли к одному из самых главных механизмов решения проблемы взаимного исключения — семафорам Дейкстры.
Семафорные примитивы Дейкстры
Понятие семафорных механизмов было введено Дейкстрой [27]. Семафор — переменная специального типа, которая доступна параллельным процессам для проведения над ней только двух операции: «закрытии» и «открытия», названных соответственно Р- и V-операциями. Эти операции являются примитивами относительно семафора, который указывается в качестве параметра операций. Здесь семафор выполняет роль вспомогательного критического ресурса, так как операции Р и V неделимы при своем выполнении и взаимно исключают друг друга.
Семафорный механизм работает по схеме, в которой сначала исследуется состояние критического ресурса, идентифицируемое значением семафора, а затем уже осуществляется допуск к критическому ресурсу или отказ от пего па некоторое время. При отказе доступа к критическому ресурсу используется режим «пассивного ожидания». Поэтому в состав механизма включаются средства формирования и обслуживания очереди ожидающих процессов. Эти средства реализуются супервизором операционной системы. Необходимо отмстить, что в силу взаимного исключения примитивов попытка в различных параллельных процессах одновременно выполнить примитив над одним и тем же семафором приведет к тому, что она будет успешной только для одного процесса. Все остальные процессы будут взаимно исключены па время выполнения примитива.
Основным достоинством использования семафорных операций является отсутствие состояния «активного ожидания», что может существенно повысить эффективность работы мультипрограммной вычислительной системы
В настоящее время на практике используется много различных видов семафорных механизмов [75]. Варьируемыми параметрами, которые отличают различные виды примитивов, являются начальное значение и диапазон изменения значений семафора, логика действий семафорных операций, количество семафоров, доступных для обработки при исполнении отдельного примитива.
Обобщенный смысл примитива P(S)1 состоит в проверке текущего значения семафора S, и если оно не меньше пуля, то осуществляется переход к следующей за примитивом операции. В противном случае процесс снимается па некоторое время с выполнения и переводится в состояние «пассивного ожидания». Находясь в списке заблокированных, ожидающий процесс нс проверяет семафор непрерывно, как в случае активного ожидания. Вместо него на процессоре может исполняться другой процесс, который реально совершает полезную работу.
Операция V(S)2 связана с увеличением значения семафора на единицу и переводом одного или нескольких процессов в состояние готовности к центральному процессору.
Отмстим еще раз, что операции Р и V выполняются операционной системой в огвег на запрос, выданный некоторым процессом и содержащий имя семафора в качестве параметра.
Рассмотрим первый! вариант алгоритма работы семафорных операций, который представлен в листинге 6.7.
Допустимыми значениями семафоров являются только целые числа. Двоичным семафором будем называть семафор, максимально возможное значение которого будет равно единице. В противном случае семафоры называют N-пчпыми. Есть реализации, в которых семафорные переменные нс могут быть отрицательными, а есть и такие, где отрицательное значение указывает па длину очереди процессов, стоящих в состоянии ожидания открытия ссма<]юра.
Листинг 6.7. Вариант реализации семафорных примитивов
P(S): S:=S-1:
if S<0 then { остановить процесс и поместить его в очередь ожидания к семафору S } V(s): if S<0 then (поместить один из ожидающих процессов очереди семафора S в очередь готовности}:
S-S+1
Заметим, что для работы с семафорными переменными необходимо еще иметь операцию инициализации самого семафора, то есть операцию задания ему начального значения. Обычно эту операцию называют InitScm; как правило, она имеет два параметра — имя семафорной переменной и се начальное значение; следовательно, обращение к ней имеет вид:
ImtSem (Имя_семафора. Начальное_значение_семафора);
—_—
Р — от голландского Proberen — провери гь.
V — от голландского Vcrhogen — увеличить.
Попытаемся на нашем примере двух конкурирующих процессов ПР1 и ПР2 проанализировать использование данных семафорных примитивов для решения проблемы критического интервала. Программа, иллюстрирующая это решение, представлена в листинге 6.8.
Листинг 6 8. Взаимное исключение с помощью семафорных операций
van S semafor;
begin InitSem(S.l);
parbegin
ПР1 while true do
begin
P(S):
CS1 { Критический интервал процесса ЛР1 }
V(S)
end
and
ПР2: while true do
begin
P(S):
CS2 { Критический интервал процесса ПР2 }
V(S)
end
parend
end.
Семафор S имеет начальное значение, равное 1. Если процессы ПР1 и ПР2 попытаются одновременно выполнить примитив P(S), то это удастся успешно сделать только одному из них. Предположим, это сделал процесс ПР2, тогда он закрывает семафор S, после чего выполняется его критический интервал. Процесс ПР1 в рассматриваемой ситуации будет заблокирован на семафоре S Тем самым будет гарантировано взаимное исключение.
После выполнения примитива V(S) процессом ПР2 семафор S открывается, указывая на возможность захвата каким-либо процессом освободившегося критического ресурса. При этом производится перевод процесса ПР1 из заблокированного состояния в состояние готовности.
На уровне реализации возможно одно из двух решений в отношении процессов, которые переводятся из очереди ожидания в очередь готовности при выполнении примитива V:
процесс при его активизации (выборка из очереди готовности) вновь пытается выполнить примитив Р, считая предыдущую попытку неуспешной;
процесс при помещении его в очередь готовности отмечается как успешно выполнивший примитив Р. Тогда при его активизации управление будет передано не на повторное выполнение примитива Р, а на команду, следующую за ним.
Рассмотрим первый способ реализации. Пусть процесс ПР2 в некоторый момент времени выполняет операцию P(S). Тогда семафор S становится равным пулю. Пусть далее процесс ПР1 пытается выполнить операцию PCS). Процесс ПР1 в этом случае «засыпает» на семафоре S, так как значение семафора S равнялось нулю, а теперь станет равным (-1). После выполнения критического интервала процесс ПР2 выполняет операцию VCS), при этом значение семафора S становится равным нулю, а процесс ПР1 переводится в очередь готовности. Пусть через некоторое время процесс ПР1 будет активизирован, то есть выведен из состояния ожидания, п сможет продолжить свое исполнение. Он повторно пытается выполнить операцию PCS), однако это ему не удается, так как 5=0. Процесс ПР1 «засыпает» на семафоре, а его значение становится равным (-1) Если через некоторое время процесс ПР2 попытается выполнить PCS), то он тоже «уснет». Таким образом, возникнет так называемая тупиковая ситуация, так как «будить» процессы ПР1 и ПР2 некому
При втором способе реализации тупиковой ситуации нс будет. Действительно, пусть все происходит также до момента окончания исполнения процессом ПР2 примитива VCS). Пусть примитив VCS) выполнен и 5=0. Через некоторое время процесс ПР1 активизировался. Согласно данному способу реализации, он сразу же попадет в свой критический интервал. При этом никакой другой процесс не попадет в свой критический интервал, так как семафор остался закрытым. После исполнения своего критического интервала процесс ПР1 выполнит VCS). Если за время выполнения критическою интервала процесса ПР1 процесс ПР2 не делал попыток выполнить операцию PCS), семафор S установится в единицу. В противном случае значение семафора будет не больше нуля. Но в любом варианте после завершения операции VCS) процессом ПР1 доступ к критическому ресурсу со стороны процесса ПР2 будет разрешен.
Заметим, что возникновение тупиков возможно в случае несогласованного выбора механизма реактивации процессов из очереди, с одной стороны, и выбора алгоритмов семафорных операций — с другой.
Возможен другой алгоритм работы семафорных операций:
PCS)' if S>-1 then S -S 1
else hAITCS (остановить процесс и помести ь в очередь ож ания к семафору S) V(S). if S<0 then RELEASE(Si(noMecTMTb один из ожидающих процессов очереди семафора S в очередь готовности):
S-S+1.
Здесь вызов WAITCS) означает, что супервизор ОС должен перевести задачу в состояние ожидания, причем очередь процессов связана с семафором S. Вызов RELEASECS) означает обращение к диспетчеру задач с просьбой перевести первый из процессов, стоящих в очереди S, в состояние готовности к исполнению.
Использование семафорных операций, выполненных подобным образом, позво ляет решать проблему критических интервалов на основе первого способа реализации без вероятности возникновения тупиков. Действительно, пусть ПР2 в некоторый момент времени выполнит операцию PCS). Тогда семафор S становится равным нулю. Пусть далее процесс ПР1 пытается выполнить операцию PCS). Процесс ПР1 в этом случае «заснет» на семафоре S, так как 5=0, причем значе-
ние S нс изменится. После выполнения своего критического интервала процесс ПР2 выполнит операцию V(S), при этом значение семафора S станет равным единице, а процесс ПР1 переведется в очередь готовности. Если через некоторое время процесс ПР1 будет активизирован, он успешно выполнит P(S) и войдет в свой критический интервал.
В однопроцессорной вычислительной системе неделимость Р- и V-операций можно обеспечить с помощью простого запрета прерываний Сам же семафор S можно реализовать в виде записи с двумя полями (листинг 6.9). В одном поле хранится целое значение S, во втором — указатель на список процессов, заблокированных на семафоре S.
Листинг 6 9. Реализация Р- и V-операций для однопроцессорной системы
type Semaphore - record
счетчик : integer:
указатель :pointer: end:
var S Semaphore:
procedure P ( var S ; Semaphore):
begin ЗАПРЕТИТЬ_ПРЕРЫВАНИЯ:
S.счетчик:- S.счетчик-1:
if S.счетчик < 0 then
WA1T(S): ( ВСТАВИТЬ ОБРАТИВШИЙСЯ ПРОЦЕСС В СПИСОК ПО S.указатель и УСТАНОВИТЬ НА ПРОЦЕССОР ГОТОВЫЙ К ВЫПОЛНЕНИЮ ПРОЦЕСС } РАЗРЕШИТЬ_ПРЕР№АНИЯ
end:
procedure V ( var S : Semaphore):
begin ЗАПРЕТИТЬ ПРЕРЫВАНИЯ:
S.счетчик:- $.счетчик+1
if S.счетчик <= 0 then
RELEASE (S): { ДЕБЛОКИРОВАТЬ ПЕРВЫЙ ПРОЦЕСС ИЗ СПИСКА ПО S указатель } РАЗРЕШИТЬ_ПРЕРЫВАНИЯ
end:
procedure InitSem (var S : Semaphore); begin
S.счетчик:-!
S.указатель:-nil:
end:
Реализация семафоров в мультипроцессорных системах более сложна, чем в однопроцессорных. Одновременный доступ к семафору S двух процессов, выполняющихся на однопроцессорной вычислительной системе, предотвращается запретом прерываний. Однако этот механизм не подходит для мультипроцессор ных систем, так как он не препятствует двум пли более процессам одновременно обращаться к одному семафору. В силу того, что такой доступ должен рсализо-
выдаться как критический интервал необходимо дополнительное аппаратное взаимное исключение доступа для различных процессоров. Одним из решений является использование уже знакомых нам неделимых команд типа «ПРОВЕРКА И УСТАНОВКА» (TS). Двухкомпонентный семафор в этом случае расширяется включением третьего компонента — логического признака ВзаиноИскл (листинг 6.10).
Листинг 6.10. Реализация Р- и V-операций для мультипроцессорной системы
type Semaphore - record
счетчик integer.
указатель ; pointer:
взаиноискл boolean:
end:
var S : Semaphore:
procedu'e InitSeni (var S Semaphore):
begin
With S do
begin
счетчик:-!:
указатель -ni
взаиноискл:-true:
end:
end:
procedure P ( var S Semaphore):
var разрешено : boolean
beg n
ЗАПРЕТИТЬ_ПРЕРЫВАНИЯ;
repeat Т5(разрешено. S. взаиноискл) until pa решено;
5.счетчик:=5.счетчи1 1
if S.счетчик < 0 then WAIT(S): ( ВСТАВИТЬ ОБРАТИВШИЙСЯ ПРОЦЕСС В СПИСОК
ПО S.указатель И УСТАНОВИТЬ НА CPU ГОТОВЫЙ ПРОЦЕСС }
S. взаиноискл:-true:
РАЗРЕШИ Ь ПРЕРЫВАНИЯ
end:
procedure V ( var S : Semaphore ): var разрешено boolean;
begin
ЗАПРЕТИТЬПРЕРЫВАНИЯ:
repeat TS(paзрешено.5.взаиноискл) until разрешено:
S.счетчик:=S.счетчик+1
if S.счетчик <- 0 then RELEASE(S): ( ДЕБЛОКИРОВАТЬ ПЕРВЫЙ ПРОЦЕСС ИЗ СПИСКА ПО S.указатель }
S.взаиноискл -true:
РАЗРЕШИТЬ_ПРЕРЫВАНИЯ-
end;
Обратите внимание, что в данном тексте команда типа «проверка и установка» — Т5(разрешено.5.взаиноискл) — работает не с целочисленными, а с булевыми значениями. Практически это ничего не меняет, ибо текст программы и ее машинная (двоичная) реализация — это все-таки разные вещи.
Мьютексы
Одним из вариантов семафорных механизмов для организации взаимного исключения являются так называемые мьютексы (mutex). Термин mutex произошел от английского словосочетания mutual exclusion semaphore, что дословно и переводится как семафор взаимного исключения. Мьютексы реализованы по многих ОС, их основное назначение — организация взаимного исключения для задач (потоков) из одного и того же или из разных процессов. Мьютексы — это простейшие двоичные семафоры, которые могут находиться в одном из двух состояний — отмеченном или неотмеченном (открыт и закрыт соответственно). Когда какая-либо задача, принадлежащая любому процессу, становится владельцем объекта mutex, последний переводится в неотмеченное состояние. Если задача освобождает мьютекс, его состояние становится отмеченным.
Организация последовательного (а не параллельного) доступа к ресурсам с использованием мьютексов становится несложной, поскольку в каждый конкретный момент только одна задача может владеть этим объектом. Для того чтобы объект mutex стал доступен задачам (потокам), принадлежащим разным процессам, при создании ему необходимо присвоить имя. Потом это имя нужно передать «по наследству» задачам, которые должны его использовать для взаимодействия. Для этого вводятся специальные системные вызовы (CreatcMutex), в которых указываются начальное значение мьютекса, его имя и, возможно, атрибуты защиты. Если начальное значение мьютекса равно true, то считается, что задача создающая этот объект, будет им сразу владеть. Можно указать в качестве начального значение false — в этом случае мьютекс нс принадлежит ни одной из задач и только специальным обращением к нему можно изменить его состояние.
Для работы с мьютексом имеется несколько функций. Помимо уже упомянутой функции создания такого объекта (CreatcMutex), есть функции открытия (Open- Mutex), ожидания событий (WaitForSingleObject н WaitForMultiplcObjccts) и, наконец, освобождение этого объекта (ReleascMutex).
Конкретные обращения к этим функциям и перечни передаваемых и получаемых параметров нужно смотреть в документации на соответствующую ОС.
Использование семафоров при проектировании взаимодействующих вычислительных процессов
Семафорные примитивы чрезвычайно широко используются при проектировании разнообразных вычислительных процессов. При этом некоторые задачи являются настолько «типичными», что их детальное рассмотрение уже стало классическим в соответствующих учебных пособиях. Не будем делать исключений
п мы.
для а мвола А в грамматике G о редслепо более одного правила, то процедура разбора ищет среди них правило вида А->ау aeVT g(VNuVT)', ервый сим вол праной части которого совпадал бы с текущим символом входной цепочки а = сц Если такого правила по найдено, то цепочка нс принимается Иначе (если найдено правило А->ау или для символа А в грамматик G существует только одно правило А—>у) то запоминается номер правила, и когда а = а, то считывающая головка передвигается (увеличивается i) а для каждого нетерминального символа в цепочке у рекурсивно вызывается процедура разбора этого символа.
Название метода происходит из реализации алгоритма, которая заключается в последовательности рекурсивных вызовов процедур разбора Для начала разбора входной цепочки нужно вызвать процедуру для символа S с параметром i -1.
Условия применимое! и метода можно получить из описания самого алгоритма в грамма ке G(VN,VT Р S) VA VN i озможны то н> о два арп i та пра вил:
А->у, у (VNuVT)* и это единственное правило для А;
A—>a1p,|a2P2l—lanPn, Vl a gVT, р e(VNuVT)* и если i*j, то а,*а
Этим условиям удовлетворяет незначительное количество реальных грамматик. Это достаточные, но не необходимые условия. Если грамматика не удовлетворяет этим условиям, еще не значит, что заданный ею язык не может распознаваться с помощью метода рекурсивного спуска. Возможно, над грамматикой просто необходимо выполнить ряд дополнительных преобразовании.
К сожалению, не существует алгоритма, который бы позволил преобразовать произвольную КС-грамматику к указанному выше виду, равно как не существу- ч ет и алгоритма, который бы позволял проверить, возможны ли такого рода преобразования. 'Го есть для произвольной КС-грамматики нельзя сказать, анализируема ли она методом рекурсивного спуска или нет.
Можно рекомендовать ряд преобразовании, которые способствуют приведению грамматики к требуемому виду, но не гарантируют его достижения.
Исключение ?^-правил
Исключение левой рекурсии.
Добавление новых нетерминальных символов.
Например
если правило: Л->аа,|аа2 |аа, Ь^ЬгР | bmpn„
то заменяем его на два: А—>аА’| b^jbjijl |bmPm и А->а||а2|...|ап.
. Замена нетерминальных символов в правилах на цепочки их выводов.
Например
если имеются правила:
A->B1|B2|...|B„|blpI|b2p2|...!bmpm.
В|^«| а|2|. |aik,
Bn->anl|an2|...|anp.
заменяем первое правило на
A—>an|a12|...|alk |...|а111|ап2|...|апр|Ь1р1|Ь2р^
В целом алгоритм рекурсивного спуска эффективен и прост в реализации, по имеет очень ограниченную применимость.
Пример реализации метода рекурсивного спуска
Дана грамматика G({a,b,c},{A B,C,S},P,S):
Р:
S -> аА | ЬВ
А —> а | ЬА | сС
В -> b I аВ I сС
С -> АаВЬ
Необходимо построить распознаватель, работающий по методу рекурсивного спуска.
Видно, что грамматика удовлетворяет условиям, необходимым для построения такого распознавателя.
Напишем процедуры на языке программирования С, которые будут обеспечивать разбор входных цепочек языка, заданного данной грамматикой. Согласно алгоритму, необходимо построить процедуру разбора для каждого нетерминального символа грамматики, поэтому дадим процедурам соответствующие наименования. Входные данные для процедур разбора будут следующие:
цепочка входных символов;
положение указателя (считывающей головки МП-автомата) во входной цепочке;
массив для записи номеров примененных правил;
порядковый номер очередного правила в массиве.
Результатом работы каждой процедуры может быть число, отличное от пуля («истина»), или 0 («ложь»), В первом случае входная цепочка символов принимается распознавателем, во втором случае — не принимается. Для удобства реализации в том случае, если цепочка принимается распознавателем, будем возвращать текущее положение указателя в цепочке. Кроме того, потребуется еще одна дополнительная процедура для ведения записей в массиве последовательности правил (назовем ее WnteRules\
void WriteRulesdnt* piRul mt* iP. int iRule)
{
piRul[*iP] - IRule:
*iP - *1P + 1:
)
int proc_S (char* szS. int IN. int* piRul. int* iP)
{
switch (szSCINJ)
< case 'а'
WriteRules(piRul JP.1):
return proc_A(szS.1N+l.piRul.IP); case ‘b’:
Hr teRulestpiRul iP.2):
return proc_B(szS. 1 N+l .pi RuL i P):
) return 0:
)
int proc_A (char* szS. mt IN. int* piRul. int* IP)
<"
switch (szS[1N])
< case "a":
WnteRulestp Rul.iP.3):
return iN+1 case "b":
WriteRules(piRul .1P.4):
return proc_A(szS.iN+l.piRul IP): case ’c':
WriteRules(p1Rul.IP.5):
return proc C(szS.iN+1.piRul,1P);
)
return 0:
)
int proc_B (char* szS int IN int* piRul. int* 1P) (
switch (szSEiN])
< case "b":
WriteRules(piRul,iP.6): return iN+1; case 'a':
WriteRules(piRul JP.7);
return proc_B(szS.iN+l.p Rul.iP); case ‘c’:
WriteRules(piRul.iP B)
return proc_B(szS.1N+l.pi Rui.i P):
) return 0.
}
i it proc C (char* szS, mt iN int* piRul. int* iP) { int i:
WriteRules(piRul,1Р.9):
1 - proc A(szS.iN.p1Rul .1Р):
if (i -- 0) return 0:
if (szS[i] i- 'a') return 0: i++:
- proc_B(szS.i piRul.iP):
if (i == 0) return 0:
if (szS[i] !- 'b") return 0. return i+1
}
Теперь для распознавания входной цепочки необходимо иметь целочисленный массив Rules достаточного объема для хранения номеров правил. Тогда работа распознавателя заключается в вызове процедуры proc_S(Str,0.Rules,&N), где Str — это входная цепочка символов, N — переменная для запоминания количества примененных правил (первоначально N - 0). Затем 1ребуется обработка полученного результата: если результат па 1 превышает длину цепочки — цепочка принята, иначе — цепочка не принята. В первом случае в массиве Rules будем иметь последовательность номеров правил грамматики, необходимых для вывода цепочки, а в переменной N количество этих правил. На основе этой цепочки можно легко построить дерево вывода.
Обьем массива Rules заранее нс известен, так как заранее не известно количество шагов вывода. Чтобы избежать проблем с недостаточным объемом статического массива, приведенные выше процедуры распознавателя можно модифицировать так, чтобы они работали с динамическим распределением памяти (изменив процедуру WriteRules и тип параметра piRul в вызовах остальных процедур). На логику работы распознавателя это никак не повлияет. Следует помнить также, что метод рекурсивного спуска основан па рекурсивном вызове множества процедур, что при значительной длине входной цепочки символов может потребовать соответствующего объема стека вызовов для хранения адресов процедур, их параметров и локальных переменных (более подробно об этом можно посмотреть в данном пособии в разделе «Семантический анализ и подготовка к генерации кода», глава 14).
Из приведенного примера видно, что алгоритм рекурсивного спуска удобен и прост в реализации. Главным препятствием в его применении является то, что класс грамматик, допускающих разбор на основе этого алгоритма, сильно ограничен.
Расширенное применение распознавателей на основе метода рекурсивного спуска
Метод рекурсивного спуска позволяет выбрать альтернативу, ориентируясь па текущей символ входной цепочки, обозреваемый считывающей головкой МП- авюмата. Если имеется возможность просматривать не один, а несколько символов вперед от текущего положения считывающей головки, то можно расширить область применимости метода рекурсивного спуска. В этом случае уже можно искать правила на основе некоторого терминального символа, входящего в правую часть правила. Естественно, и в таком варианте выбор должен быть однознач-
ным — для каждого нетерминального символа в левой части правила необходимо, чтобы в правой част и правила не всгречалось двух одинаковых терминальных символов.
Этот метод требует также анализа тина присутствующей в правилах рекурсии, поскольку один и ют же герминальный символ может встречаться во входной строке несколько раз, и в Зависимости от тина рекурсии следует искать его крайнее левое или крайнее правое вхождение в строке.
Рассмотрим грамматику арифметических выражений без скобок для символов а и bG({+.-./.*.a.b}, {S.T.E}, Р, S):
Р:
S -> S+T | S-Т | Т
Т -> Т*Е I Т/Е I Е
Е -> (S) | а | Ь
Это I раммагнка для арифметических выражений, которая уже была рассмотрена в разделе «Проблемы однозначности и эквивалентности грамматик», глава 9 и служила основой для построения распознавателей в разделе «Распознаватели КС-языков с возвратом», глава 11.
Запишем правила этой грамматики в форме с применением метасимволов. Получим:
Р:
S -> Т {+T.-D)
Т -► Е{(*Е./Е))
Е -> (S) | а | b
При такой форме записи процедура разбора для каждого нетерминального символа становится тривиальной
Для символа S распознаваемая строка должна всегда начинаться со строки, допустимой для символа Т за которой может следовать любое количество символов + или , и если они найдены, то за ними опять должна быть строка, допусти мая для символа Т. Аналогично, для символа Т распознаваемая строка должна всегда начинаться со строки, допустимой для символа Е, за которой может следовать любое количество символов * или /, и если они найдены, то за ними опять должна быть строка, допустимая для символа Е. С другой стороны, для символа Е строка должна начинаться строго с символов С а или Ь, причем в нервом случае за ( должна следовать строка, допустимая для символа S, а за ней — обя- затеплю символ ).
Исходя из этого, построены процедуры разбора входной строки на языке Pascal (используется Borland Pascal или Borland Delphi, которые допускают тип str ing — строка). Входными данными для них являются:
исходная рока символов;
текущее положение указателя в исходной строке;
длина исходной строки (в принципе, этот параметр можно опустить, но он введен для удобства);
резу илирующая строка правил
Процедуры построены так, что в результирующую строку правил помещаются номера примененных правил в строковом формате, перечисленные через запятую (,). Правила номеруются в грамматике, записанной в форме Бэкуса— Наура, в порядке слева направо и сверху вниз (всего в исходной грамматике 9 правил). Распознаватель строит левосторонний вывод, поэтому на основе строки номеров правил всегда можно получить цепочку вывода или дерево вывода.
Для начала разбора нужно вызвать процедуру proc_S(S,l,N,Pr), где S — входная строка символов; N — длина входной строки (в языке Borland Pascal вместо N можно взять Length(S)); Рг — строка, куда будет помещена последовательность примененных правил
Результатом proc_S(S,l.N.Pr) выполнения будет N+1, если строка S принимается, и некоторое число, меньшее N+1, если строка не принимается. Если строка S принимается то строка Рг будет содержать последовательность номеров правил, которые необходимо применить для того, чтобы вывести S.
procedure proc_S (S: string: in integer var pr: string): integer var si : string begin
i :- proc_T(S.i n.sl)
if i > 0 then
begin
pr :- 3.' + si
while (1 <= n) and (i о 0) do
case S[i] of
begin
if i - n then i - 0
else
begin
i - proc_T(S.i+l.n.sl)
'pr :- '1.' + pr + si
end:
end:
'-': begin
if i = n then i :- 0
else
begin
i - proc_T(S,i+1.n.sl):
pr = '2.' + pr +',si
end:
end:
else break
end:(case)
end:{if}
proc_S - i:
end:
procedure proc_S (S: string i.n: integer: var pr string). Integer var si string:
begin
i - proc_E(S.1.n.sl)
if 1 > 0 then
begin
pr :- "6.' * si:
while (i <- n) and (1 <> 0) do
case S[i] of
begin
if 1 - n then i :- 0
else begin
1 ;- proc_E(S.i+l.n.sl);
pr - '4. ‘ + pr *'.' + si: end;
end
V. begin
if 1 = n then 1 - 0
else
begin
:- proc_E(S.i+l.n.sl):
/pr - "5.‘ + pr +".'+ si;
end:
end:
else break;
end:{case}
‘ end,{if}
proc_S :- i;
end.
procedure proc_E (S: string: i.n integer; var pr: string), integer var si ; string: begin
case S[1] of
'a' begin
pr :- '8';
procE :- i+l:
end;
'b': begin
рг :- '9':
ргос_Е •- 1*1:
end:
' С: begin
proc_E := 0: if 1 < п then begin
i :- proc_S(S.i+l.n.sl)
f (i > 0) .and (i < n) then begin
pr := '7.' + si:
if S[i] - ')' then procE :- i+l end:
end:
end;
else proc_E = 0: end:(case)
end:
Конечно, н в данном случае алгоритм рекурсивного спуска позволил построить доста точно простой распознаватель, однако, прежде чем удалось его применить, потребовался неформальный анализ правил грамматики. Далеко пе всегда такого рода неформальный анализ являеп я возможным, особенно если грамматика содержит десятки и даже сотни различных правил — человек не всегда в состоянии уловить их смысл и взаимосвязь. Поэтому расширения алгоритма рекурсивного спуска, хотя просты и удобны, по не всегда применимы. Даже понят!, сам факт того, можно или нет в заданной трамматикс построить такого рода распознаватель, бывает очень непросто [15, 26, 74].
Далее будут рассмотрены распознаватели и алгоритмы, которые основаны на строго формальном подходе. Опи предваряют построение распознавателя рядом Обоснованных действий н преобразований, с помощью которых иодт отапливаются необходимые исходные данные. Все такого рода действия пост роены на основе операций над множествами и символами, а исходными данными для них служат множества символов и правил исходной грамматики. В »том случае подготовку всех исходных данных для распознавателя можно формализовать и автоматизировать вне зависимости от объема грамматики (количества правил и символов в ней) Эти предварительные действия можно выполнить па компьютере, в то время как расширенная трактовка рекурсивного спуска предполагает неформальный анализ грамматики человеком, и в этом серьезный недостаток метода.
Определение 1_1_(к)-грамматики
Логическим продолжением идеи, положенной в основу метода рекурсивного спуска, является предложение использовать для выбора одной из множества альтернатив нс один, а несколько символов входной цепочки. Однако напрямую пере-
дожить алгоритм выбора альтернативы доя одного символа па такой же алгоритм для цепочки символов не удастся два соседних символа в цепочке па самом деле могу г быть выведены с мсти ьзованием различных правил грамматики, поэтому неверным будет напрямую ис кать их в одном правиле. Тем нс менее существует класс грамматик, основанный именно на этом принципе — выборе одной альтернативы из множества возможных па основе нескольких очередных символов в цепочке. Это так называемые кк(к)-грамматики. Правда, алгоритм работы распознавателя для них не так очевидно прост, как рассмотренный выше алгоритм рекурсивного спуска.
Грамматика обладает свойством kk(k), к > 0, если па каждом шаге вывода для однозначного выбора очередной альтернативы МП-автомату достаточно знать символ на верхушке стека и рассмотреть первые к символов от текущего положения считывающей головки во входной цепочке символов.
Грамматика называется ЕЬ(к)-грамлштикой, если она обладает свойством kk(k) для некоторого к > О.
Название «LL(k)» несет определенный смысл. Первая литера «к» происходит от слова «left» и означает, что входная цепочка символов читается в направлении слева направо. Вторая литера «к» также происходит от слова «left» и означает, что при работе распознавателя используется левосторонний вывод. Вместо «к» в названии класса грамматики стоит некоторое число, которое показывает, сколько символов надо рассмотреть, чтобы однозначно выбрать одну из множества альтернатив. Так, существуют кк(1)-грамматики, кк(2)-грамматнки и другие классы.
В совокупности все кк(к)-грамматикн для всех к>0 образуют класс кк-грам- матик
На рис. 12.1 схематично показано частичное дерево вывода доя некоторой kk(k)- грамматнки. В нем со обозначает уже разобранную часть входной цепочки а, которая цостроеца на ос пове левой части дерева у. Правая часть дерева х — это еще rte разобранная часть, а А — текущий нетерминальный символ на верхушке стека МП-автомата. Цепочка х представляет собой незавершенную часть цепочки вывода, содержащую как терминальные, так и нетерминальные символы. После завершения вывода символ А раскрывается в часть входной цепочки и, а правая часть дерева х преобразуется в часть входной цепочки т. Свойство kk(k) предполагает, что однозначный выбор альтернативы доя символа А может быть сделан на основе к первых символов цепочки от, являющейся частью входной цепочки а.
Алгоритм разбора входных цепочек для кк(к)-грамматики носит название «к- прсдсказывающсго алгоритма». Принципы его выполнения во многом соответствую г функционированию МП-автомата с тон разницей, что на каждом шаге ра-
боты этот алгоритм может просматривать к символов вперед от текущего положения считывающей головки автомата.
Рис. 12.1. Схема построения дерева вывода для Щк)-грамматики
Для Ы.(к)-грамматик известны следующие полезные свойства:
всякая Ы.(к)-грамматика для любого к>0 является однозначной;
существует алгоритм, позволяющий проверить, является ли заданная грамматика ЬЦк)-грамматикой для строго определенного числа к.
Кроме того, известно, что все грамматики, допускающие разбор по методу рекурсивного спуска, являются подклассом 1.Ь(1)-грамматик. 'Го есть любая грамматика, допускающая разбор по методу рекурсивного спуска, является LL( ^-грамматикой (но не наоборот!).
Есть, однако, неразрешимые проблемы для произвольных КС-грамматик:
не существует алгоритма, который бы мог проверить, является ли заданная КС-грамматика ЕЬ(к)-грамматикоп для некоторого произвольного числа к;
не существует алгоритма, который бы мог преобразовать произвольную КС- грамматику к виду ЬЦк)-грамматики для некоторого к (или доказать, что преобразование невозможно).
Это несколько ограничивает применимость ЬЦк)-грамматик, поскольку не всегда для произвольной КС-грамматики можно очевидно найти число к, для которого опа является ЬЦк)-грамматикой, или узнать, существует ли вообще для нее такое число к.
Для Щк)-грамматики при к>1 совсем не обязательно, чтобы все правые части правил грамматики для каждого нетерминального символа начинались с к различных терминальных символов. Принципы распознавания предложений входного языка такой грамматики накладывают менее жесткие ограничения на правила грамматики, поскольку к соседних символов, по которым однозначно выбирается очередная альтернатива, могут встречаться и в нескольких правилах грамматики (эти условия рассмотрены ниже). Грамматики, у которых все правые части правил для всех нетерминальных символов начинаются с к различных терминальных символов, носят название «сильно ЬЦк)-грамматик». Метод построения распознавателей для них достаточно прост, алгоритм разбора очевиден, но, к сожалению, такие грамматики встречаются крайне редко.
Для LL(1)-грамматики, очевидно, для каждого нетерминального символа не может быть двух правил, начинающихся с одного и того же терминального символа. Однако это менее жесткое условие, чем то, которое накладывает распознаватель по методу рекурсивного спуска, поскольку в принципе Щ1)-грамматика допускает в правой частя правил цепочки, начинающиеся с нетерминальных символов, а также Х-правила. ЕЦ1)-граммагпки позволяют построить достаточно простой и эффективный распознаватель, поэтому они рассматриваются далее отдельно в соответствующем разделе.
Поскольку все И.(к)-грамматпки используют левосторонний нисходящий распознаватель, основанный на алгоритме с подбором альтернатив, очевидно, что они не могут допускать левую рекурсию. Поэтому никакая леворекурсивная грамматика не может быть LL-грамматикой. Следовательно, первым делом при попытке преобразовать грамматику к виду LL-грамматики необходимо устранить в ней левую рекурсию (соответствующий алгоритм был рассмотрен выше)
Класс LL-грамматик широк, но все же он недостаточен для того, чтобы покрыть все возможные синтаксические конструкции в языках программирования (к ним относим все детерминированные КС-языки). Известно, что существуют детерминированные КС-языки, которые нс могут быть заданы Ьк(к)-грамматнкой ни для каких к. Однако LL-грамматики удобны для использования, поскольку позволяют построить распознаватели с линейными характеристиками (линейной зависимостью требуемых для работы алгоритма распознавания вычислительных ресурсов от длины входной цепочки символов).
Принципы построения распознавателей для Щк)-грамматик
Для построения распознавателей LL(k) грамматик используются два важных множества, определяемые следующим образом
FIRST(k.a) —■ множество терминальных цепочек, выводимых из ae(VToVN)', укороченных до к символов;
FOLLOW(k.A) — множество укороченных до к символов терминальных цепочек, которые могут следовать непосредственно за Ае VN в цепочках вывода.
Формально эти два множества могут быть определены следующим образом:
FIRST(k.a) = {ojgVT | либо |со|<к и a=>*w, либо |о>|>к и a=>*mx, xg(VTUVN)’} aG(VTuVN)*, к>0.
FOLLOW(k,A) - {toGVT*| S=>*aAy и weFIRST(y,k), aeVT}, AeVN k>0.
Очевидно, что если имеется цепочка терминальных символов aeVT, то FIRST(k,a) — это первые к символов цепочки а.
Доказано, что грамматика G(VT,VN,P,S) является LL(k )-грамматикой тогда и только тогда, когда выполняется следующее условие: V А->р g Р н V А-» ->g g Р (р^): FIRST(k,pw) n FIRST(k.yto) = 0 для всех цепочек <о таких, что S =>* аАш.
Иначе говоря, если существуют дне цепочки вывода:
S =>* аАу => агу =>* аю
S =>* аАу => aty аи
то из условия FIRST(k,a>) - FIRST(k,u) следует, что г = L
На основе этих двух множеств строится алгоритм работы распознавателя для КЦк)-грамматик, который представляет собой к предсказывающий алгоритм для МП-автомата, заданного так: R({q},VT,Z,5,q,S,{q}), где Z - VTuVN, aS — целевой символ грамматики G Функция переходов автомата строится на основе управляющей таблицы М, которая отображает множество (Zu{X)) х VTk на множество, состоящее из следующих элементов:
пар вида (РД), где р — цепочка символов, помещаемая автоматом на верхушку стека, a i — номер правила вида А->Р, AeVN, peZ’;
«выброс»;
«допуск»;
«ошибка».
Конфигурацию распознавателя можно отобразить в виде конфигурации МП- автомата с дополнением цепочки л, в которую помещаются номера примененных правил. Поскольку автомат имеет только одно состояние, его в конфигурации! можно не указывать. Если считать, что X — это символ па верхушке стека автомата, a — непрочитанная автоматом часть входной цепочки символов, а и = FIRST(k,a), то работу алгоритма распознавателя можно представить следующим образом:
(а, Ху, л) -г (а, Ру, ni), yeZ’, если М(Х,и) - (РД);
(а, Ху, л) -ь (а’, у, л), если X = aeVT и а - aa', М(а,и) - «выброс»;
(X, X, л) — завершение работы, при этом М(Х,Х) = «допуск»;
иначе — «ошибка».
Цепочка и - FIRST(k,a) носит в работе автомата название «аваицепочка».
Таким образом, для создания алгоритма распознавателя языка, заданного произвольной ЕЦк)-грамматикой, достаточно уметь построить управляющую таблицу М Управляющую таблицу М, а также множества FIRST и FOLLOW можно получить на основе правил исходной грамматики. Методы построения этой таблицы для к>1 в данном пособии не рассматриваются, с ними можно ознакомиться в работах [5, 6, т. 1, 65].
При k - 1 все существенно проще. Методы построения для ЕЕ(1)-грамматик, а также проверки принадлежности грамматики к классу ЕЕ(1)-грамматик рассмотрены ниже.
Алгоритм разбора для Ц_(1)-грамматик
Для ЕЕ(1)-грамматик алгоритм работы распознавателя предельно прост. Он заключается всего в двух условиях, проверяемых на шаге выбора альтернативы. Исходными данными для этих условий являются символ aeVT, обозревае-
мый считывающей головкой МП-автомата (текущий символ входной цепочки), и символ AeVN, находящийся на верхушке стека автомата*.
Эти условия можно сформулировать так:
необходимо выбрать в качестве альтернативы правило А~>х, если aeFIRST(l,x);
О необходимо выбрать в качестве альтернативы правило А—>Х, если aeFOLLOW(l.A).
Если ни одно из этих условий не выполняется (нет соответствующих правил), то цепочка не принадлежит заданному языку и МП-автомат не принимает ее (алгоритм должен сигнализировать об ошибке).
Работа автомата на шаге «выброса» остается без изменений.
Кроме того, чтобы убедиться, является ли заданная грамматика G(VT,VN,P,S) ЕЕ(1)-грамматикой, необходимо и достаточно проверить следующее условие: для каждого символа AeVN, для которого в грамматике существует более одного правила вида А -> ai|a2|...|an, должно выполняться требование
FIRST(l,ajFOLLOW(l,A)) n FIRST(l,ajFOLLOW(l,A)) - 0
Vi*j, n > i > 0, n > j > 0
Очевидно, что если для символа AeVN отсутствует правило вида А->Х, то согласно этому требованию все множества FIRST(la,), FIRST(l,a2), ..., FIRST(l,an) должны попарно не пересекаться, если же присутствует правило А—>Х, то они не должны также пересекаться со множеством FOLLOW(l.A). Отсюда видно, что ЕЕ(1)-грамматика не может содержать для одного и того же нетерминального символа AeVN двух правил начинающихся с одною и того же терминального символа.
словие, накладываемое на правила ЬЦ1)-грамматики, является довольно жестким Очень немногие реальные грамматики могут быть отнесены к классу LL(1)- грамматик. Например, даже довольно простая грамматика G({a},{S}, {S->a|aS}, S) не удовлетворяет этому условию (хотя она является LL(2)-грамматикой и даже регулярной праволинейной грамматикой)
Иногда удается преобразовать правила грамматики так, чтобы они удовлетворяли требованию ЕЕ(1)-грамматик. Например, приведенная выше грамматика мо-
Может показаться, что класс ЕЕ(1)-грамматик соответствует классу грамматик, анализируемых методом рекурсивного спуска, где выбор альтернативы также основан на текущем символе входной цепочки. На самом деле это не так — класс ЬЦ1)-грамматик является более широким, чем класс грамматик, анализируемым методом рекурсивного спуска. Любая грамматика, анализируемая методом рекурсивного спуска, является ЕЬ(1)-грамматикой, но не наоборот: существуют ЬЦ1)-грамматики, которые напрямую методом рекурсивного спуска не анализируются. Дело в том, что распознаватель на базе LL( 1 )-грамматики (который основан на множествах FIRST и FOLLOW) при выборе альтернативы фактически анализирует не одно, а сразу несколько правил, связанных с текущим символом на верхушке стека, в то время как рекурсивный спуск основан на анализе только одного правила, непосредственно относящегося к этому символу.
жст быть преобразована к виду G'({a),(S,A}, {S->aA, A->X|S}, S)*. В такой форме она уже является LL(1) грамматикой (эго можно проверить). Но формального метода преобразовать произвольную КС-грамматику к виду ЕЦ1)-грамматпки или убедиться в том, что такое преобразование невозможно, не существует. Первое преобразование правил грамматики, которое можно рекомендовать, — устранение левой рекурсии2. Второе преобразование носит название «левая факторизация», оно уже было упомянуто выше при знакомстве с методом рекурсивного спуска. Это преобразование заключается в следующем: если для символа AeVN существует ряд правил
А-^apMl...lapJyM^ Vi: p,e(VTuVN)*, Vj: ^(VTuVNy, aeVT
и ни одна цепочка символов у, не начинается с символа а, тогда во множест во нетерминальных символов грамматики VN добавляется новый символ А', а правила для А и А' записываются следующим образом: А—>аА’|у1|у2|—|у,„ и A’->pJp2|... р„. Левую факторизацию можно применять к правилам грамматики несколько раз с целью исключить для каждого нетерминального символа правила, начинающие ся с одних и тех же терминальных символов. Однако применение этих двух преобразований отнюдь не гарантирует, что произвольную КС-грамматику удастся привести к виду ЬЦ1)-грамматики.
Для того чтобы запрограммировать работу МП-автомата, выполняющего разбор входных цепочек символов языка, заданного КЬ(1)-грамматикой, надо научиться строить множества символов FIRST(l,x) и FOLLOW(l.A). Для множества FIRST(1 х) все очевидно, если цепочка х начинается с терминального символа, если же опа начинается с нетерминального символа В (х = By, xg(VTuVN)<, ye(VTuVN)'), то FIRST(1,x) = FlRST(l.B). Следовательно, для ЬЦ1)-грамматик остается только найти алгоритм построения множеств FIRST(1,B) и FOLLOW(l.A) для всех нетерминальных символов A,BgVN
Исходными данными для этих алгоритмов служат правила грамматики.
Алгоритм построения множества FIRST(1,A)
Алгоритм строит множества FIRST(1,A) сразу для всех нетерминальных символов грамматики G(VT,VN,P,S), AeVN. Для выполнения алгоритма надо предварительно преобразовать исходную грамматику G(VT,VN,P,S) в грамматику G'(VT,VN',P’,S'), не содержащую Х-пранил (см. алгоритм преобразования в разделе «Преобразование КС-грамматик. Приведенные грамматики», глава И). Па основании полученной грамматики G' и выполняется построение множеств FIRST(l.A) для всех AeVN (если AeVN, то согласно алгоритму преобравова-
Можпо убедиться, что две приведенные грамматики задают один и тог же язык: L(G) - = L(G')- Это легко сделать, поскольку обе они являются нс только КС-грамматиками, ио и регулярными праволпнейными грамматиками. Кроме того, формальное преобразование G' в G существует — достаточно устранить в грамматике G' Х-иравила и ценные ира вила, и будет получена исходная грамматика G. А вот формального преобразования G в G' пет. В общем случае все может быть гораздо сложнее.
1 Устранение левой рекурсии — это, конечно, необходимое, ио не достаточное условие для преобразования грамматики к виду LL-грамматики Это будет видно далее из примера
ния также оправедливо AeVN'). Множес тва строятся методом последовательного приближения. Если в результате преобразования грамматики G в грамматику С множество VN’ содержит новый символ S', то при построении множества FIRST(1,A) он не учитывается.
Алгоритм состоит из нескольких шагов.
Шаг 1. Для всех AeVN FIRSTO(1,A) = {X | А->Ха е Р Xe(VTuVN), ae(VIWN)’} (первоначально вносим во множество первых символов для каждого нетерминального символа А все символы, стоящие в начале правых частей правил для этого символа А); й-О.
Шаг 2. Для всех AeVN: FIRSTi+l(l,A) - FIRST^LA) и FIRST,(1,В), для всех нетерминальных символов Be(FlRSTj(l,A) п VN).
Шаг 3. Если 3 AeVN: FIRSTiH(l,A) * FIRSTj(l,A), то i:=i+l и вернуться к шагу 2, иначе перейти к шагу 4.
Шаг 4. Для всех AeVN: FIRST(1,A) — FIRSTj(l,A) \ VN (исключаем из постро- еппых множеств все нетерминальные символы).
Алгоритм построения множества FOLLOW(1,A)
Алгоритм строит множества FOLLOW(LA) сразу для всех нетерминальных символов грамматики G(VT,VN,P,S), AeVN. Для выполнения алгоритма предварительно надо построить все множества FIRST(1,A), V AeVN. Множества строятся методом последовательного приближения. Алгоритм состоит из нескольких шагов.
Шаг 1. Для всех AeVN: FOLLOW0(1,A) = {X | 3 В-мхАХр е Р, BeVN, Xe(VToVN), a,Pe(VT<jVN)‘} (первоначально вносим во множество последующих символов для каждого нетерминального символа А все символы, которые в правых частях правил встречаются непосредственно за символом A); й-0.
Шаг 2. FOLLOW0(1,S) = FOLLOW0(1,S) kj {X} (вносим пустую цепочку во множество последующих символов для целевого символа S — это означает, что в конце разбора за целевым символом цепочка кончается, иногда для этой цели используется специальный символ конца цепочки ±к).
Шаг 3. Для всех AeVN: FOLLOW; (l.A) = FOLLOW^LA) u F1RST(1,B), для всех нетерминальных символов Be(FOLLOW,(l,A) n VN)
Шаг 4. Для всех AeVN: FOLLOW", (1,A) = FOLLOW'LL A) и FOLLOW'i(l,B), для всех нетерминальных символов Be(FOLLOW’,(l,A) n VN) и существует правило В->Х.
Шаг 5. Для всех AeVN: FOLLOW|+1(1,A) = FOLLOW'^!,А) и FOLLOW",(1,B), для всех нетерминальных символов BeVN, если существует правило В->аА, ae(VTuVN)*
Шаг 6. Если 3 AeVN: FOLLOW,,,(1,А) * FOLLOW^l.A), то i: i+1 и вернуться к шагу 3, иначе перейти к шагу 7.
Шаг 7. Для всех AeVN: FOLLOW(l.A) FOLLOWj(l,A) \ VN (исключаем из построенных множеств все нетерминальные символы).
Пример построения распознавателя для 1_1_(1)-грамматики
Рассмотрим в качестве примера грамматику G({+.-,/.*.a,b}, {S.R.T.F.E}, Р, S) с правилами
Р:
S -> Т | TR
R -^ +Т | -Т | +TR | -TR
Т -> Е | EF
F -> *Е | /Е | *EF | /EF
Е —> (SJ | а | Ь
Это пелеворекурсивная грамматика для арифметических выражений (ранее она была построена в разделе «Распознаватели КС-языков с возвратом», глава 11).
Эта грамматика не является ЬЦ1)-грамматикой Чтобы убедиться в этом, достаточно обратить внимание на правила для символов R и F — для них имеется по два правила, начинающихся с одного и того же терминального символа.
Преобразуем ее в другой вид, добавив Х-правила. В результате получим новую грамматику G’({+.-./.*.a.b}. {S,R,T,F,E}, Р', S) с правилами:
Р":
S -> TR
R -> X | +TR | -TR
Т -» EF
F -х X | *EF | /EF
Е -> (S) | а | b
Построенная грамматика G эквивалентна исходной грамматике G В этом можно убедиться, если воспользоваться алгоритмом устранения Х-правил из раздела «Преобразование КС-грамматик. Приведенные грамматики», глава И. Применив его к грамматике G" получим грамматику G, а по условиям данного алгоритма L(G') - L(G). Таким образом, мы получили эквивалентную грамматику, хотя она и построена неформальным методом (следует помнить, что не существует формального алгоритма, преобразующего произвольную КС-грамматику в LL(k)- грамматику для заданного к).
Эта грамматика является LL(1/грамматикой. Чтобы убедиться в этом, построим множества FIRST и FOLLOW для нетерминальных символов этой грамматики (поскольку речь заведомо идет об LL(1)-грамматике, цифру «1» в обозначении множеств опустим для сокращения записи).
Для построения множества FIRST будем использовать исходную грамматику G, так как именно она получается из G' при устранении Х-правил.
Построение множеств FIRST.
Шаг. 1. FIRST0(S) - {Т};
FlRSTo(R) - {+,-};
FIRST0(T) = {Е};
FIRST0(F)-{*,/};
FIRST^E) = {(.a.b); i-0.
Шаг 2.
FIRST,(S) - {Т,Е};
FIRSTAR) = {+,-};
FIRST/T) = {E,(,a,b};
F1RSTXF) = {*/};
FIRST,(E) - {(.a,b)
Шаг 3.
Шаг 2.
i = 1 возвращаемся к шагу 2.
FIRST2(S) - {T.E,(.a,b};
FIRSTAR) -{+,-};
FIRST2(T) - {E,(,a,b};
F1RST/F)-{*./},
FIRST2(E) - {(.a.b)
ШагЗ.
Шаг 2.
i = 2, возвращаемся к шагу 2.
FIRST3(S) = {T,E,(,a,b};
FIRSTAR) = {+,-};
FIRST,(T) = {E,(,a.bb Fl RST,(F) = {*,/};
FIRST3(E) - {(,a b)
Шаг 3.
Шаг 4.
i = 2, переходим к шагу' 4. FIRST(S) = {(,a b}, FIRST(R) = {+,-};
FIRST(T) - {(,a,b};
FIRST(F) = {*,/};
FIRST(E) = {(,a,b}.
Построение закопчено.
Построение множества FOLLOW.
Шаг 1.
FOLLOW0(S) = {)};
LOLLOWo(R) = 0;
FOLLOW0(T) - {R};
FOLLOW0(F) - 0 FOLLOW„(E) - {F}; i-0.
Шаг 2
FOLLOW0(S) - {),X};
FOLLOWER) - 0; FOLLOW^T) - {R}, FOLLOW0(F) = 0: FOLLOW0(E) {F}
Шаг 3. FOLLOW'o(S) - {),Х}; FOLLOWER) = 0, FOLLOW'0(T) - {R,+,-}; FOLLOWo(F) = 0;
FOLLOW0(E) - {F,*,/}-
Шаг 4. FOLLOW"0(S) = {),X};
FOLLOWER) = 0; FOLLOWoCT) - {R,+-}; FOLLOW"0(F) - 0;
FOLLOW "0(E) = {F,*,/}.
Шаг 5. FOLLOW,(S) - {).X}; FOLLOWER) = ()Л); FOLLOW,(T) - {R.+,-); FOLLOW,(F) - (R,+,-}; FOLLOW,(E) “ {F,*,/}.
Шаг 6. i = 1, возвращаемся к шагу 3.
Шаг 3FOLLOW',(S) - {),X};
FOLLOWER) - 0,X); FOLLOW’1(T)-{R,+ -}; FOLLOW’,(F) - {R.+.-}; FOLLOW’,(E) = {F,*,/}.
Шаг 4. FOLLOW ”,(S) = {),X>; FOLLOW”,(R) - {),X}; FOLLOW",(T) = {R,+.-,),X}; FOLLOW",(F) - {R,+,-,),X}; FOLLOW ”|(E) - (F,R *,/,+,-,)A}.
Шаг 5. FOLLOW2(S) - {).X};
FOLLOW2(R) = D,X};
FOLLOW2(T) = {R,+.-,),X};
FOLLOW2(F) - {R.+,-,).X};
FOLLOW2(E) - {F R,*,/,+-,),x}.
Шаг 6. i - 2, возвращаемся к шагу 3.
Шаг 3 FOLLOW .AS) - {),X};
FOLLOWER) = {),X};
FOLLOW’2(T) = {R,+,-,),X};
FOLLOW'2(F) = {R,+,-,),X};
FOLLOW'2(E) - {F,R,*,/,+-,),X}.
Шаг 4 FOLLOW"2(S) - {),Х};
FOLLOWER) - ОД};
FOLLOW"2(T) - {R,+,-,),X};
FOLLOW2(F) - {R,+,-,),X};
FOLLOW"t(E) - {F.R,*,/,+,-.)A}
Шаг 5. FOLLOW^S) - (),Х);
FOLLOW3(R) - (),Х);
FOLLOW3(T) - (R.+,-,)Д};
FOLLOW3(F) - {R.+.-)Л};
FOLLOW3(E) - {F.R,*,/,+,-,),Х).
Шаг 6. i — 2, переходим к шагу 7
Шаг 7. FOLLOW(S) - {)Х};
FOLLOW(R) - ОЛ};
FOLLOW(T) - {+,-,),Х};
FOLLOW(F) - {+, ,),Х};
FOLLOW(E) - {•,/,+,-,),Х}.
Построение закончено.
В результате выполненных построений можно видеть, что необходимое и достаточное условие принадлежности КС-грамматики к классу LL( 1 )-грамматик выполняется.
Построенные множества FIRST и FOLLOW можно представить в виде таблицы.
Результат выполненных построений отражает табл. 12.1.
Таблица 12 1 Множества FIRST и FOLLOW для грамматики G‘
Символ A VN
FIRST(1,A)
FOLLOW(1,A)
S
(* ь
)Х
R
+ -
)Х
Г
( а b
)Х
F
*/
_+- ) X
Е
( а Ь
• / + - ) X
Рассмот рим работу распознавателя. Ход разбора будем от ражат ь по шагам работы автомата в виде конфигурации МП-автомата, к которой добавлена цепочка, содержащая последовательность примененных правил грамматики. Состояние автомата q, указанное в его конфигурации, можно опустить, так как оно единственное- (a, Z. у), где а — непрочитанная часть входной цепочки символов; Z — содержимое стека (верхушка стека находится слева); у — последовательность номеров примененных правил (последовательность дополняется слева, так как автомат порождает левосторонний вывод)
Примем, что правила в грамматике номеруются в порядке едена направо и сверху вниз. На основе номеров примененных правил при успешном завершении разбора можно построить цепочку вывода и дерево вывода.
В качестве примера возьмем две правильные цепочки символов а+а*Ь и (а+а)*Ь и две ошибочные цепочки символов а+а* и (+а)*Ь.
Разбор цепочки а+а*Ь.
(a+a*b, S, X)
(a+a*b, TR, 1), так как aeFIRST(l,TR)
(a+a*b, EFR, 1,5), так как aeFIRST(l,EF)
(a+a*b, aFR, 1,5,10), так как aeFIRST(l,a)
(+a*b, FR, 1,5,10)
(+a*b, R, 1,5,10,6), гак как +gFOLLOW(1,F)
(+a*b, +TR, 1,5,10,6,3), так как +gFIRST(1,+TR)
(a*b, TR, 1,5,10,6,3)
(a*b, EFR, 1,5,10,6,3,5), так как aGFIRST(l ЕЕ)
(a*b, aFR, 1,5,10,6,3,5,10), так как aGFIRST(l,a)
И. (*b, FR, 1,5,10,6,3,5,10)
(*b, *EFR, 1,5,10,6,3.5,10,7), так как *gFIRST(1 *ЕЕ)
(b, EFR, 1,5,10,6,3,5,10,7)
(b, bFR, 1,5,10,6,3,5,10,7,11), так как beHRST(l,b)
(X, FR, 1,5,10,6,3,5,10,7,11)
(X, R, 1,5,10,6,3,5,10,7,11,6), так как XgFOLLOW(I.F)
(X, X, 1,5,10,6,3,5,10,7,11,6,2), так как XgFOLLOW(1,R), разбор закончен. Цепочка принимается.
Получили цепочку вывода:
S => TR => EFR => aFR => aFR => aR => a+TR => a+EFR => a+aFR => a+a*EFR => a+a*bFR => a+a*bR => a+a*b
Соответствующее ей дерево вывода приведено на рис. 12.2.
Разбор цепочки (а+а)*Ь.
((a+a)*b, S, X)
((a+a)*b, TR, 1), так как (gFIRST(LTR)
((a+a)*b, EFR, 1,5), так как (gFIRST(1,EF)
((a+a)*b, (S)FR, 1,5,9), так как (gF1RST(1,(S))
(a+a)*b, S)FR, 1,5,9)
(a+a)*b, TR)FR, 1,5,9,1), так как aGFIRST(l,TR)
(a+a)*b, EFR)FR, 1,5,9,1,5). так как aGFIRST(LEF)
(a+a)*b, aFR)FR, 1,5,9,1,5,10), так как aGFIRST(l,a)
(+a)*b, FR)FR, 1,5.9,1,5,10)
Рис. 12.2. Дерево вывода в Щ1)-грамматике для цепочки «а+а*Ь»
(+a)*b, R)FR, 1,5.9,1.5,10,6), так как +gFOLLOW(1,F)
И. (+a)*b, +TR)FR, 1,5,9,1,5,10,6,3), так как +gFIRST(1,+TR)
(a)*b, TR)FR, 1 5,9,1,5,10,6,3)
(a)*b, EFR)FR, 1,5,9,1,5,10,6.3,5), так как aGFIRST(l,EF)
(a)*b, aFR)FR, 1 5,9,1,5,10,6,3,5,10), так как aeFIRST(l,a)
(q,)*b, FR)FR, 1,5,9,1,5,10,6,3,5,10)
()*b, R)FR, 1,5,9,1,5,10,6,3,5,10,6), так как )gFOLLOW(1,F)
()*b, )FR, 1,5,9,1,5.10,6,3.5,10,6.2), так как )gFOLLOW(1,R)
(*b, FR, 1,5,9,1,5 10,6,3.5,10,6.2)
(*b, *EFR. 1,5,9,1.5,10,6,3,5,10,6,2,7), так как *gFOLLOW(1,*EF)
(b. EFR, 1,5,9,1,5,10,6,3,5,10,6,2,7)
(b, bFR 1,5,9,1,5,10,6,3,5,10,6,2,7,11), так как bGF!RST(l,b)
(X, FR, 1,5,9,1,5,10,6,3,5,10,6,2,7,11)
(X, R, 1,5,9,1,5,10,6,3,5,10,6,2,7,11,6), так как Xg FOLLOW(1,F)
(X, X, 1,5,9,1,5,10 6,3,5,10,6,2,7,11,6,2), так как XgFOLLOW(1,R) разбор закопчен. Цепочка принимается.
Получили цепочку вывода:
S => TR => EFR => (S)FR => (TR)FR => (EFR)FR => (aFR)FR => (aR)FR => (a+TR)FR => (a+EFR)FR => (a+aFR)FR => (a+aR)FR => (a+a)FR => (a+a)*EFR => (a+a)*bFR => (a+a)*bR => (a+a)*b
Соответствующее ей дерево вывода приведено па рис. 12 3.
Разбор цепочки а+а*
(а+а*, S, X)
(а+а*, TR, 1), так как aGFlRST(l.TR)
(а+а*, EFR. 1,5), так как aGFIRST(l.EF)
(а+а*, aFR, 1,5,10), так как aeFIRST(l,а)
(+а* FR, 1,5,10)
(+а*. R, 1,5,10,6), так как +gFOLLOW(1,F)
(+а*. +TR, 1,5,10,6,3), так как + gFIRST(1,+TR)
(а», TR, 1,5,10,6.3)
(a*, EFR, 1,5,10,6,3,5), так как aGFlRST(l.EF)
(а», aFR, 1,5,10,6,3,5,10), так как aeFIRST(l.a)
И. (*, FR, 1,5 10 6,3,5,10)
(*, *EFR, 1,5,10,6,3,5,10,7) так как *gFIRST(1 *EF)
(X, EFR, 1,5,10,6,3,5,10,7)
Ошибка, так как XgFOLLOW(1,E), но нет правила вида Е—>Х. Цепочка не принимается.
Разбор цепочки (+а)*Ь.
((+a)*b, S, X)
((+a)*b, TR, 1), так как (gFIRST(I.TR)
((+a)*b EFR, 1,5), так как (gFIRST(I.EF)
((+a)*b, (S)FR, 1,5,9), так как (gFIRST(1,(S))
(+a)*b, S)FR, 1,5,9)
Ошибка, так как нет правил для S вида S->a таких, чтобы +6FIRST(l,a) и +cFOLLOW(l,S). Цепочка не принимается.
Из рассмотренных примеров видно, что алгоритму разбора цепочек, построенному на основе распознавателя для ЕЕ(1)-грамматик, требуется гораздо меньше шагов на принятие решения о принадлежности цепочке языку, чем рассмотренному выше алгоритму разбора с возвратами. Надо отметить, что оба алгоритма распознают цепочки одного и того же языка. Данный алгоритм имеет большую эффективность, поскольку при росте длины цепочки количество шагов его рас-
тет линейно, а не экспоненциально. Кроме того, ошибка обнаруживается этим алгоритмом сразу, в то время как разбор с возвратами будет просматривать для неверной входной цепочки возможные варианты до конца, пока не переберет их все.
Очевидно, что этот алгоритм является более эффективным, но жесткие ограничения на правила для ЬЦ1)-грамматик сужают возможности его применения.
Восходящие распознаватели КС-языков без возвратов
Определение кН(к)-грамматики
Восходящие распознаватели выполняют построение дерева вывода снизу вверх. Результатом их работы является правосторонний вывод. Функционирование таких распознавателей основано на модификациях алгоритма «сдвиг-свертка» (или «перенос-свертка»), который был рассмотрен в разделе «Распознаватели КС-языков с возвратом», глава 11.
Идея состоит в том, чтобы модифицировать этот алгоритм таким образом, чтобы на каждом шаге его работы можно было однозначно дать ответ на следующие вопросы:
что следует выполнять: сдвиг (перенос) или свертку;
какую цепочку символов а выбрать из стека для выполнения свертки;
какое правило выбрать для выполнения свертки (в том случае, если существует несколько правил вида А^а, А2-*а, ... А„—>а).
Тогда восходящий алгоритм распознавания цепочек КС-языка не требовал бы выполнения возвратов, поскольку сам язык мог бы быть задан детерминированным расширенным МП-автоматом. Конечно, как уже было сказано, это нельзя сделать в общем случае, для всех КС-языков (поскольку даже сам класс детерминированных КС-языков более узкий, чем весь класс КС-языков). Но, вероятно, среди всех КС-языков можно выделить такой класс (или классы), для которых подобная реализация распознающего алгоритма станет возможной.
В первую очередь можно использовать тот же самый подход, который был положен в основу определения ЬЦк)-грамматик. Тогда мы получим другой класс КС-грамматик, который носит название ЬК(к)-грамматик.
КС-грамматика обладает свойством LR(k), k>0, если на каждом шаге вывода для однозначного решения вопроса о выполняемом действии в алгоритме «сдвиг-свертка» («перенос-свертка») расширенному МП-автомату достаточно знать содержимое верхней части стека и рассмотреть первые к символов от текущего положения считывающей головки автомата во входной цепочке символов.
Грамматика называется LR(k)-грамматикой, если она обладает свойством LR(k) для некоторого к>0*.
Название «LR(k)», как и рассмотренное выше «LL(k)», также несет определенный смысл. Первая литера «Ь» также обозначает порядок чтения входной цепочки символов: слева— направо. Вторая литера «R» происходит от слова «right» и, по аналогии с LL(k), означает, что в результате работы распознавателя получается правосторонний вывод. Вместо «к» в названии грамматики стоит число, которое показывает, сколько символов входной цепочки надо рассмотреть, чтобы принять решение о действии на каждом шаге алгоритма «сдвиг-свертка». Так, существуют ЬП(О)-грамматики, 1Л(1)-грамматикп и другие классы.
В совокупности все ЬВ(к)-грамматики для всех к>0 образуют класс LR-грамма- тик.
На рис. 12.4 схематично показано частичное дерево вывода для некоторой LR(k)- грамматики. В нем о обозначает уже разобранную часть входной цепочки а, на основе которой построена левая часть дерева у. Правая часть дерева х — это еще не разобранная часть, а А — это нетерминальный символ, к которому на очередном шаге будет свернута цепочка символов z, находящаяся на верхушке стека МП-автомата. В эту цепочку уже входит прочитанная, ио еще не разобранная часть входной цепочки и. Правая часть дерева х будет построена на основе части входной цепочки т. Свойство LR(k) предполагает, что однозначный выбор действия, выполняемого на каждом шаге алгоритма «сдвиг-свертка», может быть сделан на основе цепочки и и к первых символов цепочки т, являющихся частью входной цепочки а. Этим очередным действием может быть свертка цепочки z к символу А или перенос первого символа из цепочки т и добавление его к цепочке z.
Рис. 12.4. Схема построения дерева вывода для LR(k)-грамматики
Рассмотрев схему построения дерева вывода для ЬР(к)-грамматики на рис. 12.4 и сравнив ее с приведенной выше на рис. 12.1 схемой для ЬЦк)-грамматики,
Существование ЬК(О)-грамматик уже не является бессмыслицей в отличие от LL(0)- грамматик. В данном случае используется расширенный МП-автомат, который анализирует не один, а сразу несколько символов, находящихся на верхушке стека. Причем среди этих символов могут быть и терминальные символы из входной цепочки, попавшие в стек при выполнении сдвигов (переносов). Поэтому даже если автомат при к - 0 и нс будет смотреть на текущий символ входной цепочки, построенный им вывод все равно будет зависеть от содержимого стека, а значит, и от содержимого входной цепочки.
можно предположить, что класс LR-грамматик является более широким, чем класс LL-грамматик. Основанием для такого предположения служит тот факт, что на каждом шаге работы распознавателя ЕК(к)-грамматики обрабатывается больше информации, чем на шаге работы распознавателя ЕЦк)-грамматики, Действиюльно, для принятия решения на каждом шаге алгоритма распознавания ЬЦк)-граммагики используются первые к символов па цепочки от, а для принятия решения на шаге распознавания ЕК(к)-грамматики — вся цепочка о п еще первые к символов из цепочки т. Очевидно, что во втором случае можно проанализировать больший объем информации и, таким образом, построить вывод для более широкого класса КС-языков.
Приведенное выше довольно нестрогое утверждение имеет с трого обоснованное доказательство. Доказано, что класс LR-грамматик является более широким, чем класс LL-грамматик [6, т. 2|. То есть для каждого КС-языка, заданного LL-грам- матикой, может быть построена LR-грамматика, задающая тот же язык, но не наоборот. Существуют также языки, заданные LR-грамматикаыи, для которых невозможно построить LL-грамматику, задающую тот же язык. Иначе говоря, для всякой LL-грамматики существует эквивалентная ей LR-грамматика, по не для всякой LR-грамматики существует эквивалентная ей LL-грамматика*.
Для LR(k)-rpaMM3THK известны следующие полезные свойства
U всякая LR(k)-грамматика для любого к> О является однозначной;
существует алгоритм, позволяющий проверить, является ли заданная грамматика ЕК(к)-грамматикой для строго определенного числа к
Есть, однако, неразрешимые проблемы для произвольных КС-грамматик (они аналогичны таким же проблемам для других классов КС-грамматик):
не существует алгоритма, который бы мог проверить, является ли заданная КС-грамматика ЬК(к)-грамматикой для некоторого произвольного числа к;
не существует алгоритма, который бы мог преобразовать (или доказать, что преобразование невозможно) произвольную КС-грамматику к виду LR(k)- грамматики для некоторого к.
Кроме того, для LR-грамматик доказано еще одно очень интересное свойство — класс LR-грамматик полностью совпадает с классом детерминированных КС- языков. То есть, во-первых, любая ЕК(к)-грамматика задает детерминированный КС-язык (это очевидно следует из однозначности всех LR-грамматик), а во-вторых, для любого детерминированного КС-языка можно построить LR-граммати- ку, задающую этот язык. Второе утверждение уже не столь очевидно, но доказано в теории формальных языков [6, т. 1, 65р.
Говоря о соотношении классов LL грамматик и LR-грамматик, мы не затрагиваем вопрос о значениях к для этих грамматик. Если для некоторой ЕЕ(к)-грамматики всегда существует эквивалентная ей LR-грамматика, то это вовсе не значит, что она будет LR(.k)-rpa.M- матикой с тем же значением к, и наоборот Но если говорится, что существуют LR- грамматики, для которых нет эквивален тных им LL-грамматик, то это означает, что нет эквивалентных им ЕЕ(к)-грамматик для всех возможных значении к>0.
Более того, доказано даже, что любой детерминированный КС-язык может быть задан LR( 1 )-грамматикой.
В принципе класс LR-грамматик очень удобен для построения распознавателей детерминированных КС-языков (а все языки программирования, безусловно, относятся к этому классу). Но тот факт, что для каждого детерминированного КС-языка существует задающая его LR-грамматика, еще пи о чем не говорит, поскольку из-за неразрешимости проблемы преобразования отсутствует алгоритм, который позволил бы эту грамматику построить всегда. Данный детерминированный КС-язык может быть изначально задан грамматикой, которая не относится к классу LR-грамматик. В таком случае совсем не очевидно, что для этого языка удастся построить распознаватель на основе 1 R-грамматики, потому что в общем случае нет алгоритма, который бы позволил эту грамматику получить, хотя и известно, что она существует. То, что проблема нс разрешима в общем случае, совсем не означает, что се не удастся решить в конкретной ситуации. И здесь факт существования LR-грамматики для каждого детерминированного КС-языка играет важную роль — всегда есть смысл в каждом конкретном случае пытаться построить такую грамматику.
Принципы построения распознавателей для ЬП(к)-грамматик
Для того чтобы формально определить LR(k) свойство для КС-грамматик, введем понятие пополненной КС-грамматики. Грамматика G' является пополнен нон грамматикой, построенной на основании исходной |рамматики G(VT,VN,P,S), если выполняются следующие условия:
грамматика С совпадает с грамматикой G, если целевой символ S не встречается нигде в правых частях правил грамматики G;
грамматика С строится как грамматика G'(VT,VNu{S'),Pu{S'->S},S'), если целевой символ S встречается в правой части хотя бы одного правила из множества Р в исходной грамматике G
Фактически пополненная КС-грамматика строится таким образом, чтобы ее целевой символ не встречался в правой части пи одного правила. Если нужно, то в исходную грамматику G для этого добавляется новый терминальным символ S', который становится целевым символом, и новое правило S'->S. Очевидно, что пополненная грамматика G' эквивалентна исходной грамматике G, то есть L(G) - L(G)
Теперь рассмотрим формальное определение LR(k) свойства.
Если для произвольной КС-грамматики G в ее пополненной грамматике G' для двух произвольных цепочек вывода из условий:
S' =>* aAw => aPw
S' =>* уВх => apy
FlRST(k.w) = FIRST(k,y)
следует, что aAw = уВх (то есть a-у, А = В и х = у), то доказано, что грамматн ка G обладает LR(k) свойством. Очевидно, что тогда п пополненная грамматн ка G' также обладает LR(k) свойством.
Понятие «пополненной грамматики» введено исключительно с той целью, чтобы в процессе работы алгоритма «сдвиг-свертка» выполнение свертки к целевому символу пополненной грамматики S' служило сигналом к завершению алгоритма (поскольку в пополненной грамматике символ S' в правых частях правил нигде не встречается). Если условие отсутствия целевого символа в правых частях правил грамматики не будет соблюдаться, то на алгоритм распознавателя но- гребуется наложить дополнительные ограничения, так как появление целевого символа па вершине стека уже не будет означать завершение работы алгоритма. Поскольку построение пополненных грамматик выполняется элементарно и не накладывает никаких дополнительных ограничений на исходную КС-грамматику, то дальше будем считать, что все распознаватели для ЕК(к)-грамматик работают с пополненными грамматиками.
Распознаватель для LR(k)-грамматик функционирует па основе управляющей таблицы Т Эта таблица состоит из двух частей, называемых «действия» и «переходы». По строкам таблицы распределены псе цепочки символов на верхушке стека, которые могут приниматься во внимание в процессе работы распознавателя. По столбцам в части «действия» распределены все части входной цепочки символов длиной пе более к (аванцепочки), которые могут следовать за считывающей головкой автомата в процессе выполнения разбора; а в части «переходы» — все терминальные и нетерминальные символы грамматики, которые мо- гут появляться на верхушке стека автомата при выполнении действий (сдвигов или сверток).
Клетки управляющей таблицы Т и части «действия» содержат следующие данные;
«сдвиг» — если в данной си гуации требуется выполнение сдвига (переноса текущего символа из входной цепочки в стек);
«успех» — если возможна свертка к целевому символу грамматики S и разбор входной цепочки завершен;
целое число («свертки») — если возможно выполнение свертки (число обозначает номер правила грамматики, но которому должна выполняться свертка);
«ошибка» — во всех других ситуациях.
Действия, выполняемые распознавателем, можно вычислять всякий pan на основе состояния стека и текущей аванцепочки. Однако этого вычисления можно избежать, если после выполнения действия сразу же определять, какая строка таблицы Т будет использована для выбора следующего действия. Тогда эту строку можно поместить в стек вместе с очередным символом и выбирать затем в момент, когда опа понадобится. Таким образом, автомат будет хранить в стеке не только символы алфавита, но и связанные с ними строки управляющей таблицы Т.
Клетки управляющей таблицы Т в части «переходы» как раз и служат для выяснения номера строки таблицы, которая будет использована для определения выполняемого действия на очередном шаге. Эти клетки содержат следующие дант ные:
целое число - номер строки таблицы Т
«ошибка» — во всех других ситуациях.
Для удобства работы распознаватель ЬК(к)-грамматнки использует также два специальных символа ±„ и 1К. Считается, что входная цепочка символов всегда начинается символом ±„ и завершается символом ±к. Тогда в начальном состоянии работы распознавателя символ J_„ находится на верхушке стека, а считывающая головка обозревает первый символ входной цепочки. В конечном состоянии в стеке должны находиться символы S (целевой символ) и ±„, а считывающая головка автомата должна обозревать символ ±к.
Алгоритм функционирования распознавателя ЕК(к)-грдмматжи можно описать следующим образом:
Шаг 1. Поместить в стек символ . , и начальную (нулевую) строку управляющей таблицы Т. В конец входной цепочки поместить символ ±к. Перейти к шагу' 2.
Шаг 2. Прочитать с вершины стека строку управляющей таблицы Т. Выбрать из этой строки часть «действие» в соответствии с аванцепочкой, обозреваемой считывающей головкой автомата. Перейти к шагу 3
ШагЗ. В соответствии с типом действия выполнить выбор из четырех вариантов: □ «сдвиг» — если входная цепочка не прочитана до конца, прочитать и запомнить как «новый символ» очередной символ из входной цепочки, сдвинуть считывающую головку на о ту позицию вправо, иначе прервать выполнение алгоритма и сообщить об ошибке;
целое число ^«свертка»} — выбрать правило в соответствии с номером, удалить из стека цепочку символов, составляющую правую часть выбранного правила, взять символ из левой части пр шила и Запомнить его как «новый символ»;
«ошибка» — прервать выполнение алгоритма, сообщить об ошибке;
«успех» — выполнить свертку к целевому символу S, прервать выполнение алгоритма, сообщить об успешном разборе входной цепочки символов, если входная цепочка прочитана до конца, иначе сообщить об ошибке
Конец выбора. Перейти к шагу 4.
Шаг 4. Прочитать с вершины стека строку управляющей таблицы Т. Выбрать из этой строки часть «переход» в соответствии с символом, который был запомнен как «новый символ» на предыдущем шаге. Перейти к шагу 5.
Шаг 5. Если часть «переход» содержит вариант «ошибка», тогда прервать выполнение алгоритма и сообщить об ошибке, иначе (если там содержится помер строки управляющей таблицы Т) положить в щек «новый символ» и строку таблицы Т с выбранным номером. Вернуться к шагу 2.
Для работы алгоритма кроме управляющей таблицы Т используется также некоторая временная переменная («новый символ»), хранящая значение терминального или петерййиалыюго символа, полученного в результате сдвига пли свертки В программной реализации алгоритма вовсе не обязательно помещать в стек сами строки управляющей таблицы — поскольку сама таблица неизменна в процессе выполнения алгоритма, то достаточно запоминать соответствующие ссылки
Доказано, что данный алгоритм имеет линейную зависимость необходимых для сто выполнения вычислительных ресурсов от длины входной цепочки символов. Следовательно, распознаватель для КН(к)-грамматики имеет линейную зависимость сложности от длины входной цепочки, а потому является линейным распознавателем.
Для построения распознавателя осталось научиться строить управляющую таблицу Т. Исходными данными для се построения служат правила исходной грамматики. Вопросы построения этой таблицы в данном пособии не рассматриваются, они достаточно подробно освещены в [6, т. 1]. Далее будут рассмотрены только примеры управляющих таблиц для некоторых достаточно простых грамматик.
Распознаватель для ГЯ(О)-грамматики
Простейшим случаем ЬН(к)-грамматик являются ЬК(О)-грамматикп. При к - О распознающий расширенный МП-автомат совсем не принимает во внимание текущий символ, обозреваемый его считывающей головкой. Решение о выполняемом действии принимается только на основании содержимого стека автомата. При этом не должно возникать конфликтов между выполняемым действием (сдвиг или свертка), а гакже между различными вариантами при выполнении свертки. Управляющая таблица для ЬК(О)-грамматики строится на основании понятия «левых контекстов» для нетерминальных символов: очевидно, что после выполнения свертки для нетерминального символа А в стеке МП-автомата ниже этого символа будут располагаться только те символы, которые могут встречаться в цепочке вывода слева от А. Эти символы и составляют «левый контекст» для А. Поскольку выбор между' сдвигом или сверткой, а также между типом свертки в ЬК(О)-грамматиках выполняется только на основании содержимого стека, то ЬК(О)-грамматика должна допускать однозначный выбор па основе левого контекста для каждого символа [5, 65, т. 1, 15, 65]
Рассмотрим простую КС-грамматику G((а,b}. {S}, {S—>aSS|b}, S). Пополненная грамматика для нес будет иметь вид G((a,b], (S, S’], {S'->S, S ->aSS|b}, S'). Эта грамматика является LR(0)-грамматикой. Управляющая таблица для псе приведена в табл. 12.2.
Таблица 12.2. Пример управляющей таблицы для иЗ(О)-грамматики
Стек
Действие
Переход
S
в
ь
1„
СДВИГ
1
2
3
S
успех, 1
а
СДВИГ
4
2
3
ь
свертка, 3
aS
сдвиг
5
2
3
aSS
свертка, 2
Колонка «Стек», присутствующая в таблице, в принципе не нужна для распознавателя. Она введена исключительно для пояснения каждого состояния стека автомата. Пустые клетки в таблице соответствуют состоянию «ошибка». Правила в грамматике пронумерованы от 1 до 3 (при этом будем считать, что состоянию «успех» — свертке к нулевому символу — в пополненной грамматике всегда соответствует первое правито). Распознаватель работает, невзирая на текущий символ, обозреваемый считывающей головкой расширенного МП-автомата, поэтому колонка «Действие» в таблице имеет только один столбец, нс помеченный никаким символом, — указанное в ней данное действие выполняется всегда для каждой строки таблицы.
Рассмотрим примеры распознавания цепочек этой грамматики. Работу распознавателя будем отображать ио шагам. Конфигурацию расширенного МП-автомата будем отображать в виде трех компонентов: не прочитанная еще часть входной цепочки символов, содержимое стека МП-автомата, последовательность номеров примененных правил грамматики (поскольку автомат имеет только одно состояние, его можно не учитывать) В стеке МП-автомата вместе с Помещенными туда символами показаны и номера строк управляющей таблицы, соответствующие этим символам в формате \симвоХ, номер строки}.
Разбор цепочки abababb.
(abababb±K, {±,,,0}, X)
(bababb±K, {±„,0}{а,2}, X)
(ababb±K, {±„,0}{a,2}{b,3}, X)
(ababb±K, {lH,0}(a,2}{S,4}, 3)
(babb±K, {±l„0}(a,2}{S,4}{a,2}, 3)
G. (abb±K, {±1„0}{a,2}{S,4}{a,2}{b,3}, 3)
(abb±K, {±,„0}{a,2}(S,4}{a,2}{S,4}, 3,3)
(bb±K, {±„,0}{a,2){S,4}{a,2}{S,4}{a,2}, 3,3)
(blK, {±1„0}{a,2}{S,4}{a,2}{S,4}{a,2)(b,3}, 3,3)
(b±K, {±11,0}{a,2}{S,4}{a,2}{S,4}{a,2}{S,4} 3,3,3)
(1K, (±ll,0}{a,2}{S,4}{a,2}{S,4}{a,2}{S,4}{b,3), 3,3,3)
(±K, {±l„0}{a,2}{S,4}(a,2}{S,4}{a,2}{S,4}{S,5}, 3,3,3,3)
(±K, {±l„0}{a,2}{S,4}{a,2}{S,4}{S,5}, 3,3,3,3,2)
(±K, {±,„0}{a,2}{S,4}{S,5}, 3,3,3,3,2,2)
(±K, {l^OKS,!}, 3,3 3,3,2,2,2)
(±K, {±„,0}{S',*}, 3,3,3,3,2,2,2,1) — разбор завершен.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S' => S => aSS => aSaSS => aSaSaSS => aSaSaSb => aSaSabb => aSababb => abababb.
Разбор цепочки aabbb.
(aabbb±K, {±и,0}, X)
(abbb±K, {±„,0}{a,2}, X)
(ЬЬЫК, {±„,0}{а,2}{а,2}, X)
(bb±K, {lH,0}{a,2}{a,2}{b,3}, X)
(ЬЬ±К, {±„,0}{a,2}{a,2}{S,4}, 3)
(b±K, {±l„0}{a,2}{a,2}{S,4}{b,3}, 3)
(Ык, {±„,0}{а,2}{а,2}{5,4}{5,5}, 3,3)
(b±K, {±,„0}{a,2){S,4}, 3,3,2)
(±к, {llp0}{a,2}{S,4}{b,3}, 3,3,2)
(1К, {±„,0}{a,2}{S,4}{S,5}, 3,3,2,3)
(1К, {±„,0}{S,l}, 3,3,2,3,2)
(±к, {±,„0}{S',*}, 3,3,2,3,2,1) — разбор завершен.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S' => S => aSS => aSb => aaSSb => aaSbb => aabbb.
Разбор цепочки aabb.
(aabb±K, {±„,0}, X)
(abblK, {±н,0){а,2}, X)
(bblK, {l,„0}{a,2}{a,2}, X)
(b±K, {±„,0}{a,2}{a,2}{b,3}, X)
(b±K, {±ll,0}{a,2}{a,2}{S,4}, 3)
(±K, {lH,0Ka,2}{a,2}{S,4}{b,3}, 3)
(±K,{±l„0}{a,2}{a,2){S4}{S,5},3,3)
(±K, U„,O}{a,2}{S,4}, 3,3,2)
Ошибка, невозможно выполнить сдвиг.
Распознаватель для ЬК(О)-грамматики достаточно прост. Приведенный выше пример можно сравнить с методом рекурсивного спуска или с распознавателем для ЬЦ1)-грамматики — оба эти метода применимы к описанной выше грамматике. По количеству шагов работы распознавателя эти методы сопоставимы, но по реализации нисходящие распознаватели в данном случае немного проще.
Распознаватель для СР(1)-грамматики
Другим употребительным классом РК(к)-грамматик являются LR(1 )-граммати- ки. В этих грамматиках основанием для принятия расширенным МП-автоматом решения о выполнении сдвига или свертки служит информация о содержимом стека автомата и текущий символ, обозреваемый считывающей головкой.
Рассмотрим простую КС-грамматику G({a,b}, (S), (S—>SaSb|X}. S). Пополненная грамматика для нее будет иметь вид G((a,b}, (S, S'], {S’->S. S—>SaSb X}. S'). Эта грамматика является LR( 1 /грамматикой [5, 15, 65] Управляющая таблица для нее приведена в табл. 12.3.
Колонка «Стек», присутствующая в таблице, в принципе не нужна для распознавателя. Она введена исключительно для пояснения каждого состояния стека автомата. Пустые клетки в таблице соответствуют состоянию «ошибка» Прави-
ла в грамматике пронумерованы от 1 до 3 (при этом будем считать, что состоянию «успех» — свертке к нулевому символу — в пополненной грамматике всегда соответствует первое правило). Колонка «Действие» в таблице содержит перечень действий, соответствующих текущему входному символу, обозреваемому считывающей головкой расширенного МП-автомата.
Таблица 12.3. Пример управляющей таблицы для LR(1 )-грамматики
Стек
Действие
Переход
a
b
1,
а
b
S
li.
свертка, 3
свертка, 3
1
S
СДВИГ
успех, 1
2
Sa
свертка, 3
свертка, 3
3
SaS
СДВИГ
СДВИГ
4
5
SaSa
свертка, 3
свертка, 3
6
SaSb
свертка, 2
свертка, 2
SaSaS
сдвиг
сдвиг
4
7
SaSaSb
свертка, 2
свертка, 2
Рассмотрим примеры распознавания цепочек этой грамматики по шагам, которые совершает распознаватель. Конфигурацию расширенного МП-автомата будем отображать в виде трех компонентов: не прочитанная еще часть входной цепочки символов, содержимое стека МП-автомата, последовательность номеров примененных правил грамматики (поскольку автомат имеет только одно состояние, его можно не учитывать). В стеке МП-автомата вместе с помещенными туда символами показаны и номера строк управляющей таблицы, соответствующие этим символам в формате {символ, номер строки}.
Разбор цепочки abababb.
(abababblK, {1„,0}, X)
(abababb±K, {1H,O}{S,1}, 3)
(bababb±K, {J_ll,O}{S,l}{a,2}, 3)
(bababblK, {l,„0}{S,l){a,2}{S,3}, 3,3)
(ababblK, {±,„0}{S,l}{a,2}{S,3}{b,5}, 3,3)
(ababblK, {1H,O}{S,1), 3,3,2)
(babblK, {±,„0}{S,l}{a,2}, 3,3,2)
(babb±K, {l,„0}{S,l}{a,2}{S,3}, 3,3,2,3)
(abblK, {±1„0}{S,l}{a,2}{S,3){h,5}, 3,3,2,3)
(abb±K, {±,„0}{S,l}, 3,3,2,3,2)
(bblK, {l„,0}{S,l}{a,2}, 3,3,2,3,2)
(bb±K, {±,„0}{S,l}{a,2}{S,3}, 3,3,2,3,2,3)
(b±K, {±,„0}{S,l}{a,2}{S,3}(b,5}, 3,3,2,3,2,3)
Ошибка, нет данных для «Ь» в строке 5.
Разбор цепочки aababb.
(aababb±K, {_LH,0}, X)
(aababb±K, {_LM,O}{S,1}, 3)
(ababb±K, {±,„0}{S,l}{a,2}, 3)
(ababb±K, {ln,0}{S,l}(a,2}{S,3}, 3,3)
(babb±K, {±,„0}{S,l}{a,2}{S,3}{a 4}, 3,3)
(babb±K, {±H,0}{S,l}{a,2}{S,3}{a,4}{S,6}, 3,3,3)
(abb±K, {±H,0}{S,l}{a,2}{S,3}{a,4}{S,6){b,7}, 3,3,3)
(abb±K, {!„ 0}{S,l}(a,2}{S,3), 3,3,3,2)
(bblK, {±1„0}{S,l}(a,2}{S,3}{a,4}, 3,3,3,2)
(bb±K, {±1„0}{S,l}{a,2}{S,3}{a,4}{S,6}, 3,3,3,2,3)
(b±K, {l„,0}{S,l}{a,2}{S,3}{a,4}{S,6}{b,7}, 3.3.3.2.3)
(b±K, {±„,0}{S,l}{a,2}{S,3}, 3,3,3,2,3,2)
(±K, {l„,0}{S,l}{a,2}{S,3}{b,5}, 3,3,3,2,3,2)
(±K, {±ll,0}(S,l},3.3,3,2.3,2,2)
15 (±K, {IEOHS',*}, 3,3,3,2,3,2,2,1) — разбор завершен.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S' => S => SaSb => SaSaSbb => SaSabb => SaSaSbabb => SaSababb => Saababb => aababb.
Невозможно непосредственно сравнить работу двух рассмотренных вариантов распознавателей: восходящего (LR) и нисходящего (LL). Эго очевидно, поскольку приведенная в примере Грамматика не является ЬЦ1)-грамматикой. Соответственно, опа не может быть разобрана и методом рекурсивного спуска (можно убедиться в этом, построив множества FIRST и FOLLOW для символов грамматики: I IRST(1,S) - {a}, FOLLOW(1,S) - {а,ЬЛ}; FIRST(1,S) о FOLLOW(l.S) * 0) Па основании этой грамматики вообще невозможно построить нисходящий распознаватель, поскольку опа явно содержит левую рекурсию. Устранив левую рекурсию (см. алгоритм в разделе «Преобразование КС-грамматик. Приведенные грамматики», глава 11) и выполнив ряд несложных преобразований, можно получить эквивалентную ей грамматику G"({a.b}.{S],{S->?JaSbS),S), которая будет относиться к классу ПС(1)-фамматик (действительно, дтя нее F1RST(1,S) - {a}, FOLLOW(1,S) - = {ЬЛ};. FIRST(l.S) n FOLLOW(1,S) - 0). Теперь уже возможно сравнить работу двух вариантов распознавателей — нисходящего и восходящего.
Несмотря па то что распознаватель на основе ЕЕ(1)-грамматики и в данном случае имеет более простой алгоритм функционирования, нельзя сказать, что им легче воспользоваться, поскольку его создание требует дополнительных преобразований исходной грамматики. Для более сложных ЬН(1)-грамматнк такие преобразования могут быть в принципе невозможны, поскольку класс языков, заданных ЕК(1)-грамматиками, шире, чем класс языков, заданных БЬ(1)-грам- магпками. Поэтому для конкретной заданной грамматики чаще бывает проще построить восходящий распознаватель, чем нисходящий.
На практике ЕН(к)-грамматикн при k> 1 не применяются. На это имеются две причины. Во-первых, управляющая таблица для ЕН(к)-грамматики при к> 1 будет содержать очень большое число состояний, и распознаватель будет дос таточно сложным и не столь эффективным. Во-вторых, для любого языка, определяемого LR(k)-грамматикой, существует LR(1/грамматика, определяющая тот же язык. То есть для любой Ы<(к)-грамматнки с k > 1 всегда существует эквивалентная ей LR(1/грамматика. Более того, для любого детерминированного КС- языка существует LR(1/грамматика (другое дело, что далеко не всегда такую грамматику можно легко построить).
Грамматики предшествования (основные принципы)
Еще одним распространенным классом КС-грамматик, для которых возможно построить восходящий распознаватель без возвратов, являются грамматики предшествования. Так же как и распознаватель рассмотренных выше LR-грам- магик, распознаватель для грамматик предшествования строится на (к нове алгоритма «сдвиг-свертка» («перенос-свертка»), который в общем виде был рассмотрен в разделе «Распознаватели КС-языков с возвратом», глава 11.
Прийцип организации распознавателя входных цепочек языка, заданного грамматикой предшествования, основывается на том, что для каждой упорядоченной пары символов в граммат ике устанавливается некоторое отношение, называемое отношением предшествования. В процессе разбора входной цепочки расширенный МП-автомат сравнивает текущий символ входной цепочки с одним из символов, находящихся на верхушке стека автомата. В процессе сравнения проверяется, какое из возможных отношений предшествования существует между этими двумя символами. В зависимости от найденного отношения выполняется либо
сдвиг (перенос), либо свертка. При отсутствии отношения предшествования между символами алгоритм сигнализирует об ошибке.
Задача заключается в том, чтобы иметь возможность непротиворечивым образом определить отношения предшествования между символами грамматики Если это возможно, то грамматика может быть отнесена к одному из классов грамматик предшествования.
Существует несколько видов грамматик предшествования. Они различаются по тому, какие отношения предшествования в них определены и между какими типами символов (терминальными или нетерминальными) могут быть установлены эти отношения. Кроме того, возможны незначительные модификации функционирования самого алгоритма «сдвиг-свертка» в распознавателях для таких грамматик (в основном на этапе выбора правила для выполнения свертки, когда возможны неоднозначности) [5, 6, 23, 65}.
Выделяют следующие виды грамматик предшествования:
простого предшествования;
расширенного предшествования;
слабого предшествования;
смешанной стратегии предшествования;
операторного предшествования.
Далее будут рассмотрены два наиболее простых и распространенных типа — грамматики простого и операторного предшествования.
Грамматики простого предшествования
Грамматикой простого предшествования называют такую приведенную КС-грамматику’ G(VN,VT,P,S), V - VTuVN, в которой:
Для каждой упорядоченной пары терминальных и нетерминальных символов выполняется не более чем одно из трех отношений предшествования:
О В, =• В; (V ВрВ,е¥), если и только если В правило A—>xB,BJy еР, где AeVN, x,yeV*;
О Bj <• Bj (V Bj.BjeV), если и только если 3 правило A->xB,Dy еР и вывод D=>*SjZ, где A.DeVN, x,y,zeV*
О В, •> В, (V B„BjeV) , если и только если 3 правило А->хСВ,у еР и вывод C=>*zB, или 3 правило A-»xCDy еР и выводы C=>*zB, и D=>*BjW, где A,C,DeVN x,y,z,WGV.
Различные правила в грамматике имеют разные правые части (го есть в грам магике не должно быть двух различных правил с одной и той же правой частью).
Напоминаем, что КС-грамматика называется приведенной, если она не содержит циклов, бесплодных и недостижимых символов и Л-иранил (см раздел «Преобразование КС- граммашк Приведенные грамматики»,1 Глава И)
Отношения “-, <• и •> называют отношениями предшествования для символов. Отношение предшествования единственно для каждой упорядоченной пары символов. При этом между какими-либо двумя символами может и не быть отношения предшествования — это значит, что они не могут находиться рядом ни в одном элементе разбора синтаксически правильной цепочки. Отношения предшествования зависят от порядка, в котором стоят символы, и в этом смысле их нельзя путать со знаками математических операций — они нс обладают ни свойством коммутативности, ни свойством ассоциативности. Например, если известно, что Bj •> В,, то не обязательно выполняется В; <• В) (поэтому знаки предшествования иногда помечают специальной точкой: -•, <-, ->).
Для грамматик простого предшествования известны следующие полезные свойства:
всякая грамматика простого предшествования является однозначной;
легко проверить, является или нет произвольная КС-грамматика грамматикой простого предшествования (для этого достаточно проверить рассмотренные выше свойства грамматик простого предшествования или воспользоваться алгоритмом построения матрицы предшествования, который рассмотрен далее).
Как и для многих других классов грамматик, для грамматик простого предшествования не существует алгоритма, который бы мог преобразовать произвольную КС-грамматику в грамматику простого предшествования (или доказать, что преобразование невозможно).
Метод предшествования основан на том факте, что отношения предшествования между двумя соседними символами распознаваемой строки соответствуют трем следующим вариантам:
В, <• Вц.!, если символ Bi+I — крайний левый символ некоторой основы (это отношение между символами можно назвать «предшествует основе» или просто «предшествует»);
В; •> Bi+I, если символ Bj — крайний правый символ некоторой основы (это отношение между символами можно назвать «следует за основой» или просто «следует»);
В, =• Bi+1, если символы В, и Bi+1 принадлежат одной основе (это отношение между символами можно назвать «составляют основу»).
Исходя из этих соотношений, выполняется разбор строки для грамматики предшествования.
Суть принципа такого разбора можно пояснить на рис. 12.5. На нем изображена входная цепочка символов аурб в тот момент, когда выполняется свертка цепочки у. Символ а является последним символом подцепочки а, а символ b — первым символом подцепочки р. Тогда, если в фаммагикс удастся установить непротиворечивые отношения предшествования, то в процессе выполнения разбора по алгоритму «сдвиг-свертка» можно всегда выполнять сдвиг до тех пор, пока между символом на верхушке стека и текущим символом входной цепочки существует отношение <• или —•. А как только между этими символами будет обнару-
жспо отношение •>, так сразу надо выполнять свертку. Причем для выполнения свертки из стека надо выбирать все символы, связанные отношением -•. То, что все различные правила в грамматике предшествования имеют различные правые части, гарантирует непротиворечивость выбора правила при выполнении свертки Таким образом, установление непротиворечивых отношений предшествования между символами грамматики в комплексе с несовпадающими правыми частями различных правил дает ответы на все вопросы, которые надо решить для организации работы алгоритма «сдвиг-свертка» без возвратов.
а
а
т
ь
Р
6
Рис. 12.5. Отношения между символами входной цепочки в грамматике предшествования
На основании отношении предшествования строят матрицу предшествования грамматики Строки матрицы предшествования помечаются первыми (левыми) символами, столбцы — вторыми (правыми) символами отношений предшествования. В клетки матрицы на пересечении соответствующих столбца и строки помещаются знаки отношений. При этом пустые клетки матрицы говорят о том, ч го между данными символами нет ни одного отношения предшествования.
Матрицу предшествования грамматики сложно построить, опираясь непосредственно на определения отношений предшествования. Удобнее воспользоваться двумя дополнительными множествами — множеством крайних левых и множеством крайних правых симвотов относительно нетерминальных символов грамматики G(VN,VT,P,S), V = VTuVN Эти множества определяются следующим образом:
L(A) = {X | 3 A=>*Xz), Ае VN, XeV, zeV* — множество крайних левых символов относительно нетерминального символа А (цепочка z может быть и пустой цепочкой);
R(A) = {X | 3 A=>*zX}, AgVN, XeV, zeV" — множество крайних правых символов относительно нетерминального символа А
Иными словами, множество крайних левых символов относительно нетерминального символа А — вто множество всех крайних левых символов в цепочках, которые могут быть выведены из символа А. Аналогично, множество крайних правых символов относительно нетерминального символа А — это множество всех крайних правых символов в цепочках, которые могут быть выведены из символа А.
Тогда отношения предшествования можно определить так:
В; =• Bj (V BMB|GV). если 3 правило A-»xBjBjy е Р, гд» AgVN, х,ус¥"
Bj <■ В, (V В„В;е¥), если 3 правило A—>xB,Dv g Р и BjgL(D). где A,D VN X,yGV‘
Bj •> Bj (V Bj,BjGV), если 3 правило А->хСВ(у е Ри B,gR(C) или 3 правило A^xCDVg Р и BjGR(C), BjgL(D), где A,C,DgVN, х,уеУ.
Такое определение отношений удобнее на практике, так как не требует построения выводов, а множества L(A) и R(A) могут быть построены для каждого нетерминального символа AgVN грамматики G(VN,VT,P,S), V - VTuVN по очень I фостому алгоритму.
Шт. 1. V AgVN:
R0(A) - {X | А->уХ, XgV, yGV-}, L0(A) - {X | A^Xy, XgV, ycW}, i := 1.
Для каждого нетерминального символа А ищем все правила, содержащие А в левой части Во множество L(A) включаем самый левый символ из правой части правил, а во множество R(A) — самый крайний правый символ из правой части. Переходим к шагу 2.
Шаг 2. V AgVN:
R (А) - R ^Л) u R,(B), V В g (R.t(A) n VN),
L (A) L^A) и ЬИ(В), V В g (LH(A) n VN).
Для каждого нетерминального символа А: если множество L(A) содержит нетерминальные символы грамматики А1, А", ..., то его надо дополнить символами, входящими в соответствующие множества L(A’), L(A"), ... и не входящими в L(A). Ту же операцию надо выполнить для R(A).
Шаг 3. Если 3 AgVN: Rj(A) * R, ,(А) или Ц(А) * L^A), то i:-i+l и вернуться к шагу 2, иначе построение закопчено: R(A) = R,(A) и L(A) = L (А).
Если на предыдущем шаге х№я бы одно множество L(A) или R(A) для некоторого символа грамматики изменилось, то надо вернуться к шагу 2, иначе построение закончено.
После построения множеств L(A) и R(A) по правилам грамматики создастся матрица предшествования. Матрицу предшествования дополняют символами ±„ и ±к (начало и колец цепочки). Для них определены следующие отношения предшествования:
„ <• X, V acV, если 3 S=>*Xy, где SgVN, ycV* или (с другой стороны) если XgL(S);
±к •> X, V acV, если 3 S=>*yX, где SgVN, уgV* или (с другой стороны) если XgR(S)
Здесь S — целевой символ грамматики.
Матрица предшествования служит основой для работы распознавателя языка, заданного грамматикой простого предшествования.
Алгоритм «сдвиг-свертка» для грамматики простого предшествования
Данный алгоритм выполняется расширенным МП-автоматом с одним состоянием. Отношения предшествования служат для того, чтобы определить в процессе выполнения алгоритма, какое действие — сдвиг или свертка — должно выполняться на каждом шаге алгоритма, и однозначно выбрать цепочку для свертки. Однозначный выбор правила при свертке обеспечивается за счет различия правых частей всех правил грамматики. В начальном состоянии автомата считываю-
щая головка обозревает первый символ входной цепочки, в стеке МП-автомата находится символ ±„ (начало цепочки), в конец цепочки помещен символ ±к (конец цепочки). Символы ±|5 и ±к введены для удобства работы алгоритма, в язык, заданный исходной грамматикой, они не входят.
Разбор считается законченным (алгоритм завершается), если считывающая головка автомата обозревает символ ±к и при этом больше нс может быть выполнена свертка. Решение о принятии цепочки зависит от содержимого стека. Автомат принимает цепочку, если в результате завершения алгоритма в стеке находятся начальный символ грамматики S и символ ±н. Выполнение алгоритма может быть прервано, если на одном из его шагов возникнет ошибка. Тогда входная цепочка не принимается.
Алгоритм состоит из следующих шагов.
Шаг 1. Поместить в верхушку стека символ ±,„ считывающую головку — в начало входной цепочки символов.
Шаг 2. Сравнить с помощью отношения предшествования символ, находящийся на вершине стека (левый символ отношения), с текущим символом входной цепочки, обозреваемым считывающей головкой (правый символ отношения)
Шаг 3. Если имеет место отношение <• или —, то произвести сдвиг (перепое текущего символа из входной цепочки в стек и сдвиг считывающей головки на один шаг вправо) и вернуться к шагу 2. Иначе перейти к шагу 4.
Шаг 4. Если имеет место отношение •>, го произвести свертку. Для этого надо найти на вершине стека все символы, связанные отношением =• («основу»), удалить эти символы из стека. Затем выбрать из грамматики правило, имеющее правую часть, совпадающую с основой, и поместить в стек левую часть выбранного правила (если символов, связанных отношением = <$]command>, на верхушке стека нет, то в качестве основы используется один, самый верхний символ стека). Если правило, совпадающее с основой, найти не удалось, то необходимо прервать выполнение алгоритма и сообщить об ошибке, иначе, если разбор нс закончен, то вернуться к шагу 2.
Шаг 5. Если не установлено ни одно отношение предшествования между текущим символом входной цепочки и символом на верхушке стека, то надо прервать выполнение алгоритма и сообщить об ошибке.
Ошибка в процессе выполнения алгоритма возникает, когда невозможно выполнить очередной шаг — например, если нс установлено отношение предшествования между двумя сравниваемыми символами (на шагах 2 и 4) или если нс удается найти нужное правило в грамматике (па шаге 4). Тогда выполнение алгоритма немедленно прерывается.
Пример распознавателя для грамматики простого предшествования
Рассмотрим в качестве примера грамматику G({+,-,/,*.a.b}. {S.R.T.F.E}, Р, S) с правилами:
Р:
S -> TR | Т
R -> +Т | -Т | HR | -TR
Т -» ЕЕ | Е
F -> *Е | /Е | *ЕЕ | /ЕЕ
Е -» (S) | а | b
Эта иелсворекурсивная грамматика для арифметических выражений над символами а и b уже несколько раз использовалась в качестве примера для построения распознавателей (см. раздел «Распознаватели КС-языков с возвратом», глава 11). Хотя эта грамматика и содержит ценные правила, легко увидеть, что она не содержит циклов, совпадающих правых частей правил и Х-правил, следовательно, по формальным признакам ее можно отнести к грамматикам простого предшествования. Осталось определить отношения предшествования.
Построим множества крайних левых и крайних правых символов относительно нетерминальных символов грамматики.
1. Шаг 1.
L0(S) = {T}L0(R) = {+, -} L0(T) = {E}L0(F)-{*. /} UE) = {(, a, b}2. Шаг 2.L.(S)-{T, E} L,(R) = {+. -}UT) - {E, (, a, b) L^F) - Г, /} L,(E)-{(,a, b)L0(S) = {T}L0(R) = {+, -} L0(T) = {E}L0(F)-{*. /} UE) = {(, a, b}2. Шаг 2.L.(S)-{T, E} L,(R) = {+. -}UT) - {E, (, a, b) L^F) - Г, /} L,(E)-{(,a, b)R0(S) = {R, Т} R0(R) - {R, Т} R0(T)-{E,F} R0(F) = {E, F} Ro(E) = {), a, b}R0(S) = {R, Т} R0(R) - {R, Т} R0(T)-{E,F} R0(F) = {E, F} Ro(E) = {), a, b}R,(S) - {R, T, E, F} R((R) = {R, T, E, F} R,(T) - (E, F, ), a, b} ^(Е)-{Е, F, ), a, b } R(E) = {), a, b}R,(S) - {R, T, E, F} R((R) = {R, T, E, F} R,(T) - (E, F, ), a, b} ^(Е)-{Е, F, ), a, b } R(E) = {), a, b}
L0(S) = {T}
L0(R) = {+, -} L0(T) = {E}
L0(F)-{*. /} UE) = {(, a, b}
2. Шаг 2.
L.(S)-{T, E} L,(R) = {+. -}
UT) - {E, (, a, b) L^F) - Г, /} L,(E)-{(,a, b)
L0(S) = {T}
L0(R) = {+, -} L0(T) = {E}
L0(F)-{*. /} UE) = {(, a, b}
2. Шаг 2.
L.(S)-{T, E} L,(R) = {+. -}
UT) - {E, (, a, b) L^F) - Г, /} L,(E)-{(,a, b)
R0(S) = {R, Т} R0(R) - {R, Т} R0(T)-{E,F} R0(F) = {E, F} Ro(E) = {), a, b}
R0(S) = {R, Т} R0(R) - {R, Т} R0(T)-{E,F} R0(F) = {E, F} Ro(E) = {), a, b}
R,(S) - {R, T, E, F} R((R) = {R, T, E, F} R,(T) - (E, F, ), a, b} ^(Е)-{Е, F, ), a, b } R(E) = {), a, b}
R,(S) - {R, T, E, F} R((R) = {R, T, E, F} R,(T) - (E, F, ), a, b} ^(Е)-{Е, F, ), a, b } R(E) = {), a, b}
Шаг 3. Имеется US) * L^S), возвращаемся к шагу 2.
4. Шаг 2.US) = {T, E, (, a, b} UR)-{+, -}L (T) = {E, (, a, b}L2(F) = {*, /}L2(E) = {(, a, b}4. Шаг 2.US) = {T, E, (, a, b} UR)-{+, -}L (T) = {E, (, a, b}L2(F) = {*, /}L2(E) = {(, a, b}R2(S) - {R, T, E, F, ), a, b} R2(R) - {R, T, E, F, ), a, b} R2(T)-{E, F, ), a, b} R2(F) - {E, F, ), a, b } R(E)-{), a, b}R2(S) - {R, T, E, F, ), a, b} R2(R) - {R, T, E, F, ), a, b} R2(T)-{E, F, ), a, b} R2(F) - {E, F, ), a, b } R(E)-{), a, b}
4. Шаг 2.
US) = {T, E, (, a, b} UR)-{+, -}
L (T) = {E, (, a, b}
L2(F) = {*, /}
L2(E) = {(, a, b}
4. Шаг 2.
US) = {T, E, (, a, b} UR)-{+, -}
L (T) = {E, (, a, b}
L2(F) = {*, /}
L2(E) = {(, a, b}
R2(S) - {R, T, E, F, ), a, b} R2(R) - {R, T, E, F, ), a, b} R2(T)-{E, F, ), a, b} R2(F) - {E, F, ), a, b } R(E)-{), a, b}
R2(S) - {R, T, E, F, ), a, b} R2(R) - {R, T, E, F, ), a, b} R2(T)-{E, F, ), a, b} R2(F) - {E, F, ), a, b } R(E)-{), a, b}
Шаг 3. Имеется L^S) * L2(S), возвращаемся к шагу 2.
Шаг 2.
L3(S) = {T, E, (, a, b} k(R) = {+, -} L3(T)-{E, (, a, b} UF) = {*,/}
L3(E) - {(, a, b}
R (S) - {R, T, E, F, ), a, b) R3(R) - {R, T, E, F, ), a, b) R3(T)-{E,F,), a, b} R3(F) - {E, F, ), a, b } R3(E) = {), a, b}
Шаг. 3. Ни одно множество не изменилось, построение закончено. Результат:
L(S) - {Т, Е, (, а, Ь} L(R) = {+, -} L(T)-{E,(,a, b} L(F)-{* /} L(E) - {(, a, b}L(S) - {Т, Е, (, а, Ь} L(R) = {+, -} L(T)-{E,(,a, b} L(F)-{* /} L(E) - {(, a, b}R(S) - {R, T, E, F, ), a, b} R(R) = {R, T, E, F, ), a, b} R(T) - {E, F, ), a, b} R(F) - {E, F, ), a, b } R(E) = {), a, b}
L(S) - {Т, Е, (, а, Ь} L(R) = {+, -} L(T)-{E,(,a, b} L(F)-{* /} L(E) - {(, a, b}
L(S) - {Т, Е, (, а, Ь} L(R) = {+, -} L(T)-{E,(,a, b} L(F)-{* /} L(E) - {(, a, b}
Ila основании построенных множеств крайних левых и крайних правых символов и правил грамматики построим матрицу предшествования. Результат приведен в табл. 12.4.
Таблица 12 4 Таблица предшествования для грамматики простого предшествования
*
-
•
/
(
)
а
ь
S
R
т
F
Е
-L,
+
<.
<•
<•
<•
-
<•
<•
<-
<.
*
<.
<-
<.
/
<.
<-
=.
(
<•
<.
<.
<.
<•
)
•>
>
->
•>
.>
>
>
>
а
>
>
>
>
>
>
>
>
ь
>
>
>
>
>
>
.>
>
S
R
>
>
т
<.
<.
>
>
F
>
.>
>
>
•>
Е
>
>
<-
<•
->
>
—
>
1„
<•
<-
<•
<•
<.
Поясним построение таблицы на примере символа +
Во-первых, в правилах грамматики R —> +Т| +TR символ + стоит слева от символа Т. Значит, в строке символа + в столбце, соответствующем символу Т, ставим знак —. Кроме того, во множество L(T) входят символы Е, (, a, b. Tonia в строке символа + во всех столбцах, соответствующих этим четырем символам, ставим знак <-.
Во-вторых, символ + входит во множество L(R), а в грамматике имеются правила вида S -> TR и R -> +TR | -TR. Следовательно, надо рассмотреть также мио-
жсство R(T). Туда входят символы Е, F, ), а, b Значит, в столбце символа + во всех строках, соответствующих этим пяти символам, ставим знак •>. Больше символ + ни в какие множества не входит и ни в каких правилах нс встречается. Продолжая эти рассуждения для остальных терминальных и нетерминальных символов грамматики, заполняем все ячейки матрицы предшествования, приведенной выше. Если окажется, что согласно логике рассуждений в какую-либо клетку матрицы предшествования необходимо поместить более чем один-едии- ствеипый знак =•, <• или •>, то это означает, что исходная грамматика нс является грамматикой простого предшествования.
Отдельно можно рассмотреть заполнение строки для символа ±„ (начало строки) и столбца для символа ±к (конец строки). Множество L(S), где S — целевой символ, содержит символы Т, Е, (, а, Ь. Помещаем знак <■ в строку, соответствующую символу ±„ для всех пяти столбцов, соответствующих этим символам. Аналогично, множество R(S) содержит символы R, Т, Е, F, ), а, b Помещаем знак •> в столбец, соответствующий символу ±к для всех семи строк, соответствующих этим символам
Рассмотрим работу алгоритма распознавания на примерах. Последовательность разбора будем записывать в виде последовательности конфигураций МП-авто- мата из трех составляющих:
не просмотренная автоматом часть входной цепочки;
содержимое стека;
последовательность примененных правил грамматики.
Так как автомат имеет только одно состояние, то для определения его конфигурации достаточно двух составляющих — положения считывающей головки во входной цепочке и содержимого стека. Последовательность номеров правил песет дополнительную полезную информацию, по которой можно построить цепочку или дерево вывода (кроме того, последовательность примененных правил делает пример более наглядным). Правила в грамматике нумеруются в направлении слева направо и сверху вниз (всего в грамматике имеется 15 правил).
Будем обозначать такт автомата символом -ь. Введем также дополнительное обозначение -ь,„ если на данном такте выполнялся перенос, и ч-с, если выполнялась свертка
Последовательности разбора цепочек входных символов будут, таким образом, иметь вид.
Пример 1. Входная цепочка а+а*Ь
(а+а*Ык; 1„; 0} ^,
{+а*Ь±к; ±„а; 0} +с
{+а*Ык; 1, Е; 14} ч-с
{+а*Ык; 1„Т; 14,8} ^
{а*Ь1к; ±„Т+; 14,8} ♦„
{*МК; ЕД+а; 14,8} +с
{*Ь±К; 1„Т+Е; 14,8,14} ч-„
{Ык; 1„Т+Е*; 14 8,14} ^„
(1К; ±„Т+Е*Ь; 14,8,14} ч-с
{±к; 1„Т+Е*Е; 14,8,14,15} ч-с
{±к; 1„Т+ЕЕ; 14,8,14,15,9} +с
{±к; 1Д+Т; 14,8,14,15,9,7} +с
{1К; 1„TR: 14,8,14,15,9,7,3} +с
(±к; ±„S; 14,8,14,15,9,7,3,1}, алгоритм завершен, цепочка принята.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S => TR => Т+Т => T+EF => Т+Е*Е => Тч-Е*Ь => Т+а*Ь => Е+а*Ь => а+а*Ь.
Дерево вывода, соответствующее этой цепочке, приведено па рис. 12.6.
Рис 12.6. Первый пример дерева вывода для грамматикипростого предшествования
Пример 2. Входная цепочка (а+а)*Ь.
{(а+а)*Ык; 1„; 0} +„
{а+а)*Ык; 1Н(; 0}
(+а)*Ык; 1„(а; 0} +с
(+а)*Ык; ±„(Е; 14} +с
{+а)*Ык; 1„(Т; 14,8} ^,
(а)*Ык; ±Н(Т+; 14,8} +,,
{)*ЫК; ±„(Т+а; 14,8} ^с
{)*ЫК; ±„(Т+Е; 14,8,14} ^с
()*ЫК; 1„(Т+Т; 14,8,14,8} +с
{)*ЫК; 1„(TR; 14,8,14,8,3}+с
И. {)*ЫК; ±„(S; 14,8,14,8,3,1}+„
fb±K; 1„(S); 14,8,14,8,3,1} -s.
{*ЫК; ±„Е; 14,8,14,8,3,1,13}
{Ык; ±„Е*; 14,8,14.8,3,1,13}
(1К;1„Е*Ь; 14,8,14,8,3,1,13}-^с
{±К;1„Е*Е; 14,8,14,8,3,1,13,15}^с
(±к; 1„ЕЕ; 14,8,14,8,3,1,13,15,9} *с
{±к; 1„Т; 14,8,14,8,3,1,13,15,9,7} *с
(±к; ±„S; 14,8,14,8,3,1,13,15,9,7,2}, алгоритм завершен, цепочка принята.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S => Т => ЕЕ => Е*Е => E*b => (S)*b => (TR)*b => (T+T)*b => (Т+Е)*Ь => (Т+а)*Ь => (Е+а)*Ь => (а+а)*Ь.
Дерево вывода, соответствующее этой цепочке, приведено на рис. 12.7.
Рис. 12.7. Второй пример дерева вывода для грамматики простого предшествования
Пример 3. Входная цепочка а+а*
(а+а*±к; ±н; 0} ^
{+а*1к; ±„а; 0}+с
(+а*1к; ±„Е; 14} +с
{+а*1к; ±„Т; 14,8}
{а*1к; ±„Т+; 14,8}
(*1К; ±„Т+а; 14,8} ^с
{*±к; 1„Т+Е; 14,8,14}
{1К; 1НТ+Е*; 14,8,14}
Ошибка! (Нет отношении предшествования между символами * и ±к.)
Пример 4. Входная цепочка а+а)*Ь.
{а+а)*Ь±к 1и; 0} -г,,
{+а)*Ык; 1„а; 0} ^с
{+a)*blK; 1„Е; 14} +с
{+а)*Ык; ±НТ; 14,8}
{а)*Ык; 1„Т+; 14,8}
(ГЫК; ±„Т+а; 14,8} -
{)*Ы • 1ИТ+Е; 14,8,14} +с
{)*ЫК; ±„Т+Т; 14,8,14,8}
()*blK; ±„TR; 14,8,14,8,3} +с
{)*blK; 1„S; 14,8,14,8,3,1}
И. {*blK; ±„S); 14,8,14,8,3,1}-5-е
Ошибка! (Невозможно выбрать правило для свертки на этом шаге.)
Грамматики простого предшествования являются удобным механизмом для анализа входных цепочек КС-языков. Распознаватель для этого класса грамматик строить легче, чем для рассмотренных выше LR-грамматик. Однако при этом класс языков, заданных грамматиками простого предшествования уже, чем класс языков, заданных LR-грамматиками. Отсюда ясно, что пе всякий детерминированный КС-язык может быть задан грамматикой простого предшествования, а следовательно, не для каждого детерминированного КС-языка можно построить распознаватель по методу простого предшествования.
У грамматик простого предшествования есть еще один недостаток — при большом количестве терминальных и нетерминальных символов в грамматике матрица предшествования будет иметь значительный объем (при этом следует заметить, что значительная часть ее ячеек может оставаться пустой). Поиск в такой матрице может занять некоторое время, что существенно при работе распознавателя — фак тически время поиска линейно зависит от числа символов в грамматике, а объем матрицы — квадратично. Для того чтобы избежать храпения и обработки таких матриц, можно выполнить «линеаризацию матрицы предшествования». Тогда каждый раз, чтобы установить отношение предшествования между двумя символами, будет выполняться не поиск по матрице, а вычисление некой специально организованной функции. Вопросы линеаризации матрицы предшествования здесь не рассматриваются, с применяемыми при этом методами можно ознакомиться в {5, 6, т. 2, 23, 32].
Грамматики операторного предшествования
Операторной грамматикой называется КС-грамматика без Х-правил, в которой правые части всех правил не содержат смежных нетерминальных символов Для операторной грамматики отношения предшествования можно задать на множестве терминальных символов (включая символы ±„ и ±к).
Грамматикой операторного предшествования называется операторная КС-грам- матикаG(VNVTPS) V-VTVNдля которой выполняются следующие уело вмя:
. Для каждой упорядоченной пары терминальных символов выполняется по более чем одно из трех отношений предшествования:
О а =• Ь, если и только если существует правило A—>xaby еР пли правило А—>хаСЬу, где a,beVT, A,CeVN х,уе¥*
О а <• b, если и только если существует правило А->хаСу еР и вывод C=>*bz или вывод C=>*Dbz, где a,beVT A,C,DeVN x,y,zeV‘
О а > b, если и только если существует правило А->хСЬу еР и вывод С=>*/а или вывод C=>*zaD, где a.beVT A,C,DeVN x,y,zeV*
Различные порождающие правила имеют разные правые части, Х-пранила отсутствуют.
Отношения предшествования для грамматик операторного предшествования определены таким образом, что для них выполняется еще одна особенность правила грамматики операторного предшествования не могут содержать двух смежных нетерминальных символов в правой части То есть в грамматике операторного предшествования G(VN,VT P,S), V - VTuVN не может быть нп одного правила вида: А->хВСу, где A.B.CgVN, х.уеУ' (здесь х и у — это произвольные цепочки символов, могут быть и пустыми).
Для грамматик операторного предшествования также известны следующие свойства:
всякая грамматика операторного предшествования задает детерминированный КС язык (но не всякая грамматика операторного предшествования при этом является однозначной!);
легко проверить, является или нет произвольная КС-грамматика грамматикой операторного предшествования (точно так же, как и для простого предшествования)
Как и для многих других классов грамматик, для грамматик операторного предшествования не существует алгоритма, который бы мог преобразовать произвольную КС-грамматику в грамматику операторного предшествования (или доказать, что преобразование невозможно)
Принцип работы распознавателя для грамматики операторного предшествования аналогичен грамматике простого предшествования, но отношения предшествования проверяются в процессе разбора только между терминальными символами.
1 В литературе отношения операторного предшествования иногда обозначают другими символами, отличными от *<■», «■>» и * », чтобы не путать их с отношениями простого предшествования Например, встречаются обозначения «<°» «°>» и «=°». В евином пособии путаница исключена, поэтому будут использоваться одни и те жо обозначения, хотя, по сути, отношения Предшествования несколько различны
Для грамматики данного вида на основе установленных отношений предшествования также строится матрица предшествования, но она содержит только терминальные символы грамматики.
Для построения этой матрицы удобно ввести множества крайних левых и крайних правых терминальных символов относительно нетерминального символа А - L'(A) или R'(A):
Ll(A) - {t | Э A=>*tz или 3 A=>*Ctz }, где leVT, A.CeVN, zeV"
R‘(A) = {I | 3 A=>*zt или 3 A=>*ztC }, где teVT, A.CeVN, zeV*
Тогда определения отношений операторного предшествования будут выглядеть так:
а -• Ь, если 3 правило A—>xaby еР или правило U->xaCby, где a,beVT, A,CeVN, x.yeV";
а < b, если 3 правило А->хаСу еР и ЬеЬДС), где a.beVT, A,CeVN, x,yeV, □ а •> b, если 3 правило А->хСЬу еР и aeR'(C), где a,beVT, A.CeVN, x.yeV".
В данных определениях цепочки символов x,y,z могут быть и пустыми цепочками. Для нахождения множеств L'(A) и R*(A) предварительно необходимо выполнить построение множеств L(A) и R(A), как это было рассмотрено ранее. Далее для построения L*(A) и R'(A) используется следующий алгоритм: Шаг 1. V AeVN
Рсо(А) - {t | A->ytB или A->yt, teVT BeVN, yeV*},
Ll0(A) - {t | A->Bty или A->ty, teVT, BeVN yeV*}.
Для каждого нетерминального символа А ищем все правила, содержащие А в левой части. Во множество L(A) включаем самый левый терминальный символ из правой части правил, игнорируя нетерминальные символы, а во множество R(A) — самый крайний правый терминальный символ из правой части правил. Переходим к шагу 2.
Шаг 2. V AeVN
R^A) - R , ДА) kj R t(B), V В e (R(A) n VN)
Llj(A) - L1 ,(A) kj L ДВ), V В e (L(A) гк VN)
Для каждого нетерминального символа А если множество L(A) содержит нетерминальные символы грамматики А', А", .... то его надо дополнить символами, входящими в соответствующие множества L‘(A'), L'(A") .. и не входящими в L'(A). Ту же операцию надо выполнить для множеств R(A) и R’(A).
Шаг 3. Если 3 AeVN: R* (А) * Р^ДА) или Ь\(А) * ин(А), то i:-i+l и вернуться к шагу 2, иначе построение закопчено: R(A) = R\(A) и L(A) = U^A).
Если па предыдущем шаге хотя бы одно множество L‘(A) или R'(A) Для некоторого символа грамматики изменилось, то надо вернуться к шагу 2, иначе по- строоппезакончено.
Для практического использования матрицу предшествования дополняют символами ±„ и ±к (начало и конец цепочки). Для них определены следующие отношения предшествования:
±„ <• а, V aeVT, если 3 5=>*ах или 3 S=>*Cax, где S,CeVN, xeV* или если aeL(S);
±к •> а, V aeVT, если В 5=>*ха или 3 5=>*хаС, где S.CeVN, xeV* или если aeR‘(S).
Здесь S — целевой символ грамматики.
Матрица предшествования служит основой для работы распознавателя языка, заданного грамматикой операторного предшествования. Поскольку она содержит только терминальные символы, то, следовательно, будет иметь меньший размер, чем аналогичная матрица для грамматики простого предшествования. Следует отмстить, что напрямую сравнивать матрицы двух грамматик нельзя — не всякая грамматика простого предшествования является грамматикой операторного предшествования, и наоборот. Например, рассмотренная далее в примере грамматика операторного предшествования не является грамматикой простого предшествования (читатели могут это проверить самостоятельно). Однако если две грамматики эквивалентны и задают один и тот же язык, то их множества терминальных символов должны совпадать. Тогда можно утверждать, что размер матрицы для грамматики операторного предшествования всегда будет меньше, чем размер матрицы эквивалентной ей грамматики простого предшествования.
Все, что было сказано выше о способах хранения матриц для грамматик простого предшествования, в равной степени относится также и к грамматикам операторного предшествования, с той только разницей, что объем хранимой матрицы будет меньше.
Алгоритм «сдвиг-свертка» для грамматики операторного предшествования
Этот алгоритм в целом похож на алгоритм для грамматик простого предшествования, рассмотренный выше. Он также выполняется расширенным МП-автома- том и имеет тс же условия завершения и обнаружения ошибок. Основное отличие состоит в том, что при определении отношения предшествования этот алгоритм не принимает во внимание находящиеся в стеке нетерминальные символы и при сравнении ищет ближайший к верхушке стека терминальный символ. Однако после выполнения сравнения и определения границ основы при поиске правила в грамматике нетерминальные символы следует, безусловно, принимать во внимание.
Алгоритм состоит из следующих шагов.
Шаг 1. Поместить в верхушку стека символ ±,„ считывающую головку — в начало входной цепочки символов.
Шаг 2. Сравнить с помощью отношения предшествования термшжЛьиый сим вол, ближайший к вершине стека (левый символ отношения), с текущим символом входной цепочки, обозреваемым считывающей головкой (правый символ отношения). При этом из стека надо выбрать самый верхний терминальный сим вол, игнорируя все возможные нетерминальные символы.
Шаг 3. Если имеет место отношение <■ или =-, то произвести сдвиг (перепое текущего символа из входной цепочки в стек и сдвиг считывающей головки па один шаг вправо) и вернуться к шагу 2. Иначе перейти к шаг}' 4.
Шаг 4. Если имеет место отношение •>, то произвести свертку. Для этою надо найти на вершине стека все терминальные символы, связанные отношением =• («основу»), а также все соседствующие с ними нетерминальные символы (при определении отношения нетерминальные символы игнорируются). Если терминальных символов, связанных отношением —, па верхушке стека пет, то в качестве основы используется один, самый верхний в стеке терминальный символ стека. Все (и терминальные, и нетерминальные) символы, составляющие основу, надо удалить из стека, а затем выбрать из грамматики правило, имеющее правую часть, совпадающую с основой, и поместись в сгек левую часть выбранного правила. Если правило, совпадающее с основой, найти не удаЯооь то необходимо прервать выполнение алгоритма и сообщить об ошибке, иначе, если разбор не закопчен, то вернуться к шагу 2.
Шаг 5. Если не установлено пи одно отношение предшествования между текущим символом входной цепочки и самым верхним терминальным символом в стоке, то надо прервать выполнение алгоритма и сообщить об ошибке.
Конечная конфигурация данного МИ-автомата совпадает с конфигурацией при распознавании цепочек грамматик простого предшествования.
Пример построения распознавателя для грамматики операторного предшествования
Рассмотрим в качестве примера грамматику для арифметических выражений над символами а и b G({+,-./,*,a,b}. {S.T.E}, Р, S):
Р:
S -» S+T | S-Т | Т
Т -> Т*Е I Т/Е I Е
Е -> (S) | а | Ь
Эта грамматика уже много раз использовалась в качестве примера для построения распознавателей.
Видно, что эта грамматика является грамматикой операторного предшествования.
Построим множества крайних левых и крайних правых символов L(A), R(A) относительно всех нетерминальных символов грамматики. Рассмотрим работу алгоритма построения этих множеств по шагам.
Шаг 1.
L(,(S) = {S, Т},R0(S) — {Т},
МТ) = {Т, Е},R0(T) - {Е},
ME) - {(, a, b},R0(E) - {), a, b}, i - 1.
Шаг 2.
М8)-{8,Т, Е},R,(S) = (T, Е},
L,(T) = {Т, Е, (, a, b}, КДТ) = {Е, ), а, Ь},
L,(E) - {(, а. Ъ},R1(E)-{),a,b}.
Шаг 3. Так как L0(S) * L^S), то i = 2 и возвращаемся к вину 2.
Шаг 2.
L2(S) - {S, Т, Е, (, a. b}, R2(S) - {Г Е, ), а, Ь},
L2(T) - {Т, Е, (, a, b},R2(T)={Е,), а, Ь},
L2(E) - {(, а, Ь},R2(E)-{),а, Ь}.
Шаг 3. Так как Lt(S) * L2(S), то i - 3 и возвращаемся к шагу 2.
Шаг 2.
L(S) - {S, Т, Е, (, а, Ь}, L,(T) = {Т, Е, (, а, Ь}, L$(E) = {(, а, Ь},
Построение закончено
11олучили результат:
L(S) - {S, Т, Е, (, а, Ь}, L(T) - {Т, Е, (, а, Ь}, ЦЕ)-{С а, Ь},
R3(S) = {T, Е, ), а, Ь}, R3(T) = {E, ), а, b}, R,(E)-{), a,b}.R3(S) = {T, Е, ), а, Ь}, R3(T) = {E, ), а, b}, R,(E)-{), a,b}.R(S) = {Т, Е, ), а. Ь}, R(T) = {E, ), а, b), R(E)-{),a, b}.R(S) = {Т, Е, ), а. Ь}, R(T) = {E, ), а, b), R(E)-{),a, b}.На основе полученных множеств построим множества крайних левых и кранных правых терминальных символов L'(A), R'(A) относительно всех нетерминальных символов грамматики. Рассмотрим работу алгоритма построения этих множеств по шагам.
R3(S) = {T, Е, ), а, Ь}, R3(T) = {E, ), а, b}, R,(E)-{), a,b}.
R3(S) = {T, Е, ), а, Ь}, R3(T) = {E, ), а, b}, R,(E)-{), a,b}.
R(S) = {Т, Е, ), а. Ь}, R(T) = {E, ), а, b), R(E)-{),a, b}.
R(S) = {Т, Е, ), а. Ь}, R(T) = {E, ), а, b), R(E)-{),a, b}.
Ro(S) = {+, }. Ro(T) = {*, /}, R'oCEW), a, b}, i- 1
Шаг 2.
L'j(S) = {+, -, *, /, (, a. b^R^S) - {+,*, /},
Ц(Т) - {*, /. (. a. b},RS(T) - {*, /, ), a. b},
U,(E) = {(, a, b),RS(E) - {), a. b}.
Шаг 3. Так как L‘(I(S) * L'^S), то i = 2 и возвращаемся к шагу 2.
Шаг 2.
Ll2(S) - {+, -, *, /, (, a, b},R-2(S) - {+, -, *, /, ), а, b },
L2(T) = {*(,ab},R'2(T)-{*. ),a,b},
и2(Е) = {(, a, b},R‘2(E) = {), a, b}.
Шаг 3. З ак как R'i(S) * Rl2(S), to i - 3 и возвращаемся к Biaiу 2.
Шаг 2.
L'3(S) - {+, -, *, /, (, a, b},RVS) - {+, -, *. /, ), a, b },
LVT) = {*, /, (, a, b},R‘3(T) = {*, /, ), a, b},
L‘3(E) - {(, a. b}, R 3(Е) - {), а, Ь}.
Построение закончено.
Получили результат
L (S) - {+, -, * /. (, а, b}, R (S) - {+, -, ', /, ). а, Ь },
V(T) - {•, /, (, а, b}, R (Т) - Г, /, ), а. Ь},
L (E) - {(, а. b}, R (E)-{),a,b}
На основе этих множеств и правил грамматики G построим матрицу предшествования грамматики (табл 12.5).
Таблица 12.5. Матрица предшествования грамматики
Символы
*
-
—
/
(
)
а
ь
-1,
4-
>
>
<
<
<
>
<•
<
>
-
>
>
<
<
<.
>
<
<.
*>
•
>
>
>
•>
<
.>
<■
<■
•>
/
.>
>
>
>
<-
>
<-
<-
>
(
<
<
<*
<.
<
<■
<-
)
•>
>
>
•>
>
>
а
->
>
>
>
>
>
ь
>
>
->
>
>
.>
<
<
<
<
<.
<.
- - - -
Поясним как заполняется матрица предшествования в таб лице на примере символа +. В правиле грамматики S->S+T (правило 1) этот символ стоит слева от нетерминального символа Т. Во мн жество L‘(T) входя символы * / (, а, Ь. Ставим знак <■ в клетках матрицы, соответствующих этим символам в строке для символа * В то же время в этом же правиле симво * стоит справа от нетерми нально о символа S Во множес во R (S) входят символы ♦,-*/, ), а, Ь. Ставим знак •> в клетках матрицы, соответствующим этим символам в столбце для сим- во ia +. Больше символ * ни в каком правиле не встречается, значит, заполнение матрицы для него закончено, берем следующий символ и продолжаем заполнять матрицу таким же методом, пока не ереберем все терминальные символы.
Отдельно рассмотрим символы ±„ и 1,. В строке символа ±„ ставим знак <• в кле ках симвотов входящих во множес во L (S) Это символы +, -, *, /, (, а, Ь. В столбце символа ставим знак •> в клетках символов входящих во множество R (S) Это символы +, - * /, ), а, Ь.
Еще можно отметить, что в клетке соо ветствующеи открывающей скобки (символ () слева и закрывающей скобке (симвот )) справа помещается знак -• («составляют основу»). Так происходит, поскольку в грамматике присутствует правлю Е—>(S), где эти символы стоят рядом (через нетерминальный символ) в его
правой части. Следует отмстить, что понятия «справа» и «слева» здесь имеют важное значение- в клетке соответствующей закрывающей скобке (символ )), слева и открывающей скобке (символ () справа знак отсутствует — такое сочетание символов недопустимо (отношение (=■) верно, а отношение )=•( — неверно). Алгоритм разбора цепочек грамматики операторного предшествования игнорирует нетерминальные символы Поэтому имеет смысл преобразовать исходную грамматику таким образом, чтобы оставить в ней только один нетерминальный символ. Тогда получим следующий вид правил:
Р:
S -> S+S | S-S | S (правила 1 2 и 3)
5 -» 5*5 | 5/5 | 5 (правила 4. 5 и 6)
5 —» (5) | а | b (правила 7. 6 и 9)
Если теперь исключить бессмысленные правила вида S—>S, то получим следующее множество правил (нумерацию правил сохраним в соответствии с исходной грамматикой):
Р:
5 -> 5+5 5-5 (правила 1 2)
5 -+ 5*5 | 5/5 (правила 4. 5)
5 -> (5) | а | Ь (правила 7. 8 и 9)
Такое преобразование не ведет к созданию эквивалентной грамматики и выполняется только для упрощения работы алгоритма (который при выборе правил все равно игнорирует нетерминальные символы) после построения матрицы предшествования. Полученная в результате преобразования грамматика не яв ляегся однозначной, но в алгоритм распознавания уже были заложены все необходимые данные о порядке применения правил при создании матрицы предшествования, поэтому распознаватель остается детерминированным. Построенная таким способом грамматика называется «остовной» грамматикой. Вывод, полученный при разборе на основе остовной грамматики, называют результатом «остовного» разбора или «остовным» выводом [6, 23, 32].
По результатам остовного разбора можно построить соответствующий ему вывод на основе правил исходной грамматики. Однако эта задача не представляет практического интереса, поскольку остовиый вывод отличается от вывода на ос пове исходной грамматики только тем, что в нем отсутствуют main, связанные с применением цепных правил, и не учитываются типы нетерминальных символов Для компиляторов же распознавание цепочек входного языка заключается не в нахождении того или иного вывода, а в выявлении основных синтаксических конструкции исходной программы с целью построения на их основе цепочек языка результирующей программы. В этом смысле типы нетерминальных символов и цепные правила не несут никакой полезной информации, а нанро- тив только ус южияют обработку цепочки вывода (или дерева вывода). Поэтому для реального компилятора нахождение остовного вывода является даже более полезным, чем нахождение вывода на основе исходной грамматики. Найденный остовнын вывод в дальнейших преобразованиях уже не нуждается (более подробно см. главу «Основные принципы построения трансляторов»)
Рассмотрим работу алгоритма распознавания на примерах. Последовательность разбора будем записывать в виде последовательности конфигураций расширенного МП-автомата из трех составляющих:
не просмотренная автоматом часть входной цепочки;
содержимое стека;
последовательность примененных правил грамматики.
Так как автомат имеет только одно состояние, то для определения его конфигурации достаточно двух составляющих — положения считывающей головки во входной цепочке и содержимого стека. Последовательность номеров правил песет дополнительную полезную информацию, по которой можно построить цепочку или дерево вывода (кроме того, последовательность примененных правил делает пример более наглядным).
Будем обозначать такт автомата символом +. Введем также дополнительное обо- значеинс -=-„, если на данном такте выполнялся перепое, и ч-с, если выполнялась свертка.
Последовательности разбора цепочек входных символов будут, таким образом, иметь вид, приведенный ниже.
Пример 1. Входная цепочка а+а*Ь.
{а+а*Ь±к; -„; 0} +„
(+а*Ь±к; ±„а; 0} +с
(+a*b±K; ±„S; 8}
(a*b±K; ±„5+; 8} *„
{*МК; ±„5+а; 8}
(*ЫК; ±„5+5; 8,8} ч-„
(Ь±к; ±„5+5*; 8,8} +„
(±^ ±„5+5*Ь; 8,8} ч-с
{±к; ±„5+5*5; 8,8,9}
(±к; ±„5+5; 8,8,9,4} ч-с
{±к; ±„5; 8,8,9,4,1} — разбор завершен, цепочка принята.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S => S+S => S+S*S => S+S*b => S+a*b => a+a*b
Дерево вывода, соответствующее этой цепочке, приведено па рис. 12.8.
Следует отмстить, что распознаватель, работающий на основе грамматики операторного предшествования, имеет еще одно весьма полезное свойство — за счет того, что при построении цепочки вывода нс учитываются типы нетерминальных символов, в результате игнорируются цепные правила, присутствующие в грамматике. Это сокращает длину цепочки вывода и размер дерева вывода.
Рис. 12.8. Первый пример дерева вывода для грамматики операторного предшествования
Пример 2. Входная цепочка (а+а)*Ь.
{(а+а)^; ±„; 0} ^,
{а+а)*Ь±к; 1„(; 0} %
{+а)*Ык; ±„(а; 0} ^с
{+а)*Ь1к; ±„(5; 8}
{a)*blK; 1„(S+; 8}
{)*ЫК; ±„(S+a; 8} +с
{)*blK; ±„(S+S: 8,8}
{)*blK;l„(S; 8,8,1} 4-„
(*b±K; 1„(S); 8,8,1}
{*ЫК; 1„S; 8,8,1,7}
И. {b±K;±„S* 8,8,1,7}^,
(1К; l„S*b; 8,8,1,7}
{±к; 1„S*S; 8,8,1,7,9} ч-с
(±к; ±„S; 8,8,1,7,9,4} — разбор завершен, цепочка принята.
Соответствующая цепочка вывода будет иметь вид (используется правосторонний вывод): S => S*S => S*b => (S)*b => (S+S)*b => (S+a)*b => (a+a)*b.
Дерево вывода, соответствующее этой цепочке, приведено на рис. 12.9.
Пример 3. Входная цепочка а+а*.
(а+а*±к; ±,„ 0} +,,
{ч-а*±к; ±„а; 0} ^с
{+а*±к; ±„S; 8}
{a*lK; ±„S+; 8} ^п
{*±к; ±„S+a; 8}
{*±к; ±„S+S; 8,8}
{±к; ±HS+S*; 8,8}
Ошибка! (Нет правила для выполнения свертки па этом шаге.)
Рис. 12 9. Второй пример дерева вывода для грамматики операторного предшествования
Пример 4. Входная цепочка а+а)*Ь.
{а+а)*Ык; 1„; 0} н-п
{+а)*Ык; 1на; 0}
{+а)*Ык; ±„S; 8}
{a)*b; ±„S+; 7} ч-„
{)*b±K; ±„S+a; 8}
{)*Ь±К; ±„S+S; 8,8}
{ГЬЦ; ±,,8; 8.8,1}
Ошибка! (Нет отношений предшествования между символами ±„ и ).)
Два первых примера наглядно демонстрируют, что приоритет операций, установленный в грамматике, влияет па последовательность разбора и последовательность применения правил, несмотря па то что нетерминальные символы распознавателем нс учитываются.
Как было сказано выше, матрица для грамматики операторного предшествования всегда имеет меньший объем, чем матрица для эквивалентной ей грамматики простого предшествования (поскольку в первую из них входят только терминальные символы, а во вторую также и нетерминальные). Кроме того, распознаватель грамматики операторного предшествования игнорирует нетерминальные символы в процессе разбора, а значит, не учитывает цепные правила, делает меньше шагов и порождает более короткую цепочку вывода. Поэтому распознаватель для грамматики операторного предшествования всегда проще, чем распознаватель для эквивалентной ей грамматики простого предшествования.
Интересно, что поскольку распознаватель па основе грамматик операторного предшествования не учитывает типы нетерминальных символов, то он может работать даже с некоторыми неоднозначными грамматиками, в которых есть правила, различающиеся только типами нетерминальных символов. Примером такой грамматики может служить грамматика G"({a,b}, {S,A,B}, Р, S) с правилами:
Р:
S -> А | В
А -> aAb | ab
В -> aBb | ab
Как и для любой другой грамматики операторного предшествования, распознаватель для этой грамматики будет детерминированным Остовная грамматика, построенная па ее основе, будет иметь только два правила вида: S->aSb| ab. Неоднозначность заключается в тем, что каждому найденному остовиому выводу будет соответствовать не одни, а несколько выводов в мсходной грамматике (в данном случае — всегда два вывода в зависимости от того, какое правило из S->A|B будет применено на первом шаге вывода). Грамматики, содержащие правила, различающиеся только типами нетерминальных символов практического значения не имеют, а потому ии гереса для компиляторов не представляют.
К сожалению, хотя классы грамматик простого и операторного предшествования несопоставимы, но класс языков операторного предшествования уже, чем класс языков простого предшествования. Поэтому не всегда возможно для языка, заданного грамматикой простого предшествования, построить грамматику операторного предшествования. Соответственно, поскольку класс языков, заданных грамматиками операторного предшествования, еще более узок, чем даже класс языков, заданных грамматиками простого предшествования, то с помощью этих грамматик можно определить далеко не каждый детерминированный КС-язык. Грамматики операторного предшестновация — эго очень удобный инструмент для построения распознавателей, но они имеют ограниченную область применения.
Соотношение классов КС-языков и КС-грамматик
В этом подразделе рассматриваются вопросы, связанные с отношениями между различными классами известных КС-языков и КС-грамматик. Эти соотношения представляются весьма интересными с точки зрения теории формальных языков Многие из них совсем неочевидны. Однако читатель, интересующийся только практическими аспектами построения трансляторов п компиляторов, может в принципе пропустить этот подраздел.
Особенности восходящих и нисходящих распознавателей
Выше были рассмотрены варианты двух основных типов распознавателей для цепочек КС-языков — восходящих и нисходящих. Далее можно не рассматривать распознаватели с возвратом и табличные распознаватели, поскольку их практическое применение сильно ограничено. Реальные компиляторы пестроят-
ся па основе универсальных распознавателей из-за их высокой требовательности К необходимым вычислительным ресурсам. Как правило, конкретную КС грамматику и заданный ею КС-язык удается отнести к тому пли иному классу, для которого существует специального рода распознаватель с линейной зависимостью требуемых вычислительных ресурсов от длины входной цепочки — линейный распознаватель.
Линейные распознана гели (распознаватели без возвратов) существуют и в виде восходящих, и в виде нисходящих распознавателей. И те и другие имеют линейную зависимость вычислительных ресурсов, необходимых для выполнения алгоритмов разбора, от длины входной цепочки символов. Существуют также другие варианты линейных распознавателей, сочетающие в себе некоторые черты восходящего и нисходящего разбора [6, т. 1].
Но возникает другой вопрос, очень часто одна и га же КС-грамматика может быть отнесена не к одному, а сразу к нескольким классам грамматик, допускающих построение линейных распознавателей. Иногда для этого надо выполнить некоторые преобразования правил грамматики*. В этом случае необходимо решить, какой из нескольких возможных распознавателей выбрать для практической реализации.
Если можно выбрать один из двух явно сопоставимых по сложности распознавателей (например, распознаватели ЕК(1)-грамматик н грамматик предшествования можно сопоставить по объему управляющих таблиц, так как они используют схожие алгоритмы работы), тогда ответ очевиден. Однако ответить па этот вопрос не всегда легко, поскольку могут быть построены два принципиально разных распознавателя, алгоритмы работы которых несопоставимы. В первую очередь речь идет именно о восходящих и нисходящих распознавателях: в основе первых лежит алгоритм подбора альтернатив, в основе вторых — алгоритм «сдвиг-свертка».
На вопрос о том, какой распознаватель — нисходящий или восходящий — выбрать для построения распознавателя, нет однозначного ответа. Эту проблему необходимо решать, опираясь на некую дополнительную информацию о том, как будут использованы или каким образом будут обработаны результаты работы распознавателя. Тут следует вспомнить, что распознаватель входных цепочек КС-языков — это всего лишь один из этапов компиляции, составная часть компилятора, лежащая в основе синтаксического анализатора входного языка программирования. И с этой точки зрения результаты работы распознавателя служат исходными данными для различных этапов компиляции: прежде всею синтаксического анализа, затем — генерации кода. Поэтому выбор того или иного распознавателя во многом зависит от реализации компилятора, от того, какие принципы положены в его основу.
Восходящий синтаксический анализ, как правило, привлекательнее нисходящего, так как для данного языка программирования часто легче nocipoi гь ирапо-
1 Примером такой грамматики может служить грамматика арифметических выражений, которая была впервые упомянута в разделе «Проблемы однозначности и эквивалентности грамматик», глава 9, а потом многократно использовалась в качестве примера для иллюстрации работы различных распознавателей
сторонний (восходящий) распознаватель на основе правоанализируемой грамматики. С этим вопросом мы уже сталкивались в данном пособии, когда говорили о том, что класс языков, заданных LR-грамматиками, шире, чем класс языков, заданных LL-фамматиками (хотя следует сказать, что не все здесь столь однозначно).
С другой стороны, как будет показано далее, левосторонний (нисходящий) синтаксический анализ предпочтителен с точки зрения процесса трансляции, поскольку на его основе легче организовать процесс порождения цепочек результирующего языка. Ведь в задачу компилятора входит не только распознать (проанализировать) входную программу па входном языке, но и построить (синтезировать) результирующую программу. Более подробную информацию об этом можно получить в разделе «Генерация кода. Методы генерации кода», глава 14 или в работах 16, г. 2, 42]. Левосторонний анализ, основанный па нисходящем распознавателе, оказывается предпочтительным также при учете вопросов, связанных с обнаружением и локализацией ошибок в тексте исходной программы [40, 82].
Желание использовать более простой класс грамматик для построения распознавателя может потребовать каких-то манипуляций с заданной грамматикой, необходимых для се преобразования к требуемому классу. При этом нередко грамматика становится неестественной и мало попятной, что в дальнейшем затрудняет ее использование в схеме синтаксически управляемого перевода и Трансляции на этапе генерации результирующего кода (см. главу «Генерация и оптимизация кода»). Поэтому часто бывает удобным использовать исходную грамматику такой, какая она есть, не стремясь преобразовать ее к более простому классу.
В целом следует отметить, что с учетом всего сказанного выше, интерес представляют как левосторонний, так и правосторонний анализ. Конкретный выбор зависит от реализации конкретного компилятора, а также от сложности грамматики входного языка программирования.
Отношения между классами КС-грамматик
В данном учебном пособии было рассмотрено несколько основных классов КС- фамматик. для которых существуют линейные распознаватели, — это LL-грамматики, LR-грамматики и грамматики предшествования. Не все они были рассмотрены достаточно подробно, к тому же этими классами далеко не нечерпыва ется список всех известных КС-грамматик такого рода.
Можно еще. например, упомянуть класс фамматик ограниченного правого контекста (пт,п) - ОПК(ш,п). Это грамматики, допускающие построение распознавателя, основанного па алгоритме «сдвиг-свертка», в котором однозначный выбор между сдвигом и сверткой делается исходя из анализа m символов, находящихся на верхушке стека, и п текущих символов входной цепочки, считая от положения считывающей головки расширенного МП-автомата [6, т. 1]. Все ОПК(ш.п)- фамматикп для всех значений пт и п составляют класс ОПК-грамматик. Простейшим вариантом грамматик такого класса являются ОПК(1,1)-грамматики. Интересно, что с помощью этих грамматик, как и с помощью LR-фамматик, можно определить любой детерминированный КС-язык.
Далее в этом пункте будут рассмотре ны различные классы КСлрамматик и существующие между ними нетривиальные соотношения.
В общем случае можно выделить нравоанализируемые и левоанализируемые КС- грамматики. Об этих двух принципиально разных классах грамматик уже говорилось выше: первые предполагают построение левое тороннего (восходящего) распознавателя, вторые — правостороннего (нисходящего). Это вовсе не значит, что для КС-языка, заданного, например, некоторой левоаналнбируе-мой грамматикой, невозможно построить расширенный МП-автомат, который порождает правосторонний вывод. Указанное разделение грамматик относится только к построению на их основе детерминированных МП-автоматов и детерминированных расширенных МП-автоматов. Только эти типы автоматов представляют интерес при создании комииляторов и анализе входных цепочек языков программирования. Недетерминированные автоматы, порождающие как левосторонние, так и правосторонние выводы, можно построить в любом случае для языкг! заданного любой КС-грамматикой, по для создания компилятора такие автоматы миТереса не представляют (см. раздел «Распознаватели КС-языков. Автоматы с магазинной памятью», глава И).
На рис. 12.10 изображена условная схема, дающая представление о соотношении классов левоанализируемых и правоанализируемых КС-грамматик 15, G, т. 2, 42, 65]
Рис. 12.10. Соотношение классов левоанализируемых и правоанализируемых КС-грамматик
Интересно, что классы левоанализируемых и правоанализируемых грамматик являются несопоставимыми. То есть существуют левоапализпруимые КС-грамматики, на основе которых нельзя построить детерминированный расширенный МП-автомат, порождающий правосторонний вывод; и наоборот — существуют правоапалнзнруемые КС-грамматики, не допускающие построение МП-автома- та, порождающего левосторонний вывод. Конечно, существуют грамматики, подпадающие под оба класса и допускающие построение детерминированных автоматов как с правосторонним, так и с левосторонним выводом.
Следует помнить также, чго все упомянутые классы КС-грамматик — это счетные, но бесконечные множества. Нельзя построить и рассмотреть все возможные левоанализируемые грамматики или даже все возможные LL(1) грамматики. Сопоставление классов КС грамматик производится исключительно на основе анализа структуры их правил. Только па основании такого рода анализа произвольная КС-грамматика может быть отнесена в тот или иной класс (или несколько классов).
Все это тем более интересно, если вспомнить, что рассмотренный в данном пособии класс левоанализирусмых LL-грамматик является собственным подмножеством класса LR-грамматик: любая LL-грамматика является LR-грамматлкой, ио не наоборот — существуют LR-грамматики, которые не являются LL-грамма- тикамн. Этот факт также нашел свое страже ше в схеме на рис 12.10. Значит, любая LL-грамматика является правоанализируемои, но существуют также и другие левоанализируемые грамматики, не попадающие в класс правоанмизи- руемых грамматик.
Для Ы-(к)-грамматик, составляющих класс LL-грамматик, интересна еще одна особенность: доказано, что всегда существует язык, который может быть задан LL(k)-rpaMMaTHKOH для некоторою к > 0, но не может быть задай LL(k- 1)-грам- магикой. Таким образом, все кк(к)-грамматики для всех к представляют определенный интерес (другое дело, что распознаватели для них при больших значениях к будут слишком сложны). Интересно, что проблема эквивалентности для двух 1^(к)-грамматик разрешима.
С другой стороны, для LR(k)-rpaMMaTiiK, составляющих класс LR-грамматик, доказано, что любой язык, заданный LR(k)-rpaMMaTHKon с k > 1, может быть задал кК(1)-грамматикой. То есть LR(k)-rpaMManiKii с k> 1 интереса не представляют. Однако доказательство существования LR(1) грамматики вовсе не означает, что такая грамматика всегда может быть построена (проблема преобразования КС-грамматик неразрешима).
На рис. 12.11 условно показана связь между некоторыми классами КС грамматик, упомянутых в данном пособии. Из этой схемы видно, например, что любая ОПК-грамматика является LR-грамматикой, а также любая LL-грамматика является LR-грамматикоп, но не всякая LL-грамматика является LR( ^-грамматикой.
Рис. 12.11. Схема взаимосвязи некоторых классов КС-грамматик
Если вспомнить, что любой детерминированный КС-язык может быть задан, например, ЕК(1)-грамматикой (или ОПК(1,1)-грамматикой), но в то же время, классы левоаиализируемых и правоанализируемых грамматик несопоставимы, то напрашивается вывод: один и тот же детерминированный КС-язык может быть задан двумя или бочее несопоставимыми между собой грамматиками. Таким образом, можно вернуться к мысли о том, что пробтема преобразования КС- грамматик неразрешима (на самом де ie, конечно, наоборот: из неразрешимости проблемы преобразования КС-грамматик следует возможность задать один и тот же КС-язык двумя несопоставимыми грамматиками). Эго, наверное, самый интересный вывод, который можно сделать из сопоставления разных классов КС- грамматик.
Отношения между классами КС-языков
КС-язык называется языком некоторого класса КС-языков, если он может быть задан КС-грамматикой из данного класса КС-грамматик. Например, класс LL- языков составляют все языки, которые могут быть заданы с помощью LL-грам- матнк.
Соотношение классов КС-языков представляет определенный интерес, оно не совпадает с соотношением классов КС-грамматик. Эго связано с многократно уже упоминавшейся проблемой преобразования грамматик. Например, выше уже говорилось о том, что любой LL-язык является и ЕК(1)-языком — то есть язык, заданный LL-грамматикой, может быть задан также и LR( 1 )-грамыатикой. Однако не всякая LL-грамматика является при этом ЕК(1)-грамматикой и нс всегда можно найти способ, как построить ЕК(1)-грамматнку, задающую тог же самый язык, что и исходная LL-грамматика.
На рис. 12.12 приведено соотношение между некоторыми известными классами КС-языков [6, г. 2, 42, 47].
КС языки
Детерминированные КС-языки
LR(1) языки-LR языки
(1,1 )ОПК-языки~ОПК-языки
Языки простогопредшествования
LL-языки
Языкиоператорногопредшествования
Рис. 12.12. Соотношение между различными классами КС-языков
Следует обратить внимание прежде всего на то, что интересующий разработчиков компи чяторов в первую очередь класс детерминированных КС-языков
полностью совпадает с классом LR-языков и, более того, совпадает с классом LR( 1‘)-языков. То есть доказано, что для любого детерминированного КС-языка существует задающая его LR( 1 )-грамматика. Этот факт уже упоминался выше. Проблема состоит в том, чю не всегда возможно найти такую грамматику и нет формализованного алгоритма, как ее построить в общем случае. То же самое относится к упоминавшимся здесь ОПК-грамматикам и ОПК(1,1)-граммагикам.
Также уже упоминалось, что LL-языки являются собственным подмножеством LR-языков: всякий LL-язык является одновременно LR-языком, но существуют LR языки, которые нс являются LL-языками. Поэтому LL-языки образуют более узкий класс, чем LR-языки.
Языки простого предшествования, в свою очередь, также являются собственным подмножеством LR-языков, а языки операторного предшествования — собственным подмножеством языков простого предшествования. Интересно, что языки операторного предшествования представляют собой более узкий класс, чем языки простого предшествования.
В то же время языки простого предшествования и LL-языки несопоставимы между собой: существуют языки простого предшествования, которые не являются LL-языками, и в то же время существуют LL-языки, которые не являются языками простого предшествования. Однако существуют языки, которые однонремен но являются и языками простого предшествования, и LL-языками Аналогичное замечание относится также к соотношению между собой языков операторного предшествования и LL-языков.
Можно еще отметить, чго язык арифметических выражений над символами а и Ь, заданный фамматыкой G({+,-,/,*.a,b},{S,T.E},P,S), Р = {S->S+T|S-T|T, Т^Т*Е|Т/Е|Е. Е—>(S)|a|b}, который многократно ш пользовался в примерах в данном учебном пособии, подпадает иод все указанные выше классы языков. Из приведенных ранее по всей главе 3 примеров можно заключить, что этот язык является и LL- языком, и языком операторного предшествования, а следовательно, и языком простого предшествования и, конечно, LR(l)-H3biKOM. В то же время этот язык по мере изложения материала пособия описывался различными грамматиками, не все из которых могу г быть отнесены в указанные классы. Более того, в главе 1 он был задан с помощью грамматики, которая не являлась даже однозначной.
Таким образом, соотношение классов КС-языков не совпадает с соотношением задающих их классов КС-грамматик. Это связано с неразрешимостью проблем преобразования и эквивалентности грамматик, которые не имеют строго формализованного решения.
Контрольные вопросы и задачи
Вопросы
1 На алгоритме какого распознавателя основан метод рекурсивного спуска?
В чем заключается принципиальное изменение, внесенное в алгоритм? Ведет ли это изменение к ограничению применимости данного алгоритма?
Какие преобразования возможно выполнить над грамматикой, чтобы к ней был применим метод рекурсивного спуска? Можно ли формализовав и автоматизировать выполнение этих преобразований?
За счет чего можно расширить применимость метода рекурсивного спуска? Как влияет рекурсия, присутствующая в грамматике, на применимость данного метода? Можно ли реализовать распознаватель по методу расширенного рекурсивного спуска для грамматики, содержащей левую рекурсию?
За счет чего класс ЬЦ1)-грамматик является более широким, чем класс КС- грамматик, для которых можно построить распознаватель по методу рекурсивного спуска?
На каком алгоритме основана работа распознавателя для LL(k)-i рамматики? Какие изменения внесены в алгоритм? Насколько они сузили применимость данного алгоритма?
Объясните работу алгоритмов построения множеств FIRST(l.A) и FOLLOW(1,A). Можно ли эти алгоритмы тривиально распространить на множества FlRST(k.A) и FOLLOW(k,A)? Если нет, то почему?
Почему класс языков, заданных LR-грамматиками, является бочее широким, чем класс языков, заданных LL-грамматиками?
На каком алгоритме основана работа распознавателя для LR(k)-i pa.MMaTHKn? Какие изменения внесены в алюригм? Насколько они сузили применимость данного алгоритма?
Почему не существует языков, заданных ЕЕ(О)-грамматнкамп. но существуют языки, заданные ЕК(О)-грамматиками?
Почему можно утверждать, что любая LL-грамматика и любая LR-грамматика являются однозначными?
И В чем особенности реализации алгоритма типа «сдуш -свертка» Для грамматик простою и операторного предшествования? Какие ограничения эти особенности накладывают на правила грамматики?
Почему в грамматике простого предшествования не могут присутствовать ценные правила, а в грамматике операторного предшествования — могут? Почему в обоих классах грамматик не могут присутствовать л-правича?
Распознаватели на основе грамматик операторного предшествования имеют массу преимуществ перед распо павателямн па основе грамматик простого предшествования. Почему же тем не менее используются оба класса КС- грамматик?
Класс языков, заданных LR( 1)-грамм, тиками, совпадает с классом детерминированных КС-языков. Зачем же тогда используются другие классы грамматик?
Как объяснить тог факт, что любой язык, заданный LL раммтгикон, может быть задан и ЕК(1)чрамматикоп, но не всякая LL-граммагнка при этом является LR(1) грамматикой?
Почему любая LL-грамматика является LR грамматикой?
Задачи
Постройте распознаватель, основанный на методе рекурсивного спуска для грамматики: G({a,b,с}, {S,A,B,C}, Р S)
Р:
S -> аАВЬ | ЬВАа | сСс
А -» аА | ЬВ | сС
В -> b | аАС
С -> аА | ЬА | сС
выполните разбор цепочки символов «aabbabbcabbb».
Постройте распознаватель, основанный на расширенном методе рекурсивного спуска для грамматики: G({a.<,“,>.+,-,/,*}, {S.T.E} Р, S)
Р:
S -> Т<Т I Т>Т I Т<-Т I т>-т
Т -> Т+Е | Т-Е I Е
Е -> а*а | а/а | а
выполните разбор цепочки символов «а*а+а<а/а-а*а».
Дана грамматика G({ (,).".&.-,a}, {S.R.T.G.E.F}, Р, S)
Р:
S ->■ Т | TR
R -> ^Т | 4R
Т -> Е | EG
G —> &Е | &EG
Е -* -Е | F
F ^ (S) | а
О докажите, что она не является ЬЦ1)-грамматикой;
О преобразуйте данную грамматику к виду Ы-(1)-грамматики;
О постройте распознаватель на основе полученной грамматики и выполните разбор цепочки символов: «аЛа&~а&(аЛ—а)».
Постройте распознаватель на основе грамматики простого предшествования для грамматики, заданной в задаче № 3. Выполните разбор цепочки символов, указанной в задаче № 3.
Дана грамматика G({( ),*,&,-,a) {S.T.E.F}, Р S)
Р:
S -* S‘T | Т
Т -> Т&Е | Е
Е -> -Е | F
F (S) | а
постройте для нее распознаватель на основе грамматики операторного предшествования. Выполните разбор цепочки символов, указанной в задаче № 3.
Придумайте функцию линеаризации матрицы предшествования для грамматики, заданной в задаче № 5.
Дана грамматика. G({«1f».«then»,«else».a,b}. (S.T.E F] P. S)
P
S -» if b then T else S | if b then S | a
I -» if b then I else S | a
постройте для нее распознаватель на основе грамматики операторного предшествования, считая «if», «then» и «else» едиными терминальными символами Является ли данная грамматика однозначной? Выполните разбор цепочки символов «if b then if b then if b then a else а». К первому или к последнему «if» будет отнесено «else» в этой цепочке?
Перестройге правила грамматики, данной в задаче № 7 так, чтобы «else» всегда относилось к объемлющему «if», а не к ближайшему.
Придумайте функцию линеаризации матрицы предшествования для грамматики, заданной в задаче № 7.
ГЛАВА 13 Основные принципы построения трансляторов
Выше были рассмотрены теоретические вопросы, связанные с теорией (формальных языков и грамматик. В этом разделе пойдет речь о том, как эти теоретические аспекты применяются на практике при построении трансляторов.
Трансляторы, компиляторы и интерпретаторы — общая схема работы
Определение транслятора, компилятора, интерпретатора
Для начала дадим несколько определений — что же все-таки такое есть уже многократно упоминавшиеся трансляторы и компиляторы.
Формальное определение транслятора
Транслятор — это программа, которая переводит входную программу на исходном (входном) языке в эквивалентную ей выходную программу на результирующем (выходном) языке. В этом определении слово «программа» встречается три раза, и это нс ошибка и не тавтология. В работе транслятора, действительно, участвуют всегда три программы.
Во-первых, сам транслятор является программой — обычно он входит в состав системного программного обеспечения вычислительной системы. То есть транслятор — это часть программного обеспечения (ПО), он представляет собой набор машинных команд и данных и выполняется компьютером, как и все прочие программы в рамках операционной системы (ОС). Все составные части транслятора представляют собой фрагменты или модули программы со своими входными и выходными данными.
Во-вгорых, исходными данными для работы транслятора служит текст входной программы — некоторая последовательность предложений входного языка программирования. Обычно это символьный файл, но этот файл должен содержать текст программы, удовлетворяющий синтаксическим и семантическим требованиям входного языка. Кроме того, этот файл несет в себе некоторый смысл, определяемый семантикой входного языка.
В-третьих, выходными данными транслятора является текст результирующей программы. Результирующая программа строится но синтаксическим правилам, заданным в выходном языке транслятора, а ее смысл определяется семантикой выходного языка. Важным требованием в определении транслятора является эквивалентность входной и выходной программ. Эквивалентность двух программ означает совпадение их смысла с точки зрения семантики входного языка (для исходной программы) и семантики выходного языка (для результирующей программы). Без выполнения этого требования сам транслятор теряет всякий прак- тический смысл.
Итак, чтобы создать транслятор, необходимо прежде всего выбрать входной и выходной языки. С точки зрения преобразования предложений входного языка в эквивалентные им предложения выходного языка транслятор выступает как переводчик. Например, трансляция программы с языка С в язык ассемблера по сути ничем не отличается от перевода, скажем, с русского языка на английский, с топ только разницей, что сложность языков несколько иная (почему не существует трансляторов с естественных языков — см. раздел «Классификация языков и грамматик», глава 9) Поэтому и само слово «транслятор» (английское: translator) означает «переводчик».
Результатом работы транслятора будет результирующая программа, но только в том случае, если текст исходной программы является правильным — нс содержит ошибок с точки зрения синтаксиса и семантики входного языка. Если исходная программа неправильная (содержит хотя бы одну ошибку), то результатом работы транслятора будет сообщение об ошибке (как правило, с дополнительными пояснениями и указанием места ошибки в исходной программе). В этом смысле транслятор сродни переводчику, например, с английского, которому подсунули неверный текст.
Определение компилятора.
Отличие компилятора от транслятора
Кроме понятия «транслятор» широко употребляется также близкое ему по смыслу понятие «компилятор».
Компилятор — это транслятор, который осуществляет перевод исходной программы в эквивалентную ей объектную программу на языке машинных команд или на языке ассемблера.
Таким образом, компилятор отличается от транслятора лишь тем, что его результирующая программа всегда должна быть написана на языке машинных кодов или на языке ассемблера. Результирующая программа транслятора, в общем случае, может быть написана на любом языке — возможен, например, транслятор программ с языка Pascal на язык С. Соответственно, всякий компилятор является транслятором, но нс наоборот — не всякий транслятор будет компилятором. Например, упомянутый выше транслятор с языка Pascal па С компилятором являться не будет.
Само слово «компилятор» происходит от английского термина «compiler» («составитель», «компоновщик»). Видимо, гермип обязан своему происхождению способности компиляторов составлять объектные программы па основе исходных программ.
Результирующая программа компилятора называется «объектной программой» или «объектным кодом». Файл, в который опа записана, обычно называется «объектным файлом». Даже в том случае, когда результирующая программа порождается на языке машинных команд, между объектной программой (объектным файлом) и исполняемой программой (исполняемым файлом) есть существенная разница. Порожденная компилятором программа не может непосредственно выполняться па компьютере, так как она не привязана к конкретной области памяти, где должны располагаться ее код и данные (более подробно — см. раздел «Принципы функционирования систем программирования», глава 15).
Компиляторы, безусловно, самый распространенный вид трансляторов (многие считают их вообще единственным видом трансляторов, хотя это не так) Опи имеют самое широкое практическое применение, которым обязаны широкому распространению всевозможных языков программирования. Далее всегда будем говорить о компиляторах, подразумевая, что выходная программа написана па
языке машинных кодов или языке ассемблера (если это не так, то это будет спе- цпажшо указывался отдельно).
Естественно, трансляторы и компиляторы, как и все прочие программы, разрабатывает человек (люди) — обычно это группа разработчиков. В принципе они могли бы создавать его непосредственно на языке машинных команд, однако объем кода н данных современных компиляторов таков, что их создание па языке машинных команд практически невозможно в разумные сроки при разумных трудозатратах. Поэтому практически все современные компиляторы также создаются с помощью компиляторов (обычно в этой роли выступают предыдущие версии компиляторов той же фирмы-производителя). И в этом качестве компилятор является уже выходной программой для другого компилятора, которая ничем нс лучше и не хуже всех прочих порождаемых выходных программ.
Определение интерпретатора. Разница между интерпретаторами и трансляторами
Кроме схожих между собой понятий «транслятор» и «компилятор» существует принципиально отличное от них понятие интерпретатора.
Интерпретатор — это программа которая воспринимает входную программу на исходном языке и выполняет ее.
В отличие or трансляторов интерпретаторы не порождают результирующую программу (и вообще какого-либо результирующего кода) — и в этом принципиальная разница между ними. Интерпретатор, так же как и транслятор, анализирует текст ит ходпой программы. Однако он не порождает результирующей программы, а сразу же выполняет исходную в соответствии с ее смыслом, заданным семантикой входного языка. Таким образом, результатом работы интерпретатора будет результат, заданный смыслом исходной программы, в том случае, если эта программа правильная, или сообщение об ошибке, если исходная программа неверна. Конечно, чтобы исполнить исходную программу, интерпретатор так или иначе должен преобразовать ее в язык машинных кодов, поскольку иначе выполнение программ на компьютере невозможно. Он и делает это, однако полученные машинные коды не являются доступными — их не видит пользователь интерпретатора. Эти машинные коды порождаются интерпретатором, исполняются и унич-
тожаются по мере надобности — так, как того требует конкретная реализация интерпретатора. Пользователь же видит результат выполнения этих кодов — то есть результат выполнения исходной программы (требование об эквивалептно- сти исходной программы и порожденных машинных кодов и в атом случае, безусловно, должно выполняться).
Более подробно вопросы, связанные с реализацией интерпретаторов и их отличием от компиляторов, рассмотрены далее в соответствующем разделе.
Назначение трансляторов, компиляторов и интерпретаторов. Примеры реализации
Первые программы, которые создавались еще для ЭВМ первого поколения, писались непосредственно па языке машинных кодов. Это была поистине адская работа. Сразу стало ясно, что человек не должен и не может говорить на языке машинных команд, даже если он специалист по вычислительной технике. Однако и все попытки научить компьютер говорить на языках людей успехом не увенчались и вряд ли когда-либо увенчаются (на что есть определенные обьек- тивные причины, рассмотренные в первой главе этого пособия).
С гех пор все развитие программного обеспечения компьютеров неразрывно связано с возникновением и развитием компиляторов.
Первыми компиляторами были компиляторы с языков ассемблера пли, как они назывались, мнемокодов. Мнемокоды превратили «филькину грамоту» языка машинных команд в более-менее доступный пониманию специалиста язык мнемонических (преимущественно англоязычных) обозначений этих команд. Создавать программы уже стало значительно проще, но исполнять сам мнемокод (язык ассемблера) пи один компьютер неспособен, соответственно, возникла необходимость в создании компиляторов. Эти компиляторы элементарно просты, но они продолжают играть существенную роль в системах программирования по сей день. Более подробно о языке ассемблера и компиляторах с него рассказано далее в соответствующем разделе.
Следующим этапом стало создание языков высокого уровня. Языки высокого уровня (к ним относится большинство языков программирования) представляют собой некоторое промежуточное звено между чисто формальными языками и языками естественного общения людей. От первых им досталась строгая формализация синтаксических структуру предложений языка, от вторых — значительная часть словарного запаса, семантика основных конструкций и выражений (с элементами математических операций, пришедшими из алгебры).
Появление языков высокого уровня существенно упростило процесс программирования, хотя и не свело ею до «уровня домохозяйки», как самонадеянно пола- гали некоторые авторы на заре рождения языков программирования. Сначала
таких языков были единицы, затем десятки, сейчас, наверное, их насчитывается более согни. Процессу этому не видно конца Тем не менее по-прежнему преобладают компьютеры традиционной, «неймановской», архитектуры, которые умеют понимать только машинные команды, поэтому вопрос о создании компиляторов продолжает быть актуальным.
Как только возникла массовая потребность в создании компиляторов, стала развиваться н специализированная теория. Со временем опа нашла практическое приложение во множестве созданных компиляторов. Компиляторы создавались и продолжают создаваться нс только для новых, по и для давно известных языков. Многие производители от известных, солидных фирм (таких, как Microsoft пли Inprise) до мало кому знакомых коллективов авторов выпускают на рынок все новые и новые образцы компиляторов. Это обусловлено рядом причин, которые будут рассмотрены далее.
Наконец, с тех пор как большинство теоретических аспектов в области компиляторов получили свою практическую реализацию (а это, надо сказать, произошло довольно быстро, в конце 60-х годов), развитие компиляторов пошло по пути их дружественности человеку — пользователю, разработчику программ па языках высокого уровня. Логичным завершением этого процесса стало создание систем программирования — программных комплексов, объединяющих в себе кроме непосредственно компиляторов множество связанных с ними компонентов программного обеспечения. Появившись, системы программирования быстро завоевали рынок и ныне в массе своей преобладают на нем (фактически, обособленные компиляторы — это редкость среди современных npoi раммных средств). О том, что представляют собой и как организованы современные системы программирования, см. в главе «Современные системы программирования».
Ныне компиляторы являются пеотьемлемой частью любой вычислительной системы. Без их существования программирование любой прикладной задачи было бы затруднено, а то и просто невозможно. Да и программирование специализированных системных задач, как правило, ведется если не на языке высокого уровня (в этой роли в настоящее время чаще всего применяется язык С), то на языке ассемблера, следовательно, применяется соответствующий компилятор. Программирование непосредственно на языках машинных кодов происходит исключительно редко и только для решения очень узких вопросов.
Несколько слов о примерах реализации комнпляюров и интерпретаторов, а также о том, как они соотносятся с другими существующими П[юграммнымн средствами.
Компиляторы, как будет показано далее, обычно несколько проще в реализации, чем интерпретаторы. По эффективности они также превосходят их — очевидно, что откомпилированный код будет исполняться всегда быстрее, чем происходит интерпретация аналогичной исходной программы. Кроме того, нс каждый язык программирования допускает поп роение простого интерпретатора. Однако интерпретаторы имеют одно существенное преимущество — откомпилированный код вфгда привязан к архитектуре вычислительной системы, на которую он ориентирован, а исходная программа — только к семантике языка программирования, которая гораздо легче поддается стандартизации. Этот аспект Первоначально не принимали во внимание.
Первыми компиляторами были компиляторы с мнемокодов. Их потомки — современные компиляторы с языков ассемблера — существую практически для всех известных вычислительных систем. Они предельно жестко ориентированы на архитектуру. Затем появились компиляторы с таких языков, как FORTRAN, ALGOL-68, PL/1. Они были ориеигиронапы на большие ЭВМ с пакетной обработкой задач. Из вышеперечисленных только FORTRAN, пожалуй, продолжает использоваться по сей день, поскольку имеет огромное количество библиотек различного назначения [7]. Многие языки, родившись, так и не получили широкого распространения — ADA, Modula, Simula известны лишь узкому кругу специалистов. В то же время на рынке программных систем доминируют компиляторы языков, которым не прочили светлого будущего. В первую очередь, сейчас это С и C++. Первый из них родился вместе с операционными системами типа UNIX, вместе с нею завоевал свое «место под солнцем», а затем перешел под ОС других типов. Второй удачно воплотил в себе пример реализации идей обтюктно- ориентироваппого программирования на хорошо зарекомендовавшей себя практической базе. Еще можно упомянуть довольно распространенный Pascal, который неожиданно для многих вышел за рамки чисто учебного языка для университетской среды.
История интерпретаторов не столь богата (пока!). Как уже было сказано, изначально им нс предавали существенного значения, поскольку почти по всем параметрам они уступают компиляторам. Из известных языков, предполагавших интерпретацию, можно упомянуть разве что Basic, хотя большинству сейчас известна его компилируемая реализация Visual Basic, сделанная фирмой Microsoft [3, 63]. Тем не менее сейчас ситуация несколько изменилась, поскольку вопрос о переносимости программ и их аппаратно-платформенной независимости приобретает все большую актуальность с развитием сети Интернет. Самый известный сейчас пример - это язык Java (сам по себе он сочетает компиляцию и интерпретацию), а также связанный с ним JavaScript В конце концов, язык HTML на котором зиждется протокол HTTP, давший волчок столь бурному развитию Всемирной сеги, — это тоже интерпретируемый язык. По мнению автора, в области появления новых интерпретаторов всех еще ждут сюрпризы, и появились уже первые из них — например язык C# («си-дпсз», но название везде идет как «Си шарп»), анонсируемый фирмой Microsoft.
Об истории языков программирования и современном состоянии рынка компиляторов можно говорить долго и много. Автор считает возможным ограничиться уже сказанным, поскольку это не является целью данного пособия. Желающие могут обратиться к литературе [7, 8, 14, 23, 30, 45, 66, 77, 811
Этапы трансляции. Общая схема работы транслятора
Па рис. 13.1 представлена общая схема работы компилятора. Из нее видно, что в целом процесс компиляции состоит из двух основных этапов — сник за п анализа.
Рис. 13.1. Общая схема работы компилятора
На этапе анализа выполняется распознавание текста исходной программы, создание и заполнение таблиц идентификаторов. Результатом его работы служит некое внутреннее представление программы, понятное компилятору.
На этапе синтеза на основании внутреннего представления программы и информации, содержащейся в таблице (таблицах) идентификаторов, порождается текст резулыирующей программы Результатом этого этана является объектный код.
Кроме того, в составе компилятора присутствует часть, ответственная за анализ и исправление ошибок, которая при наличии ошибки в тексте исходной про- граммы должна максимально полно информировйгь пользователя о типе ошибки и месте ее возникновения. В лучшем случае компилятор может предложить пользователю вариант исправления ошибки.
Эти этаны, в свою очередь, состоят из более мелких этапов, называемых фазами компиляции Состав фаз компиляции приведен в самом общем виде, их конкретная реализация и процесс взаимодействия могуД, конечно, различаться в зависимости от версии компилятора Однако в том или ином виде все представленные фазы практически всегда присутствуют в каждом конкретном компиляторе [2G./10J.
Компилятор в целом с точки зрения теории формальных языков выступает в «двух ипостасях», выполняет две основные функции
Во-первых, он является распознавателем для языка исходной программы. То есть он должен получить на вход цепочку символов входного языка, проверить ее принадлежность языку и, более того, выявить правила, по которым эта цепочка была построена (поскольку сам ответ па вопрос о принадлежности «да» или «нет» представляет мало ин тереса) Интересно, что генератором цепочек входного языка выступает пользователь — автор входной программы.
Во-вторых, компилятор является генератором для языка результирующей программы. Он должен построить па выходе цепочку выходного языка по определенным правилам, предполагаемым языком машинных команд пли языком ассемблера. Распознавателем этой цепочки будет выступать уже вычислительная система, под которую создается результирующая программа.
Далее дастся перечень основных фаз (чисгсй) компиляции и краткое описание их функций. Более подробная информация дана в подразделах, соответствующих этим фазам.
Лексический анализ (сканер) — это часть компилятора, которая читает литеры программы па исходном языке и строит из них слова (лексемы) исходного языка. На вход лексического анализатора поступает текст исходной программы, а выходная информация передается для дальнейшей обработки компилятором на этапе синтаксического разбора. С теоретической точки зрения лексический анализатор не является обязательной, необходимой частью компилятора. Однако существует причины, которые определяют его присутствие практически во всех компиляторах. Более подробно см. раздел «Лексические анализаторы (сканеры). Принципы построения сканеров».
Синтаксический разбор — это основная часть компилятора на этапе анализа. Опа выполняет выделение синтаксических конструкций в тексте исходной программы, обработанном лексическим анализатором. На этой же фазе компиляции проверяется синтаксическая правильность программы. Синтаксический разбор играет главную роль — роль распознавателя текста входного языка программирования (см. раздел «Синтаксические анализаторы. Синтаксически управляемый перевод» этой главы).
Семантический анализ — это часть компилятора, проверяющая правильность текста исходной программы с точки зрения семантики входного языка. Кроме непосредственно проверки, семантический анализ должен выполнять преобразования текста, требуемые семантикой входного языка (такие, как добавление функций неявного преобразования типов). В различных реализациях компиляторов семантический анализ может частично входить в фазу синтаксического разбора, частично — в фазу подготовки к генерации кода.
Подготовка к генерации кода — это фаза, на которой компилятором выполняются предварительные действия, непосредственно связанные с синтезом гекста результирующей программы, но еще не ведущие к порождению текста па выходном языке. Обычно в эту фазу входят действия, связанные с идентификацией элементов языка, распределением памяти и г. и. (см. раздел «Семантический анализ и подготовка к генерации кода», глава 14).
Генерация кода — это фаза, непосредственно связанная с порождением команд, составляющих предложения выходного языка и в целом текст результирующей
программы. Это основная фаза па этане синтеза результирующей программы. Кроме непосредственного порождения текста результирующей программы, генерация обычно включает в себя также оптимизацию — процесс, связанный с обработкой уже порожденного текста. Иногда оптимизацию выделяют в отдельную фазу компиляции, так как она оказывает существенное влияние на качество и эффективность результирующей программы (см. разделы «Генерация кода Методы генерации кода» и « Оптимизация кода. Основные методы оптимизации», глава 14).
Таблицы идентификаторов (иногда — «таблицы символов») — это специальным образом организованные наборы данных, служащие для храпения информации об элементах исходной программы, которые затем используются для порождения текста результирующей программы. Таблица идентификаторов в конкретной реализации компилятора может быть одна, или же таких таблиц может быть несколько. Элементами исходной программы, информацию о которых нужно хранить в процессе компиляции, являются переменные, константы, функции н г. и. — конкретный состав набора элементов зависит от используемою входного языка программирования. Понятие «таблицы» вовсе не предполагает, что эго хранилище данных должно быть организовано именно в виде таблиц или других массивов информации — возможные методы их организации подробно рассмотрены далее, в разделе «Таблицы идентификаторов. Организация таблиц идентификаторов».
Представленное па рис. 13.1 деление процесса компиляции на фазы служит скорее методическим целям и па практике может не соблюдаться столь строго. Далее в подразделах этого пособия рассматриваются различные варианты технической организации представленных фаз компиляции. При этом указано, как они могут быть связаны между собой. Здесь рассмотрим только общие аспекты такого рода взаимосвязи.
Во-первых, па фазе лексического анализа лексемы выделяются из текста входной программы постольку, поскольку они необходимы для следующей фазы синтаксического разбора. Во-вторых, как будет показано ниже, синтаксический разбор и генерация кода могут выполняться одновременно. Таким образом, эти три фазы компиляции могут работать комбинированно, а вместе с ними может выполняться и подготовка к генерации кода. Далее рассмотрены технические вопросы реализации основных фаз компиляции, которые тесно связаны с понятием прохода.
Понятие прохода. Многопроходные и однопроходные компиляторы
Как уже было сказано, процесс компиляции программ состоит из нескольких фаз. В реальных компиляторах состав этих фаз может несколько отличаться от рассмотренного выше — некоторые из них могут быть разбиты па составляющие, другие, напротив, объединены в одну фазу. Порядок выполнения фаз компиляции также можег меняться в разных вариантах компиляторов. В одном случае компилятор просматривает текст исходной программы, сразу выполняет все (фазы компиляции и получает результат — обьскгный код. В другом варианте он выполняет над исходным текстом только некоторые из (фаз компиляции и получает
не конечные результат, а набор некоторых промежуточных данных. Эти данные затем снова подвергаются обработке, причем этот процесс может повторяться несколько раз.
Реальные компиляторы, как правило, выполняют трансляцию текста исходной программы за несколько проходов.
Проход — это процесс последовательного чтения компилятором данных из внешней памяти, их обработки и помещения результата работы во внешнюю намять. Чаще всего один проход включает в себя выполнение одной или нескольких фаз компиляции. Результатом промежуточных проходов является внутреннее представление исходной программы, результатом последнего прохода — результирующая обьектная программа.
В качестве внешней памяти могуг выступать любые носители информации — оперативная память компьютера, накопители на магнитных дисках, магнитных лептах и т. п. Современные компиляторы, как правило, стремятся максимально использовать для хранения данных оперативную памя ть компьютера, и только при недостатке объема доступной памяти используются накопи тели па жестких магнитных дисках. Другие носители информации в современных компиляторах не используются из-за невысокой скорости обмена данными
При выполнении каждого прохода компилятору доступна информация, полученная в результате всех предыдущих проходов. Как правило, он стремится использовать в первую очередь только информацию, полученную на проходе, непосредственно предшествовавшем текущему, но в принципе может обращаться и к данным от более ранних проходов вплоть до исходного текста программы. Информация получаемая компилятором при выполнении проходов, недоступна пользователю. Она либо хранится в оперативной памяти, которая освобождается компилятором после завершения процесса трансляции, либо оформляется в виде временных файлов па диске, которые также уничтожаются после завершения работы компиля гора. Поэтому человек, работающий с компилятором, может даже не знать, сколько проходов выполняет компилятор — он всегда видит только текст исходной программы и результирующую объектную программу. Но количество выполняемых проходов — это важная техническая характеристика компилятора, солидные фирмы — разработчики компиляторов обычно указывают ее в описании своего продукта.
Понятно, что разработчики стремятся максимально сократить количество проходов, выполняемых компиляторами. При этом увеличивается скорость работы компилятора, сокращается объем необходимой ему памяти. Однопроходный компилятор, получающий на вход исходную программу и сразу же порождающий результирующую объектную программу, — это идеальный вариант.
Однако сократить число проходов не всегда удается. Количество необходимых проходов определяется прежде всего грамматикой и семантическими правилами исходного языка. Чем сложнее грамматика языка и чем больше вариантов предполагают семантические правила — тем больше проходов будет выполнять компилятор (конечно, играет свою роль и квалификация разработ чиков компилятора). Например, именно поэтому обычно компиляторы с языка Pascal работают быстрее, чем компиляторы с языка С — грамматика языка Pascal более проста, а семантические правила более жесткие.
Однопроходные компиляторы — редкость, они возможны только для очень простых языков Реальные компиляторы выполняют, как правило, от двух до пяти проходов. Таким образом, реальные компиляторы являются многопроходными. Наиболее распространены двух- и трехи входные компиляторы, например первый проход — лексический анализ, второй — синтаксический разбор и семантический анализ, третий — генерация и оптимизация кода (варианты исполнения, конечно, зависят от разработчика). В современных системах программирования нередко первый проход компилятора (лексический анализ кода) выполняется параллельно с редактированием кода исходной программы (такой вариант построения компиляторов рассмотрен далее в этой главе).
Интерпретаторы. Особенности построения интерпретаторов
Интерпретатор — это программа, которая воспринимает входную программу на исходном языке и выполняет ее. Как уже было сказано выше, основное отличие интерпретаторов от трансляторов и компиляторов заключается в том, что интерпретатор не порождает результирующую программу, а просто выполняет исходную программу.
Термин «интерпретатор» (interpreter), как и «транслятор», означает «переводчик» С точки зрения терминологии эти понятия схожи, но с точки зрения теории формальных языков и компиляции между ними большая принципиальная разница. Если понятия «транслятор» и «компилятор» почти неразличимы, то с понятием «шгтерпрстатор» их путать никак нельзя.
Простейшим способом реализации интерпретатора можно было бы считать вариант, когда исходная программа сначала полностью транслируется в машинные команды, а затем сразу же выполняется. В такой реализации интерпретатор, по сути, мало бы чем отличался от компилятора с той лишь разницей, что результирующая программа в нем была бы недоступна пользователю. Недостатком такого интерпретатора было бы то, что пользователь должен был бы ждать компиляции всей исходной программы прежде, чем начнется ее выполнение. По сути, в таком интерпретаторе не было бы никакого особого смысла — он не давал бы никаких преимуществ по сравнению с аналогичным компилятором.
Поэтому подавляющее большинство интерпретаторов действует так, что исполняет исходную программу последовательно, по мере ее поступления на вход интерпретатора. Тогда пользователю не надо ждать завершения компиляции всей исходной программы. Более того, он может последовательно вводить исходную программу и тут же наблюдать результат ее выполнения по мере ввода команд [82].
При таком порядке работы интерпретатора проявляется существенная особенность, которая отличает его от компилятора, — если интерпретатор исполняет команды по мере их поступления, то он не может выполнять оптимизацию исходной программы. Следовательно, фаза оптимизации в общей структуре ннтер-
ГЛАВА 12 Классы КС-языкови грамматик
Нисходящие распознаватели КС-языков без возвратов
Левосторонний разбор по методу рекурсивного спуска
Стремление улучшить алгоритм нисходящего разбора заключается в первую очередь в определении метода, по которому на каждом шаге алгоритма можно было бы однозначно выбрать одну из всего множества возможных альтернатив. В гаком случае алгоритм моделирования работы УШ-автомата не требовал бы возврата на предыдущие шаги и за счет этого обладал бы линейными характеристиками от длины входной цепочки. В случае неуспеха выполнения такого алгоритма входная цепочка однозначно нс принимается, повторные итерации разбора нс выполняются.
Наиболее очевидным методом выбора одной из множества альтернатив является выбор ее па основании символа ае VT, обуреваемого считывающей головкой автомата на каждом шаге его работы (находящегося справа от положения текущей головки во входной цепочке символов). Поскольку в процессе нисходящего разбора именно этот символ должен появиться на верхушке магазина для продвижения считывающей головки автомата па один таг (условие S(q,a,a) = {(q,?-)}, VaeVT в функции переходов МП-автомата), то разумно искать альтернативу, те он присутствует в начале цепочки, стоящей в правой части правила грамматики.
По такому принципу действует алгоритм разбора по методу рекурсивного спуска.
Алгоритм разбора по методу рекурсивного спуска
В реализации этого алгоритма для каждого нетерминального символа AeVN грамматики G(VNVTP.S) строится процедура разбора, которая получает па вход цепочку символов а и положение считывающей голшисп в цепочке i. Если
В современных системах программирования существуют реализации программного обеспечения, сочетающие в себе и функции компилятора, и функции интср- претатора — в зависимости от требований пользователя исходная программа либо компилируется, либо исполняется (интерпретируется). Кроме того, некоторые современные языки программирования предполагают две стадии разработки: сначала исходная программа компилируется в промежуточный код (некоторый язык низкого уровня), а затем этот результат компиляции выполняется с помощью интерпретатора данного промежуточного языка. Более подробно варианты таких систем рассмотрены в главе «Современные системы программирования».
Широко распространенным примером интерпретируемого языка может служить HTML (Hypertext Markup Language) — язык описания гипертекста. На его основе в настоящее время функционирует практически вся структура сети Интернет. Другой пример — языки Java и JavaScript — сочетают в себе функции компиляции и интерпретации. Текст исходной программы компилируется в некоторый промежуточный двоичный код, не зависящий от архитектуры целевой вычисли- гелыюп системы, этот код распространяется по сети и выполняется па принимающей стороне — интерпретируется.
Трансляторы с языка ассемблера («ассемблеры»)
Язык ассемблера — это язык низкого уровня Структура и взаимосвязь цепочек этого языка близки к машинным командам целевой вычислительной системы, где должна выполняться результирующая программа. Применение языка ассемб лера позволяет разработчику управлять ресурсами (процессором, оперативной памятью, внешними устройствами и т. и) целевой вычисли тельной системы на уровне машинных команд. Каждая команда исходной прог|>аммы на языке ассемблера в результате компиляции преобразуется в одну машинную команду Транслятор с языка ассемблера всегда, безусловно, будет и компилятором, ио- сколысу языком результирующей программы являются машинные коды. Транслятор с языка ассемблера зачастую просто называют «ассемблер» пли «программа ассемблера».
Реализация компиляторов с языка ассемблера
Язык ассемблера, как правило, содержит мнемонические коды машинных команд. Чаще всего используется англоязычная мнемоника команд, но существуют и другие варианты языков ассемблера (в том числе существуют и русскоязычные варианты). Именно поэтому язык ассемблера раньше носил названия «язык мнемокодов» (сейчас это название уже практически не употребляется). Все возможные команды в каждом языке ассемблера можно разбить па две группы: в первую группу входят обычные команды языка, которые в процессе трансляции преобразуются в машинные команды; вторую групп)' составляют специальные команды языка, которые в машинные команды не преобразуются, ио ис- польбуются компилятором для выполнения задач компиляции (таких, например, как Задача распределения памяти).
Синтаксис языка чрезвычайно прост. Команды исходной программы записываются обычно таким образом, чтобы на одной строке программы располагалась одна команда. Каждая команда языка ассемблера, как правило, может быть разделена на три составляющих, следующих последовательно одна за другой: поле метки, код операции и поле операндов. Компилятор с языка ассемблера обычно предусматривает и возможность наличия во входной программе комментариев, которые отделяются от команд заданным разделителем [93].
Поле метки содержит идентификатор, представляющий собой метку, либо является пустым. Каждый идентификатор метки может встречаться в программе па языке ассемблера только один раз. Метка считается описанной там, где она непосредственно встретилась в программе (предварительное описание меток не требуется). Метка может быть использована для передачи управления на помеченную ею команду. Нередко метка отделяется от остальной части команды специальным разделителем (чаще всего — двоеточием «:»).
Код операции всегда представляет собой строго определенную мнемонику одной из возможных команд процессора или также строго определенную команду самого компилятора. Код операции записывается алфавитными символами входного языка. Чаще всего его длина составляет 3-4, реже — 5 или 6 символов.
Поле операндов либо является пустым, либо представляет собой список из одного, двух, реже — трех операндов. Количество операндов строго определено и зависит от кода операции — каждая операция языка ассемблера предусматривает жестко заданное число своих операндов. Соответственно, каждому из этих вариантов соответствуют безадресные, одноадресные, двухадресные или трехадрссные команды (большее число операндов практически нс используется, в современных ЭВМ даже трехадрссные команды встречаются редко). В качестве операндов могут выступать идентификаторы или константы.
Особенностью языка ассемблера является то, что ряд идентификаторов в нем выделяется специально для обозначения регистров процессора. Такие идентификаторы, с одной стороны, не требуют предварительного описания, но, с другой, они не могут быть использованы пользователем для иных целен. Набор этих идентификаторов предопределен для каждого языка ассемблера.
Иногда язык ассемблера допускает использование в качестве операндов определенных ограниченных сочетаний обозначений регистров, идентификаторов и констант, которые объединены некоторыми знаками операций. Такие сочетания чаще всего используются для обозначения типов адресации, допустимых в машинных командах целевой вычислительной системы.
Например, следующая последовательность команд
datesdb16 dup(O)
loops:movdatas[bx+4].ex
decbx
jnzloops
представляет собой пример последовательности команд языка ассемблера процессоров семейства Intel 80x86. Здесь присутствуют команда описания набора данных (db), метка (loops), коды операций (mov. dec и jnz). Операндами являются идентификатор набора данных (dates), обозначения регистров процессора
(Ьх и сх), метка (loops) и константа (4). Составной операнд datas[bx+4] отображает косвенную адресацию набора данных datas по базовому регистру Ьх со смещением 4.
Подобный синтаксис языка без труда может быть описан с помощью регулярной грамматики. Поэтому построение распознавателя для языка ассемблера не представляет труда. По этой же причине в компиляторах с языка ассемблера лексический и синтаксический разбор, как правило, совмещены в один распознаватель.
Семантика языка ассемблера целиком и полностью определяется целевой вычисли гельной системой, на которую ориентирован данный язык. Семантика языка ассемблера определяет, какая машинная команда соответствует каждой команде языка ассемблера, а также го, какие операнды и в каком количестве допустимы для того или иного кода операции.
Поэтому семантический анализ в компиляторе с языка ассемблера также прост, как и синтаксический. Основной его задачей является проверить допустимость операндов для каждого кода операции, а также проверить, что все идентификаторы и метки, встречающиеся во входной программе, описаны и обозначающие их идентификаторы не совпадают с предопределенными идентификаторами, используемыми для обозначения кодов операции и регистров процессора.
Схемы синтаксического н семантического анали.ы в компиляторе с языка ассемблера могут быть, таким образом, реализованы па основе обычного конечного автомата. Именно эта особенность определила тот факт, что компиляторы с языка ассемблера исторически явились первыми компиляторами, созданными для ЭВМ Существует также ряд других особенностей, которые присущи именно языкам ассемблера и упрощают построение компиляторов для них.
Во-первых, в компиля торах с языка ассемблера не выполняется дополнительная идентификация переменных — все переменные языка сохраняют имена, присвоенные им пользователем. За уникальность имен в исходной программе отвечает ее разработчик, семантика языка никаких дополнительных требований на этот процесс не накладывает. Во-вторых, в компиляторах с языка ассемблера предельно упрощено распределение памяти. Компилятор с языка ассемблера работает только со статической памятью. Если используется динамическая память, то для работы с нею нужно использовать соответствующую библиотеку или функции ОС, а за ее распределение отвечает разработчик исходной программы. За передачу параметров и организацию дисплея памяти процедур и функций также отвечает разработчик исходной программы. Он же должен позаботиться и об отделении данных от кода программы — компилятор с языка ассемблера, в отличие от компиляторов с языков высокого уровня, автоматически такого разделения не выполняет. И в-третьих, на этапе генерации кода в компиляторе с языка ассемблера не производится оптимизация, поскольку разработчик исходной программы сам отвечает за организацию вычислений, последовательность машинных команд и распределение' регистров процессора.
За исключением этих особенностей компилятор с языка ассемблера является обычным компилятором, но значительно упрощенным по сравнению с любым компилятором с языка высокого уровня.
Компиляторы с языка ассемблера реализуются чаще всего по двухпроходной схеме. На первом проходе компилятор выполняет разбор исходной программы, ее преобразование в машинные коды и одновременно заполняет таблицу идентификаторов. Но на первом проходе в машинных командах остаются незаполненными адреса тех операндов, которые размещаются в оперативной памяти. На втором проходе компилятор заполняет эти адреса и одновременно обнаруживает неописанные идентификаторы. Это связано с тем, что операнд может быть описан в программе после того, как он первый раз был использован. Тогда его адрес еще не известен на момент построения машинной команды, а поэтому требуется второй проход. Типичным примером такого операнда является метка, предусматривающая переход вперед по ходу последовательности команд.
Макроопределения и макрокоманды
Разработка программ на языке ассемблера — достаточно трудоемкий процесс, требующий зачастую простого повторения одних и тех же многократно встречающихся операций. Примером может служить последовательность команд, выполняемых каждый раз для организации стекового дисплея памяти при входе в процедуру или функцию.
Для облегчения труда разработчика были созданы так называемые макрокоманды.
Макрокоманда представляет собой текстовую подстановку, в ходе выполнения которой каждый идентификатор определенного вида заменяется на цепочку символов из некоторого хранилища данных. Процесс выполнения макрокоманды называется макрогенерацией, а цепочка символов, получаемая в результате выполнения макрокоманды, — макрорасширением.
Процесс выполнения макрокоманд заключается в последовательном просмотре текста исходной программы, обнаружении в нем определенных идентификаторов и их замене па соответствующие строки символов. Причем выполняется именно текстовая замена одной цепочки символов (идентификатора) на другую цепочку символов (строку). Такая замена называется макроподстановкой [66, 83, 93].
Для того чтобы указать, какие идентификаторы на какие строки необходимо заменять, служат макроопределения. Макроопределения присутствуют непосредственно в тексте исходной программы. Они выделяются специальными ключевыми словами либо разделителями, которые не могут встречаться нигде больше в тексте программы. В процессе обработки все макроопределения полностью исключаются из текста входной программы, а содержащаяся в них информация запоминается для обработки при выполнении макрокоманд.
Макроопределение может содержать параметры. Тогда каждая соответствующая ему макрокоманда должна при вызове содержать строку символов вместо каждого параметра. Эта строка подставляется при выполнении макрокоманды в каждое место, где в макроопределении встречается соответствующий параметр. В качестве параметра макрокоманды может оказаться другая макрокоманда, тогда она будет рекурсивно вызвана всякий раз, когда необходимо выполнить подстановку параметра. В принципе макрокоманды могут образовывать последовательность
рекурсивных вызовов, аналогичную последовательности рекурсивных вызовов процедур и функций, но только вместо вычислений и передачи параметров они выполняют лишь текстовые подстановки*.
Макрокоманды и макроопределения обрабатываются специальным модулем, называемым макропроцессором или макрогенератором. Макрогенератор получает на вход текст исходной программы, содержащий макроопределения и макрокоманды, а па выходе его появляется текст макрорасширения исходной программы, не содержащий макроопределений и макрокоманд. Оба текста являются только текстами программы, никакая другая обработка не выполняется. Именно макрорасширение исходного текста поступает на вход компилятора.
Синтаксис макрокоманд и макроопределений не является строго заданным. Он может различаться в зависимости от реализации компилятора с языка ассемблера. Но сам принцип выполнения макроподстановок в тексте программы неизменен и не зависит от их синтаксиса.
Макрогенератор чаще всего не существует в виде отдельного программного модуля, а входит в состав компилятора с языка ассемблера. Макрорасширение исходной программы обычно недоступно ее разработчику. Более того, макроподстановки могут выполняться последовательно при разборе исходного текста на первом проходе компилятора вместе с разбором всего текста программы, и тогда макрорасширение исходной программы в целом может и вовсе не существовать как таковое.
Например, следующий текст определяет макрокоманду pushO в языке ассемблера процессора типа Intel 8086:
push_O macro
xor ax.ax push ax endm
Семантика этой макрокоманды заключается в записи числа «0» в стек через регистр процессора ах. Тогда везде в тексте программы, где встретится макрокоманда
push_O
она будет заменена в результате макроподстановки на последовательность команд:
хог ах.ах
push ах
Это самый простой вариант макроопределения. Существует возможность создавать более сложные макроопределения с параметрами. Одно из таких макроопределений описано ниже:
1 Глубина такой рекурсии, как правило, сильно ограничена. На последовательность рекурсивных вызовов макрокоманд налагаются обычно существенно более жесткие ограничения, чем па последовательность рекурсивных вызовов процедур и функций, которая при стековой организации дисплея памяти ограничена только размером стека передачи пара метров.
add_abx macro xl.х2 add ax.xl add bx. xl add ex,x2 push ax endm
Тогда в тексте программы макрокоманда также должна быть указана с соответствующим числом параметров. В данном примере макрокоманда
add_abx4.8
будет в результате макроподстановки заменена на последовательность команд:
addах. 4
add bx.4
addex.8
push ax
Во многих компиляторах с языка ассемблера возможны еще более сложные конструкции, которые могут содержать локальные переменные и метки. Примером такой конструкции может служить макроопределение:
1оор_ах
macro
xl.x2.yl
local
loopax
mov
ax.xl
xor
bx.bx
loopax:
add
bx.yl
sub
ax.x2
jge
loopax
endm
Здесь метка loopax является локальной, определенной только внутри данного макроопределения. В этом случае уже не может быть выполнена простая текстовая подстановка макрокоманды в текст программы, поскольку если данную макрокоманду выполнить дважды, то это приведет к появлению в тексте программы двух одинаковых меток loopax. В таком варианте макрогенератор должен использовать более сложные методы текстовых подстановок, аналогичные тем, что используются в компиляторах при идентификации лексических элементов входной программы, чтобы дать всем возможным локальным переменным и меткам макрокоманд уникальные имена в пределах всей программы.
Макроопределения и макрокоманды нашли применение не только в языках ассемблера, но и во многих языках высокого уровня. Там их обрабатывает специальный модуль, называемый препроцессором языка (например, широко известен препроцессор языка С). Принцип обработки остается тем же самым, что и для программ на языке ассемблера — препроцессор выполняет текстовые подстановки непосредственно над строками самой исходной программы.
В языках высокого уровня макроопределения должны быть отделены от текста самой исходной программы, чтобы препроцессор не мог спутать их с синтаксическими конструкциями входного языка. Для этого используются либо специальные символы и команды (команды препроцессора), которые никогда не могут встречаться в тексте исходной программы, либо макроопределения встречаются
внутри незначащей части исходной программы — входят в состав комментариев (такая реализация существует, например, в компиляторе с языка Pascal, созданном фирмой Borland). Макрокоманды, напротив, могут встречаться в произвольном месте исходного текста программы, и синтаксически их вызов может не отличаться от вызова функций во входном языке.
Следует помнить, ч то, несмотря на схожесть синтаксиса вызова, макрокоманды принципиально отличаются от процедур и функций, поскольку не порождают результирующего кода, а представляют собой текстовую подстановку, выполняемую прямо в тексте исходной программы. Результат вызова функции и макрокоманды может из-за этого серьезно отличаться.
Рассмотрим пример на языке С.
Если описана функция
int fl(int а) { return а + а; )
и аналогичная ей макрокоманда
#def1ne f2(a) ((а) + (а))
то результат их вызова не всегда будет одинаков.
Действительно, вызовы j=fl(i) и j=f2(i) (где 1 и j — некоторые целочисленные переменные) приведут к одному и тому же результату. Но вызовы j=fl(++i) и j=f2(-H-i) дадут разные значения переменной J. Дело в том, что поскольку f2 — это макроопределение, то во втором случае будет выполнена текстовая подстановка, которая приведет к последовательности операторов j=((++i) * (++D). Видно, что в этой последовательности операция -н-i будет выполнена дважды, в отличие от вызова функции fl(++i), где опа выполняется только один раз.
Таблицы идентификаторов.
Организация таблиц идентификаторов
Назначение и особенности построения таблиц идентификаторов
Проверка правильности семантики и генерация кода требуют знания характеристик переменных, констант, функций и других элементов, встречающихся в программе на исходном языке. Все эти элементы в исходной программе, как правило, обозначаются идентификаторами. Выделение идентификаторов и других элементов исходной программы происходит на фазе лексического анализа. Их характеристики определяются на фазах синтаксического разбора, семантического анализа и подготовки к генерации кода. Состав возможных характеристик и методы их определения зависят от семантики входного языка.
В любом случае компилятор должен иметь возможность хранить все найденные идентификаторы и связанные с ними характеристики в течение всего процесса компиляции, чтобы иметь возможность использовать их на различных фазах
компиляции. Для этой цели, как было сказано выше, в компиляторах используются специальные хранилища данных, называемые таблицами символов или таблицами идентификаторов.
Любая таблица идентификаторов состоит из набора полей, количество которых равно числу различных идентификаторов, найденных в исходной программе. Каждое поле содержит в себе полную информацию о данном элементе таблицы. Компилятор может работать с одной или несколькими таблицам идентификаторов — их количество зависит от реализации комш лятора. Например, можно организовывать различные таблицы идентификаторов для разтичных модулей исходной программы или для различных типов элементов входного языка.
Состав информации, хранимой в таблице идентификаторов для каждого элемента исходной программы, зависит от семантики входного языка и типа элемента. Например, в таблицах идентификаторов может храниться следующая информация:
для переменных:
О имя переменной;
О тип данных переменной;
О область памяти, связанная с переменной;
для констант:
О название константы (если оно имеется);
О значение константы;
О тип данных константы (если гребуется):
для функций
О имя функции;
О количество и типы формальных аргументов функции;
О тип возвращаемого результата;
О адрес кода функции.
Приведенный выше состав хранимой информации, конечно же, является только примерным. Другие примеры такой информации указаны в 123, 42, 74]. Конкретное наполнение таблиц идентификаторов зависит от реализации компилятора. Кроме того, не вся информация, хранимая в таблице идентификаторов, заполняется компилятором сразу — он может несколько раз выполнять обращение к данным в таблице идентификаторов на различных фазах компиляции. Например, имена переменных могут быть выделены на фазе лексического анализа, типы данных для переменных — на фазе синтаксического разбора, а область памяти связывается с переменной только на фазе подготовки к генерации кода.
Вне зависимости от реализации компилятора принцип его работы с таблицей идентификаторов остается одним и тем же — на различных фазах компиляции компилятор вынужден многократно обращаться к таблице для поиска информации и записи новых данных. Как правило, каждый элемент в исходной программе однозначно идентифицируется своим именем (вопросы идентификации более подробно рассмотрены в разделе «Семантический анализ и подготовка к генерации кода», глава 14). Поэтому компилятору приходится часто выполнять поиск
необходимого элемента в таблице идентификаторов по его имени, в то время как процесс заполнения таблицы выполняется нечасто — новые идентификаторы описываются в программе гораздо реже, чем используются. Отсюда можно сделать вывод, что таблицы идентификаторов должны быть организованы таким образом, чтобы компилятор имел возможность максимально быстрого поиска нужного ему элемента [33].
Простейшие методы построения таблиц идентификаторов
Простейший способ организации таблицы состоит в том, чтобы добавлять элементы в порядке их поступления. Тогда таблица идентификаторов будет представлять собой неупорядоченный массив информации, каждая ячейка которого будет содержать данные о соответствующем элементе таблицы.
Поиск нужного элемента в таблице будет в этом случае заключаться в последовательном сравнении искомого элемента с каждым элементом таблицы, пока не будет найден подходящий. Тогда, если за единицу принять время, затрачиваемое компилятором на сравнение двух элементов (как правило, это сравнение двух строк), то для таблицы, содержащей N элементов, в среднем будет выполнено N/2 сравнений [14].
Заполнение такой таблицы будет происходить элементарно просто — добавлением нового элемента в ее конец, и время, требуемое на добавление элемента (Т,), нс будет зависеть от числа элементов в таблице N. Но если N велико, то поиск потребует значительных затрат времени. Время поиска (Т„) в такой таблице можно оценить как Т„ - O(N). Поскольку поиск в таблице идентификаторов является чаще всего выполняемой компилятором операцией, а количество различных идентификаторов даже в реальной исходной программе достаточно велико (от нескольких сотен до нескольких тысяч элементов), то такой способ организации таблиц идентификаторов является неэффективным.
Поиск может быть выполнен более эффективно, если элементы таблицы упорядочены (отсортированы) согласно некоторому естественному порядку.
В нашем случае, когда поиск будет осуществляться по имени идентификатора, наиболее естественно расположить элементы таблицы в прямом или обратном алфавитном порядке. Эффективным методом поиска в упорядоченном списке из N элементов является бинарный или логарифмический поиск. Символ, который следует найти, сравнивается с элементом (N+l)/2 в середине таблицы. Если этот элемент не является искомым, то мы должны просмотреть только блок элементов, пронумерованных от 1 до (N+l)/2-l, или блок элементов от (N+l)/2+l до N в зависимости от того, меньше или больше искомый элемент того, с которым его сравнили. Затем процесс повторяется над нужным блоком в два раза меньшего размера. Так продолжается до тех пор, пока либо элемент не будет найден, либо алгоритм не дойдет до очередного блока, содержащего один или два элемента (с которыми уже можно выполнить прямое сравнение искомого элемента).
Так как на каждом шаге число элементов, которые могу г содержать искомый элемент, сокращается наполовину, то максимальное число сравнений равно l+log,(N).
Тогда время поиска элемента в таблице идентификаторов можно оценить как Т„ = O(log2N). Для сравнения: при N=128 бинарный поиск требует самое большее 8 сравнений, а поиск в неупорядоченной таблице — в среднем 64 сравнения. Метод называют «бинарным поиском», поскольку на каждом шаге объем рассматриваемой информации сокращается в два раза, а «логарифмическим» — поскольку время, затрачиваемое на поиск нужного элемента в массиве, имеет логарифмическую зависимость от общего количества элементов в нем.
Недостатком метода является требование упорядочивания таблицы идентификаторов. Так как массив информации, в котором выполняется поиск, должен быть упорядочен, то время его заполнения уже будет зависеть от числа элементов в массиве. Таблица идентификаторов зачастую просматривается еще до того, как она заполнена полностью, поэтому требуется, чтобы условие упорядоченности выполнялось на всех этапах обращения к ней. Следовательно, для построения таблицы можно пользоваться только алгоритмом прямого упорядоченного включения элементов.
При добавлении каждого нового элемента в таблицу сначала надо определить место, куда поместить новый элемент, а потом выполнить перенос части информации в таблице, если элемент добавляется не в ее конец. Если пользоваться стандартными алгоритмами, применяемыми для организации упорядоченных массивов данных [14], а поиск места включения вести с помощью алгоритма бинарного поиска, то среднее время, необходимое на помещение всех элементов в таблицу, можно оцепить следующим образом:
Тэ = O(N*log2 N) + k*O(N2).
Здесь к — некоторый коэффициент, отражающий соотношение между временами, затрачиваемыми компьютером на выполнение операции сравнения и операции переноса данных.
В итоге при организации логарифмического поиска в таблице идентификаторов мы добиваемся существенного сокращения времени поиска нужного элемента за счет увеличения времени на помещение нового элемента в таблицу. Поскольку добавление новых элементов в таблицу идентификаторов происходит существенно реже, чем обращение к ним, то этот метод следует признать более эффективным, чем метод организации неупорядоченной таблицы.
Построение таблиц идентификаторов по методу бинарного дерева
Можно сократить время поиска искомого элемента в таблице идентификаторов, не увеличивая значительно время, необходимое на ее заполнение. Для этого надо отказаться от организации таблицы в виде непрерывного массива данных.
Существует метод построения таблиц, при котором таблица имеет форму бинарного дерева. Каждый узел дерева представляет собой элемент таблицы, причем корневой узел является первым элементом, встреченным при заполнении таблицы. Дерево называется бинарным, так как каждая вершина в нем может имеТь не более двух ветвей (и, следовательно, не более двух нижележащих вершин). Для определенности будем называть две ветви «правая» и «левая».
Рассмотрим алгоритм заполнения бинарного дерева. Будем считать, что алгоритм работает с потоком входных данных, содержащим идентификаторы (в компиляторе этот поток данных порождается в процессе разбора текста исходной программы). Первый идентификатор, как уже было сказано, помещается в вершину дерева. Все дальнейшие идентификаторы попадают в дерево по следующему алгоритму.
Шаг 1. Выбрать очередной идентификатор из входного потока данных. Если очередного идентификатора нет, то построение дерева закончено.
Шаг. 2. Сдела гь текущим узлом дерева корневую вершину.
ШагЗ. Сравнить очередной идентификатор с идентификатором, содержащемся в текущем узле дерева.
Шаг. 4. Если очередной идентификатор меньше, то перейти к шагу 5, если равен — сообщить об ошибке и прекратить выполнение алгоритма (двух одинаковых идентификаторов быть не должно!), иначе — перей ти к шагу 7.
Шаг 5. Если у текущего узла существует левая вершина, то сделать ее текущим узлом и вернуться к шагу 3, иначе — перейти к шагу 6.
Шаг 6. Создать новую вершину, поместить в нес очередной идентификатор, сделать эту новую вершину левой вершиной текущего узла и вернуться к шагу 1.
Шаг 7. Если у текущего узла существует правая вершина, то сделать ее текущим узлом и вернуться к шагу 3, иначе — перейти к шаг)' 8.
Шаг 8. Создать новую вершину, поместить в нее очередной идентификатор, сделать эту новую вершину правой вершиной текущего узла и вернуться к шагу 1
Рассмотрим в качестве примера последовательность идентификаторов GA, DI, М22, Е, А12, ВС, F. На рис. 13.2 проиллюстрирован весь процесс построения бинарного дерева для этой последовательности идентификаторов.
Поиск нужного элемента в дереве выполняется по алгоритму, схожему с алгоритмом заполнения дерева.
Шаг 1. Сделать текущим узлом дерева корневую вершину.
Шаг 2. Сравнить искомый идентификатор с идентификатором, содержащемся в текущем узле дерева.
Шаг 4. Если идентификаторы совпадают, то искомый идентификатор найден, алгоритм завершается, иначе — надо перейти к шагу 5.
Шаг 5. Если очередной идентификатор меньше, то перейти к шагу 6, иначе — перейти к шагу 7.
Рис. 13.2. Пошаговое заполнение бинарного дерева для последовательности идентификаторов GA, D1, М22, Е, А12, ВС, F
Шаг 6. Если у текущего узла существует левая вершина, то сделать ее текущим узлом и вернуться к шагу 2, иначе искомый идентификатор не найден, алгоритм завершается.
Шаг 7. Если у текущего узла существует правая вершина, то сделать ее текущим узлом и вернуться к шагу 2, иначе искомый идентификатор не найден, алгори гм завершается.
Например, произведем поиск в дереве, изображенном на рис. 13.2, идентификатора А12. Берем корневую вершину (она становится текущим узлом), сравниваем идентификаторы GA и А12. Искомый идентификатор меньше — текущим узлом становится левая вершина D1. Опять сравниваем идентификаторы. Искомый идентификатор меньше — текущим узлом становится левая вершина А12. При следующем сравнении искомый идентификатор найден.
Если искать отсутствующий идентификатор — например, АН, — то поиск опять пойдет от корневой вершины. Сравниваем идентификаторы GA и АН. Искомый идентификатор меньше — текущим узлом становится левая вершина D1. Опять сравниваем идентификаторы. Искомый идентификатор меньше — текущим узлом становится левая вершина А12. Искомый идентификатор меньше, но левая вершина у узла А12 отсутствует, поэтому в данном случае искомый идентификатор не найден.
Для данного метода число требуемых сравнений и форма получившегося дерева во многом зависят от того порядка, в котором поступают идентификаторы. Например, если в рассмотренном выше примере вместо последовательности идеи-
тификаторов GA, DI, М22, Е. А12. ВС. F взять последовательность А12. GA. D1, М22. Е. ВС, F, то полученное дерево будет иметь иной вид. А если в качестве при мера взять последовательность идентификаторов А. В. С. D, Е, F, то дерево выродится в упорядоченный однонаправленный связный список. Эта особенность является недостатком данного метода организации таблиц идентификаторов. Другим недостатком является необходимость работы с динамическим выделением памяти при построении дерева.
Если предположить, что последовательность идентификаторов в исходной про грамме является статистически неупорядоченной (что в целом соответствует действительности), то можно считать, что построенное бинарное дерево будет невырожденным. Тогда среднее время на заполнение дерева (Т,) и на поиск элемента в нем (Т„) можно оцепить следующим образом [6, т. 2]:
Т3 - N*O(log3 N).
Т„- O(log2N).
В целом метод бинарного дерева является довольно удачным механизмом для организации таблиц идентификаторов. Он нашел свое применение в ряде компиляторов. Иногда компиляторы строят несколько различных деревьев для идентификаторов разных типов и разной длины [23, 74].
Хэш-функции и хэш-адресация
Принципы работы хэш функций
Логарифмическая зависимость времени поиска и времени заполнения таблицы идентификаторов — это самый хороший результат, которого можно достичь за счет применения различных методов организации таблиц. Однако в реальных исходных программах количество идентификаторов столь велико, что даже логарифмическую зависимость времени поиска от их числа нельзя признать удовлетворительной. Необходимы более эффективные методы поиска информации в таблице идентификаторов.
Лучших результатов можно достичь, если применить методы, связанные с использованием хэш-функций и хэш-адресации.
Хэш-функцией F называется некоторое отображение множества входных элементов R па множество целых неотрицательных чисел Z: F(r) - п, reR, neZ Сам термин «хэш-функция» происходит от английского термина «hash function» (hash — «мешать», «смешивать», «путать»). Вместо термина «хэширование» иногда используются термины «рандомизация», «переупорядочивание».
Множество допустимых входных элементов R называется областью определения хеш-фуикции. Множеством значений хэш-функции F называется подмножество М из множества целых неотрицательных чисел Z: McZ, содержащее все возможные значения, возвращаемые функцией F: VreR: F(r)eM. Процесс отображения области определения хэш-функции па множество значений называется «хэшированием».
При работе с таблицей идентификаторов хэш функция должна выполнять отображение имен идентификаторов на множество целых неотрицательных чисел.
Областью определения хэш-функции будет множество всех возможных имен идентификаторов.
Хэш-адресация заключается в использовании значения возвращаемого хэш- функцией, в качестве адреса ячейки из некоторого массива данных. Тогда размер массива данных должен соответствовать области значений используемой хэш- функции. Следовательно, в реальном компиляторе область значений хэш-функции никак не должна превышать размер доступного адресного пространства компьютера
Метод организации таблиц идентификаторов, основанный на использовании хэш- адресации, заключается в размещении каждого элемента таблицы в ячейке, адрес которой возвращает хэш-функция, вычисленная для этого элемента. Тогда в идеальном случае для размещения любого элемента в таблице идентификаторов достаточно только вычислить его хэш-функцию и обратиться к нужной ячейке массива данных. Для поиска элемента в таблице необходимо вычислить хэш- функцию для искомого элемента и проверить не является ли заданная ею ячей ка массива пустой (если она не пуста — элемент найден, если пуста — не найден).
На рис. 13.3 проиллюстрирован метод организации таблиц идентификаторов с использованием хэш-адресации. Трем различным идентификаторам Аь А2, А3 соответствуют на рисунке три значения хэш функции пь п2, п3. В ячейки, адресуемые По п2, п3, помещается информация об идентификаторах Аь А2, А3. При поиске идентификатора А3 вычисляется значение адреса п3 и выбираются данные из соответствующей ячейки таблицы.
Рис. 13.3. Организация таблицы идентификаторов с использованием хэш-адресации
Этот метод весьма эффективен — как время размещения элемента в таблице, так и время его поиска определяются только временем, затрачиваемым па вычисление хэш-функции, которое в общем случае несопоставимо меньше времени, необходимого на многократные сравнения элементов таблицы.
Метод имеет два очевидных недостатка. Первый из них — неэффективное использование объема памяти под таблицу идентификаторов: размер массива для ее храпения должен соответствовать области значений хэш-функции, в то время как реально хранимых в таблице идентификаторов может быть существенно! меньше. Второй недостаток — необходимость соответствующего разумного выбора хэш-функции. Этому существенному вопросу посвящены следующие два подраздела.
Построение таблиц идентификаторов на основе хэш-функций
Существуют различные варианты хэш-функций. Получение результата хэш- фупкпии — «хэширование» — обычно достигается за счет выполнения над цепочкой символов некоторых простых арифметических и логических операций Самой простои хэш-функцией для символа является код внутреннего представления в ЭВМ литеры символа. Эту хэш-функцию можно использовать и для цепочки символов, выбирая первый символ в цепочке. Так, если двоичное ASCII представление символа А есть 00100001, то результатом хэширования идентификатора АТаЫе будет код 00100001.
Хэш-функция, предложенная выше, очевидно не удовлетворительна при использовании такой хэш-функции возникнет новая проблема — двум различным идентификаторам, начинающимся с одной и той же буквы, будет соответствовать одно и то же значение хэш-функции. Тогда при хэш-адресации в одну ячейку таблицы идентификаторов по одному и тому же адресу должны быть помещены два различных идентификатора, что явно невозможно. Такая ситуация, когда двум или более идентификаторам соо тветствует одно и то же значение функции, называется коллизией. Возникновение коллизии проиллюстрировано на рис. 13.4 — двум различным идентификаторам А1 и z\z соответствуют два совпадающих значения ХЭШ-фуНКЦИИ П| - п2.
Рис. 13.4. Возникновение коллизии при использовании хэш-адресации
претатора будет отсутствовать. В остальном же она будет мало отличаться от структуры аналогичного компилятора. Следует только учесть, что на последнем этапе — генерации кода — машинные команды не записываются в объектный файл, а выполняются по мере их порождения.
Отсутствие шага оптимизации определяет еще одну особенность, характерную для многих интерпретаторов: в качестве внутреннего представления программы в них очень часто используется обратная польская запись (см. раздел «Генерация кода. Методы генерации кода», глава 14). Эта удобная форма представления операций обладает только одним существенным недостатком — опа плохо поддается оптимизации. Но в интерпретаторах именно это как раз и не требуется.
Далеко не все языки программирования допускают построение интерпретаторов, которые могли бы выполнять исходную программу ио мере поступления команд. Для этого язык должен допускать возможность существования компилятора, выполняющего разбор исходной программы за один проход. Кроме того, язык не может интерпретироваться но мере поступления команд, если он допускает появление обращений к функциям и структурам данных раньше их непосредственного описания. Поэтому данным методом не могут интерпретироваться такие языки, как С и Pascal.
Отсутствие шага оптимизации веде г к тому, что выполнение программы с помощью интерпретатора является менее эффективным, чем с помощью аналогичного компилятора. Кроме того, при интерпретации исходная программа должна заново разбираться всякий раз при ее выполнении, в го время как при компиляции она разбирается только один раз, а после этого всегда используется объектный файл. Таким образом, интерпретаторы всегда проигрывают компиляторам в про- изводителыюсти.
Преимуществом интерпретатора является независимость выполнения программы от архитектуры целевой вычислительной системы. В результате компиляции получается объектный код, который всегда ориентирован на определенную архитектуру. Для перехода па другую архитектуру целевой вычислительной системы программу требуется откомпилировать заново. А для интерпретации программы необходимо иметь только ее исходный текст и интерпретатор с соответствующего языка.
Интерпретаторы долгое время значительно уступали в распространен пости компиляторам. Как правило, интерпретаторы существовали для ограниченного круга относительно простых языков программирования (таких, например, как Basic). Высокопроизводительные профессиональные средства разработки программного обеспечения строились па основе компиляторов.
Новый импульс развитию интерпретаторов придало распространение глобальных вычислительных сетей. Такие сети могут включать в свой состав ЭВМ различной архитектуры, и тогда требование единообразного выполнения на каждой из них текста исходной программы становится определяющим. Поэтому с развитием глобальных сетей и распространением всемирной сети Интернет появилось много новых систем, интерпретирующих текст исходной программы. Многие языки программирования, применяемые во Всемирной сети, предполагают именно интерпретацию текста исходной программы без порождения объектного кода.
Шаг 2. Сравнить имя элемента в ячейке таблицы идентификаторов по адресу in, с именем искомого элемента А. Если они совпадают, то искомый элемент найден и алгоритм завершен, иначе — перейти к шагу 3.
Шаг 3. Проверить значение поля ссылки в ячейке таблицы идентификаторов по адресу nij. Если оно пустое, то искомый элемент не найден и алгоритм завершен; иначе j:-j+l, выбрать из поля ссылки адрес nij и перейти к шагу 2.
При такой организации таблиц идентификаторов в случае возникновения коллизии алгоритм размещает элементы в ячейках таблицы, связывая их друг с другом последовательно через поле ссылки. При этом элементы не могут попадать в ячейки с адресами, которые потом будут совпадать со значениями хэш-функции. Таким образом, дополнительные коллизии нс возникают. В тоге в таблице возникают своеобразные цепочки связанных элементов, откуда происходит и название данного метода — «метод цепочек».
На рис. 13.6 проиллюстрировано заполнение хэш-таблицы и таблицы идентификаторов для примера, который ранее был рассмотрен па рис. 13.5 для метода простейшего рехэширования. После размещения в таблице для поиска идентификатора А] потребуется 1 сравнение, для А2 — 2 сравнения, для А3 — 1 сравнение, для А4 — 1 сравнение и для А5 3 сравнения (сравните с результатами простого рехэширования).
h(As)—kniП2 ПЗП4 nsh(As)—kniП2 ПЗП4 ns55Рис. 13.6. Заполнение хэш-таблицы и таблицы идентификаторов при использовании метода цепочекРис. 13.6. Заполнение хэш-таблицы и таблицы идентификаторов при использовании метода цепочек
h(As)—kni
П2 ПЗ
П4 ns
h(As)—kni
П2 ПЗ
П4 ns
5
5
Рис. 13.6. Заполнение хэш-таблицы и таблицы идентификаторов при использовании метода цепочек
Рис. 13.6. Заполнение хэш-таблицы и таблицы идентификаторов при использовании метода цепочек
Естественно, что хэш-функциД, допускающая коллизии, не может быть напрямую использована для хэш-адресации в таблице идентификаторов. Причем достаточно получить хотя бы один случай коллизии на всем множестве идентификаторов, чтобы такой хэш-функцией нельзя было пользоваться непосредственно. Но в примере взята самая элементарная хэш-функция. А возможно ли построить нетривиальную хэш-функцию, которая бы полностью исключала возникновение коллизий?
Очевидно, что для полного исключения коллизий хэш-функция должна быть взаимно однозначной: каждому элементу из области определения хэш-функции должно соответствовать одно значение из ее множества значений, но и каждому значению из множества значений этой функции должен соответствовать только один элемент из области ее определения. Тогда любым двум произвольным элементам из области определения хэш-функции будут всегда соответствовать два различных ее значения. Теоретически для идентификаторов такую хэш-фупк- цию построить можно, т»к как и область определения хэш-функции (все возможные имена идентификаторов), и область ее значений (целые пеотрицагель- ные числа) являются бесконечными счетными множествами. Теоретически можно организовать взаимно однозначное отображение одного счетного множества на другое, но практически это сделать исключительно сложно*.
Практически существует ограничение, делающее создание взаимно однозначной хэш-функции для идентификаторов невозможным Дело в том, что в реальности область значений любой хэш-функции ограничена размером доступного адресного пространства в данной архитектуре компьютера. При организации хэш-адресации значение, используемое в качестве адреса таблицы идентификаторов, не может выходить за пределы, заданные адресной сеткой компьютера. Множество адресов любого компьютера с традиционной архитектурой может быть велико, но всегда конечно, то есть ограничено. Организовать взаимно однозначное отображение бесконечного множества на конечное даже теоретически невозможно. Можно учесть, что длина принимаемой во внимание строки идентификатора в реальных компиляторах также практически ограничена — обычно она лежит в пределах от 32 до 128 символов (то есть и область определения хэш-функции конечна). Но и тогда количество элементов в конечном множестве, составляющем область определения функции, будет превышать их количество в конечном множестве области значений функции (количество всех возможных идентификаторов все равно больше количества допустимых адресов в современных компьютерах). Таким образом, создать взаимно однозначную хэш-функцию практи-
Элсмситарным примером такого отображения для строки символов является сопоставление ей двоичного числа, полученного нутом конкатенации кодов символов, входящих в строку. Фактически сама строка тогда будет выступать адресом при хэш-адресации. Практическая ценность такого отображения весьма сомнительна.
дет не столь велико. Тогда и время поиска одного среди них будет незначительным (в принципе при высоком качестве хеш-функцпи подойдет даже перебор по неупорядоченному списку).
Такой подход имеет преимущество по сравнению с методом цепочек: для хранения идентификаторов с совпадающими значениями хэш-функции используются области памяти, не пересекающиеся с основной таблицей идентификаторов, а значит, их размещение не приведет к возникновению дополнительных коллизий. Недостатком метода является необходимость работы с динамически распределяемыми областями памяти. Эффективность такого метода, очевидно, в первую очередь зависит от качества применяемой хэш-функции, а во вторую — от метода организации дополнительных хранилищ данных.
Хэш-адресация — это метод, который применяется нс только для организации таблиц идентификаторов в компиляторах. Данный метод нашел свое применение и в операционных системах (см. часть 1 данного пособия), и в системах управления базами данных. Интересующиеся читатели могут обратиться к соответствующей литературе [23, 74].
Лексические анализаторы (сканеры). Принципы построения сканеров
Назначение лексического анализатора
Прежде чем перейти к рассмотрению лексических анализаторов, необходимо дать четкое определение того, что же такое лексема.
Лексема (лексическая единица языка) это структурная единица языка, которая состоит из элементарных символов языка и не содержит в своем составе других структурных единиц языка
Лексемами языков естественного общения являются слова*. Лексемами языков программирования являются идентификаторы, константы ключевые слова языка знаки операций и т п Состав возможных лексем каждого конкретного языка программирования определяется синтаксисом этого языка.
Лексический анализатор (или сканер) — это часть компилятора, которая читает исходную программу и выделяет в се тексте лексемы входного языка. На вход лексического анализатора поступает текст исходной программы, а выходная информация передается для дальнейшей обработки компилятором на этапе синтаксического анализа и разбора.
С теоретической точки зрения лексический анализатор не является обязательной частью компилятора. Все его функции могут выполняться па этапе синтаксического разбора, поскольку полностью регламентированы синтаксисом
1 В языках естественного общения лексикой называется словарный запас языка. Лексический состав языка изучается лексикологией и фразеологией, а значение лексем (слов языка) — семасиологией. В языках программирования словарный запас, конечно, не столь интересен и специальной наукой не изучается
Метод цепочек является очень эффективным средством организации таблиц идентификаторов. Среднее время на размещение одного элемента и на поиск элемента в таблице для него зависит только от среднего числа коллизий, возникающих при вычислении хэш-функции. Накладные расходы памяти, связанные с необходимостью иметь одно дополнительное поле указателя в таблице идентификаторов на каждый ее элемент, можно признать вполне оправданными. Этот метод позволяет более экономно использовать память, но требует организации работы с динамическими массивами данных.
Комбинированные способы построения таблиц идентификаторов
Выше в примере была рассмотрена весьма примитивная хэш-функция, которую никак нельзя назвать удовлетворительной. Хорошая хэш-функция распределяет поступающие па ее вход идентификаторы равномерно на все имеющиеся в распоряжении адреса, так что коллизии возникают не столь часто. Существует большое множество хэш-функций. Каждая из них стремится распределить адреса под идентификаторы по своему алгоритму, но идеального хэширования достичь не удается.
В реальных компиляторах практически всегда так или иначе используется хэш- адресация. Алгоритм применяемой хэш-функции обычно составляет «ноу-хау» разработчиков компилятора. Обычно при разработке хэш-фуикции создатели компилятора стремятся свести к минимуму количество возникающих коллизий не па всем множестве возможных идентификаторов, а на тех их вариантах, кото рые наиболее часто встречаются во входных программах. Конечно, принять во внимание все допустимые исходные программы невозможно. Чаще всего выполняется статистическая обработка встречающихся имен идентификаторов на некотором множестве типичных исходных программ, а также принимаются во внимание соглашения о выборе имен идептификаюров, общепринятые для входного языка. Хорошая хэш-функция — это шаг к значительному ускорению работы компилятора, поскольку обращения к таблицам идентификаторов выполняются многократно на различных фазах компиляции.
То, какой конкретно метод применяется в компиляторе для организации таблиц идентификаторов, зависит от реализации компилятора. Один и тот же компилятор может иметь даже несколько разных таблиц идентификаторов, организованных на основе различных методов.
Как правило, применяются комбинированные методы. В этом случае, как и для метода цепочек, в таблице идентификаторов организуется специальное дополнительное поле ссылки. Но в отличие от метода цепочек оно имеет несколько иное значение. При отсутствии коллизий для выборки информации из таблицы используется хэш-функция, поле ссылки остается пустым. Если же возникает коллизия, то через поле ссылки организуется поиск идентификаторов, для которых значения хэш-функции совпадают по одному из рассмотренных выше методов: неупорядоченный список, упорядоченный список или же бинарное дерево. При хорошо построенной хэш-функции коллизии будут возникать редко, поэтому количество идентификаторов, для которых значения хэш-функции совпали бу-
Важно отметить, что знаки операции сами также являются лексемами и необходимо не пропустить их при распознавании текста.
Как правило, сканеры действуют по следующему принципу: очередной символ из входного потока данных добавляется в лексему всегда, когда он может быть туда добавлен. Как только символ не может быть добавлю и в лексему, то считается, что он является границей лексемы и началом следующей лексемы (сети символ не является пустым разделителем — пробелом, символом табуляции или перевода строки, знаком комментария). Такой принцип не всегда позволяет правильно определить границы лексем в том случае, когда они не разделены пустыми символами. Например, приведенная выше строка языка С k=i+++++j; будет разбита па лексемы следующим образом: k = 1++ ++ + j: — и это разбиение неверное, компилятор должен будет выдать пользователю сообщение об ошибке, хотя правильный вариант распознавания строки существует.
Однако разработчики компиляторов сознательно идут па то, чго отсекают некоторые правильные, но не вполне читаемые варианты исходных программ. Попытки усложнить лексический распознаватель неизбежно приведут к необходимости его взаимосвязи с синтаксическим разбором. Это потребует организации их параллельной работы и неизбежно снизит эффективность работы всего компилятора. Возникшие накладные расходы никак не оправдываются достигаемым эффектом — чтобы распознавать строки с сомнительными лексемами, достаточно лишь обязать пользователя явно указать с помощью пробелов (или других незначащих символов) границы лексем, что значительно проще
Нс для всех входных языков такой подход возможен. Например, для рассмотренного выше примера с языка FORTRAN невозможно применить указанный метод ■ разница между оператором цикла и оператором присваивания слитком существенна, чтобы сю можно было пренебречь. Здесь придется прибегнуть к связи с сiinrai (ическим разбором. В таком случае говорят о непрямой рабою лексического анализатора.
Лексический анализатор работает прямо, если для данного входного текста (цепочки символов входного языка) и текущего положения указателя в нем он определяет лексему, расположенную непосредственно справа от указателя, и сдвигает сам указатель вправо от части текста, образующей эту лексему. При йрямой работе лексическою анализатора возможно его последовательное взаимодействие с синтаксическим ра юзнавателем.
Лексический анализатор работает непрямо, если для данного входного текста (цепочки символов входного языка), заданного тина лексемы и текущего поло ження указа геля в нем он определяет лексему, расположенную непосредственно справа от указателя, и если она соответствует требуемому тину, то сдвигает указатель вправо о! части текста, образующей эгу лексему. В отличие от прямого
чески ни в каком варианте невозможно. Следовательно, невозможно избежать возникновения коллизий.
Пока для двух раз гичных элементов результаты хэширования различны, время поиска совпадает со временем, затраченным на хэширование. Однако возникает затруднение, когда результаты хэширования двух разных элементов совпадают. Для решения проблемы коллизии можно использовать много способов. Одним из них является метод «рехэширования» (или «расстановки»). Согласно этому методу, если для элемента А адрес h(A), вычисленный с помощью хэш-функции, указывает на уже занятую ячейку, то необходимо вычислить значение функции П|=Ь|(А) и проверить занятость ячейки по адресу пь Если и она занята то вычисляется значение h2(А), и так до тех пор, пока либо не будет найдена свободная ячейка, либо очередное значение hj(A) совпадет с h(A). В последнем случае считается, что таблица идентификаторов заполнена и места в ней больше нет — выдается информация об ошибке размещения идентификатора в таблице. Особенностью метода является то, что первоначально таблица идентификаторов должна быть заполнена информацией, которая позволила бы говорить о том, что все ее ячейки являются пустыми (не содержат данных). Например, если используются указатели для храпения имен идентификаторов, то таблицу надо предварительно заполнить пустыми указателями.
Такую таблицу идентификаторов можно организовать по следующему алгоритму размещения элемента.
Шаг 1. Вычислить значение хэш-функции n - h(A) для нового элемента А
Шаг 2. Если ячейка по адресу п пустая, то поместить в нее элемент А и завершить алгоритм, иначе i:-l и перейти к шагу 3.
Шаг 3. Вычислить щ - hj(A). Если ячейка по адресу п, пустая, то поместить в нее элемент А и завершить алгоритм, иначе перейти к шагу 4.
Шаг 4. Если п - п„ то сообщить об ошибке и завершить алгоритм, иначе i:-i+1 и вернуться к шагу 3.
Тогда поиск элемента А в таблице идентификаторов, организованной таким образом, будет выполняться по следующему алгоритму.
Шаг 1. Вычислить значение хэш-функции n - h(A) для нового элемента А.
Шаг 2. Если ячейка по адресу п пустая, то элемент не найден, алгоритм завершен, иначе сравнить имя элемента в ячейке п с именем искомого элемента А. Если они совпадают, то элемент найден и алгоритм завершен, иначе =1 и перейти к шагу 3.
Шаг 3. Вычислить в( - hj(A). Если ячейка по адресу щ пустая или п - ni( то элемент не найден и алгоритм завершен, иначе сравнить имя элемен га в ячейке п, с именем искомого элемента А. Если они совпадают, то элемент найден и алгоритм завершен, иначе i:-i+l и повторить к шаг 3.
Алгоритмы размещения и поиска элемента схожи по выполняемым операциям. Поэтому они будут совпадать и по оценкам времени, необходимого для их выполнения.
нустима, входящие в нее операнды к, 1 и j должны быть описаны и должны допускать выполнение операций языка ++ и +.
Поэтому в большинстве компиляторов лексический и синтаксический анализаторы — это взаимосвязанные части. Возможны два принципиально различных метода организации взаимосвязи лексического анализа и синтаксического разбора:
последовательный;
параллельный.
При последовательном варианте лексический анализатор просматривает весь текст исходной программы от начала до конца и преобразует его в структурированный набор данных. Этот набор данных называют также таблицей лексем. В таблице лексем ключевые слова языка, идентификаторы и константы, как правило заменяются на специально оговоренные коды, им соответствующие (конкретная кодировка определяется при реализации компилятора). Для идентификаторов и констант, кроме того, устанавливается связь между таблицей лексем и таблицей идентификаторов, которая заполняется параллельно.
В этом варианте лексический анализатор просматривает весь текст исходной программы один раз от начала до конца. Таблица лексем строится полностью вся сразу, и больше к ней компилятор не возвращается. Всю дальнейшую обработку выполняют следующие фазы компиляции.
При параллельном варианте лексический анализ исходного текста выполняется поэтапно так, что синтаксический анализатор, выполнив разбор очередной конструкции языка, обращается к сканеру за следующей лексемой. При этом он может сообщить информацию о том, какую лексему следует ожидать. В процессе разбора при возникновении ошибки может происходить «откат назад», чтобы попытаться выполнить анализ текста на другой основе. И только после того, как синтаксический анализатор успешно выполнит разбор очередной конструкции языка (обычно такой конструкцией является оператор исходного языка), лексический анализатор помещает найденные лексемы в таблицу лексем и таблицу идентификаторов и продолжает разбор дальше в том же порядке
Работа синтаксического и лексического анализаторов в варианте их параллельного взаимодействия изображена в виде схемы на рис. 13.7.
В качестве варианта таблицы лексем можно рассмотреть некоторый фрагмент кода на языке Pascal и соответствующую ему таблицу лексем, представленную в табл. 13.1
begin
for i:-l to N do
fg :- fg * 0.5
При такой организации таблиц идентификаторов в случае возникновения коллизии алгоритм размещает элементы в пустых ячейках таблицы, выбирая их определенным образом. При этом элементы могул попадать в ячейки с адресами, которые потом будут совпадать со значениями хэш-функции, что приведет к возникновению новых, дополнительных коллизий. Таким образом, количество операций, необходимых для поиска или размещения в таблице элемента, зависит от заполненности таблицы. Естественно, функции h,(A ) должны вычисляться единообразно на этапах размещения и поиска элемента. Вопрос только в том, как организовать вычисление функций h (А).
Самым простым методом вычисления функции h,(A) является ее организация в виде hj(A) - (h(A) + р;) mod Nm, где р( — некоторое вычисляемое целое число, a Nm — максимальное число элементов в таблице идентификаторов. В свою очередь, самым простым подходом здесь будет положить р, - L Тогда получаем формулу h,(A) - (h(A)+i) mod Nm. В этом случае при совпадении значений хэш- функции для каких-либо элементов поиск свободной ячейки в таблице начинается последовательно от текущей позиции, заданной хэш-функцией 11(A).
Этот способ нельзя признать особенно удачным — при совпадении хэш-адрссов элементы в таблице начинают группироваться вокруг них что увеличивает число необходимых сравнений при поиске и размещении. Среднее время поиска элемента в такой таблице в зависимости от числа операций сравнения можно оценить следующим образом [23]:
Тп- O((l Lf/2)/(l- Lf)).
Здесь Lf — (load factor) степень заполненности таблицы идентификаторов — отношение числа занятых ячеек таблицы к максимально допустимому числу элементов в ней: Lf - N/Nm.
Рассмотрим в качестве примера ряд последовательных ячеек таблицы пь п2, п3, п4, п5 и ряд идентификаторов, которые надо разместить в ней: А„ А2, А3, А4, А5 при условии, что hfA,)- h(A2) - h(A5) = nt; h(A3) - n2; h(A4) - n4. Последовательность размещения идентификаторов в таблице при использовании простейшего метода рехэширования показана на рис. 13.5. В итоге после размещения в таблице для поиска идентификатора А, потребуется 1 сравнение, для А2 — 2 сравнения, для А3 — 2 сравнения, для А4 — 1 сравнение и для А3 — 5 сравнений. Даже такой примитивный метод рехэширования является достаточно эффективным средством организации таблиц идентификаторов при неполном заполнении таблицы. Имея, например, даже заполненную на 90 % таблицу для 1024 идентификаторов, в среднем необходимо выполнить 5,5 сравнений для поиска одного идентификатора, в то время как даже логарифмический поиск дает в среднем от 9 до 10 сравнений. Сравнительная эффективность метода будет еще выше при росте числа идентификаторов и снижении заполненности таблицы
Среднее время на помещение одного элемента в таблицу и на поиск элемента в таблице можно снизить, если применить более совершенный метод рехэширования. Одним из таких методов является использование в качестве р; для функции hi(A) - (h(A)+pj)modNm последовательности псевдослучайных целых чисел Рь Рг> •••> Рк- При хорошем выборе генератора псевдослучайных чисел длина по-
следователыюсти к будет k=Nm. Тогда оредпее время поиска одного элемента в таблице можно оценить следующим образом [23]:
Е„-О((1Д0*1о&(1-LQ).Е„-О((1Д0*1о&(1-LQ).Рис. 13 5. Заполнение таблицы идентификаторов при использовании простейшего рехэшированияРис. 13 5. Заполнение таблицы идентификаторов при использовании простейшего рехэширования
Е„-О((1Д0*1о&(1-LQ).
Е„-О((1Д0*1о&(1-LQ).
Рис. 13 5. Заполнение таблицы идентификаторов при использовании простейшего рехэширования
Рис. 13 5. Заполнение таблицы идентификаторов при использовании простейшего рехэширования
Существуют и другие методы организации функций рехэшировапия hj(A), основанные на квадратичных вычислениях или, например, на вычислении по формуле: h,(A) = (h(A)*i) mod N„„ если Nin — простое число [23]. В целом рехэширование позволяет добиться неплохих результатов для эффективного поиска элемента в таблице (лучших, чем бинарный поиск и бинарное дерево), но эффективность метода сильно зависит от заполненности таблицы идентификаторов и качества используемой хэш-функции — чем реже возникают коллизии, тем выше эффективность метода. Требование неполного заполнения таблицы ведет к неэффективному использованию объема доступной памяти.
Построение таблиц идентификаторов по методу цепочек
Неполное заполнение таблицы идентификаторов при применении хэш-функцнй ведет к неэффективному использованию всего объема памяти, доступного компилятору Причем объем неиспользуемой памяти будет тем выше, чем больше информации хранится для каждого идентификатора. Этого недостатка можно избежать, если дополнить таблицу идентификаторов некоторой промежуточной хэш-таблицей.
В ячейках хэш-таблицы может храниться либо пустое значение, либо значение указателя на некоторую область памяти из основной таблицы идентификаторов Тогда хэш-функция вычисляет адрес, по которому происходит обращение сначала к хэш-таблице, а йогом уже через нее по найденному адресу — к самой таблице идентификаторов. Если соответствующая ячейка таблицы идентификаторов пуста, то ячейка хэш-таблицы будет содержать пустое значение. Тогда вовсе не
обязательно иметь в самой таблице идентификаторов ячейку для каждого возможного значения хэш-функции — таблицу можно сделать динамической так, чтобы ее объем рос по мере заполнения (первоначально таблица идентификаторов не содержит ни одной ячейки, а все ячейки хэш-таблицы имеют пустое значение). Такой подход позволяет добиться двух положительных результатов: во-первых, нет необходимости заполнять пустыми значениями таблицу идентификаторов — это можно сделать только для хэш-таблицы; во-вторых, каждому идентификатору будет соответствовать строго одна ячейка в таблице идентификаторов (в ней не будет пустых неиспользуемых ячеек). Пустые ячейки в гаком случае будут только в хэш-таблице, и объем неиспользуемой памяти не будет зависеть от объема информации, хранимой для каждого идентификатора — для каждого значения хэш-функции будет расходоваться только память, необходимая для храпения одного указателя на основную таблицу идентификаторов.
На основе этой схемы можно реализовать еще один способ организации таблиц идентификаторов с помощью хэш-функции, называемый «метод цепочек». Для метода цепочек в таблицу идентификаторов для каждого элемента добавляется еще одно иоле, в котором может содержаться ссылка на любой элемент таблицы. Первоначально это поле всегда пустое (никуда не указывает). Также для этого метода необходимо иметь одну специальную переменную, которая всегда указывает на первую свободную ячейку основной таблицы идентификаторов (первоначально — указывает на начало таблицы).
Метод цепочек работает следующим образом по следующему алгоритму.
Шаг 1. Во все ячейки хэш-таблицы поместить пустое значение, таблица идентификаторов не должна содержать ни одной ячейки, переменная FieePtr (указатель первой свободной ячейки) указывает па начало таблицы идентификаторов; i:-l.
Шаг. 2. Вычислить значение хэш-функции п, для нового элемента А;. Если ячей ка хэш-таблицы по адресу п( пустая, то поместить в нее значение переменной FreePtr и перейти к шагу 5; иначе — перейти к шагу 3.
Шаг 3. Положить j:=l, выбрать из хэш-таблицы адрес ячейки таблицы идентификаторов nij и перейти к шагу' 4.
Шаг 4. Для ячейки таблицы идентификаторов по адресу nij проверить значение поля ссылки. Если оно пустое, то записать в него адрес из переменной FreePtr и перейти к шагу 5; иначе j:=j+l, выбрать из поля ссылки адрес т, и повторить шаг 4. Шаг 5. Добавить в таблицу идентификаторов новую ячейку, записать в нее информацию для элемента А, (поле ссылки должно быть пустым), в переменную FreePtr поместить адрес за концом добавленной ячейки. Если больше пет идентификаторов, которые надо разместить в таблице, то выполнение алгоритма закончено, иначе i:=i+l и перейти к шагу 2.
Поиск элемента в таблице идентификаторов, организованной таким образом, будет выполняться по следующему алгоритму.
Шаг 1. Вычислить значение хэш-функции п для искомого элемента А. Если ячейка хэш-таблицы по адресу п пустая, то элемент не найден и алгоритм завершен, иначе положить j:=l, выбрать из хэш-таблицы адрес ячейки таблицы идентификаторов m=n.
Дополнительные возможности систем программирования
Лексический анализ «на лету». Система подсказок и справок
Лексический анализ «на лету» — это функция текстового редактора в составе системы программирования. Она заключается в поиске и выделении лексем входного языка в тексте программы непосредственно в процессе ее создания разработчиком.
Реализуется это следующим образом: разработчик создает исходный текст программы (набирает его или получает из некоторого другого источника), и в то же время система программирования параллельно выполняет поиск лексем в этом тексте.
В простейшем случае обнаруженные лексемы просто выделяются в тексте с помощью графических средств интерфейса текстового редактора — цветом, шрифтом и г. п. Это облегчает труд разработчика программы, делает исходный текст более наглядным и способствует обнаружению ошибок на самом раннем этапе — на этапе подготовки исходного кода.
В более развитых системах программирования найденные лексемы не просто выделяются по ходу подготовки исходного текста, но и помещаются в таблицу идентификаторов компилятора, входящего в состав системы программирования. Такой подход позволяет экономить время на этапе компиляции, поскольку первая ее фаза — лексический анализ — уже выполнена на этане подготовки исход ного текста программы.
Следующей сервисной возможностью, предоставляемой разработчику системой программирования за счет лексического анализа «на лету», является возможность обращения разработчика к таблице идентификаторов в ходе подготовки исходного текста программы. Разработчик может дать компилятору команду найти нужную ему лексему в таблице. Поиск может выполняться по типу или по какой-то части информации лексемы (например, по нескольким первым буквам). Причем поиск может быть контекстно-зависимым — система программирования предоставит разработчику возможность найти лексему именно того типа который может быть использован в данном месте исходного текста. Кроме самой лексемы разработчику может быть предоставлена некоторая информация о ней — например, гипы и состав формальных параметров для функции, перечень доступных методов для типа или экземпляра класса. Это опять же облегчает труд разработчика, поскольку избавляет его от необходимости помнить состав функций и типов многих модулей (прежде всего, библиотечных) или обращаться лишний раз к документации и справочной информации.
Лексический анализ «на лету» — мощная функция, значительно облегчающая труд, связанный с подготовкой исходного текста. Она входит не только в состав многих систем программирования, но также и в состав многих текстовых редакторов, поставляемых отдельно от систем программирования (в последнем случае опа позволяет настроиться на лексику того или иного языка).
Синтаксический анализатор воспринимает выход лексического анализатора и разбирает его в соответствии с грамматикой входного языка. Однако в грамматике входного языка программирования обычно не уточняется, какие конструкции следует считать лексемами. Примерами конструкций, которые обычно распознаются во время лексического анализа, служат ключевые слова, константы и идентификаторы. По эти же конструкции могут распознаваться и синтаксическим анализатором. На практике не существует жесткого правила, определяющего, какие конструкции должны распознаваться на лексическом уровне, а какие надо оставлять синтаксическому анализатору. Обычно это определяет разработчик компилятора исходя из технологических аспектов программирования, а также синтаксиса и семантики входного языка. Принципы взаимодействия лексического и синтаксического анализаторов были рассмотрены нами в разделе «Лексические анализаторы (сканеры). Принципы построения сканеров» этой главы.
Ниже рассмотрены технические аспекты, связанные с реализацией синтаксических анализаторов для использования результатов их работы на этане генерации кода. Тем не менее основу любого синтаксического анализатора всегда составляет распознаватель, построенный па основе какого-либо класса КС-грамматик. Поэтому главную роль в том. как функционирует синтаксический анализатор и какой алгоритм лежит в его основе, играют принципы построения распознавателей КС-языков, рассмотренные в главе «Контекстно-свободные языки». Без применения этих принципов невозможно выполнить эффективный синтаксический разбор предложений входного явыка.
Дерево разбора. Преобразование дерева разбора в дерево операций
Результатом работы распознавателя КС-грамматики входного языка является последовательность пренил грамматики, примененных для построения входной цепочки. По найденной последовательности, зная тип распознавателя, можно построить цепочку вывода или дерево вывода. В этом случае дерево вывода выступает в качестве дерева синтаксического раэбора и представляе т собой результат работы синтаксического анализатора в компиляторе.
Однако пи цепочка вывода, ни дерево синтаксического разбора не являются целью работы компилятора. Дерево вывода содержит массу избыточной информации, которая для дальнейшей работы компилятора не требуется. Эта информация включает в себя все нетерминальные символы, содержащиеся в узлах дерева, — после того как дерево построено, они не несут никакой смысловой нагрузки п не представляют для дальнейшей работы интереса.
Для полного представления о типе п структуре найденной и разобранной синтаксической конструкции входного языка в принципе достаточно знать последовательность номеров правил грамматики, примененных для ее построения. Однако форма представления этой достаточной информации может быть различной как в зависимости от реализации самого компилятора, так и от фазы ком- ниляцип. Эта форма называется внутренним представлением программы (иногда используется также термины «промежуточное представление» или «промежуточная профамма»).
Существуют методы, позволяющие снизить затраты кода и времени выполнения на передачу параметров в процедуры и функции и повысить в результате эффективность результирующей программы:
передача параметров через регистры процессора;
О подстановка кода функции в вызывающий объектный код.
Метод передачи параметров через регистры процессора позволяет разместить все или часть параметров, передаваемых в процедуру или функцию, непосредственно в регистрах процессора, а не в стеке. Это позволяет ускорить обработку параметров функции, поскольку работа с регистрами процессора всегда выполняется быстрее, чем с ячейками оперативной памяти, где располагается стек. Если все параметры удается разместить в регистрах, то сокращается также и объем кода, поскольку исключаются все операции со стеком при размещении в нем параметров. Понятно, что регистры процессора, используемые для передачи параметров в процедуру или функцию, не могут быть задействованы напрямую для вычислений внутри этой процедуры или функции. Поскольку программно доступных регистров процессора всегда ограниченное количество, то при выполнении сложных вычислений могут возникнуть проблемы с их распределением. Тогда компилятор должен выбрать: либо использовать регистры для передачи параметров и спи- зигь эффективность вычислений (часть промежуточных результатов, возможно, потребуется размещать в оперативной памяти или в том же стеке), либо использовать свободные регистры для вычислений и снизить эффективность передачи параметров. Поэтому реализация данного метода зависит от количества npoipaMM- но доступных регистров процессора в целевой вычислительной системе и от используемого в компиляторе алгоритма распределения регистров. В современных процессорах число программно доступных регистров постоянно увеличивается с разработкой все новых и новых нх вариантов, поэтому и значение этого метода постоянно возрастает.
Этот метод имеет ряд недостатков. Во-первых, очевидно, он зависит от архитектуры целевой вычислительной системы. Во-вторых, процедуры и функции, оптимизированные таким методом, не могут быть использованы в качестве процедур или функций библиотек подпрограмм пи в каком виде. Это вызвано тем, что методы передачи параметров через регистры процессора не стандартизованы (в отличие от методов передачи параметров через стек) и зависят от реализации компилятора. Наконец, этот метод не может быть использован, если где бы то ни было в процедуре или функции требуется выполнить операции с адресами (указателями) на параметры.
В большинеч ве современных компиляторов метод передачи параметров в процедуры и функции выбирается самим компилятором автоматически в процессе генерации кода для каждой процедуры и функции. Однако разработчик имеет, как правило, возможность запретить передачу параметров через регистры процессора либо дчя определенных функций (для этого используются специальные ключевые слова входного языка), либо для всех функций в программе (для этого используются настройки компилятора).
Некоторые языки программирования (такие, например, как С и C++) позволяют разработчику исходной программы явно указать, какие параметры или локаль-
Другой удобной сервисной функцией в современных системах программирования является система подсказок и справок. Как правило, опа содержит грн основные части:
справку по семантике и синтаксису используемого входною языка,
подсказку по работе с самой системой программирования;
справку о функциях библиотек, входящих в состав системы программирования.
Система подсказок и справок в настоящее время является составной частью многих прикладных и системных программ. Как правило, она поддерживается соответствующими утилитами ОС. Поэтому, кроме всего прочего, многие системы программирования включают в свой состав сервисные функции, позволяющие создавать и дополнять систему подсказок н справок. Эго делается таким образом, чтобы разработчик мог создавать и распространять вместе со своими прикладными про1раммамн соответствующие им подсказки и справки.
Разработка программ в архитектуре «клиент—сервер»
Распространение динамически подключаемых библиотек и ресурсов прикладных программ привело к ситуации, когда большинство прикладных программ стою представлять собой не единый программный модуль, а набор сложным образом взаимосвязанных между собой компонентов. Многие из этих компонентов либо входили в состав ОС, либо же требовалась их поставка и установка от других разработчиков, которые очень часто могли быть никак не связаны с разработчиками самой прикладной программы.
При этом среди всего множества компонентов прикладной программы можно было выделить две логически цельные составляющие: первая — обеспечивающая «нижний уровень» работы приложения, отвечающая за методы хранения, доступа и разделения данных; вторая — организующая «верхний уровень» работы приложения, включающий в себя логику обработки данных и интерфейс пользователя.
Первая составляющая, как правило, представляла собой набор компонентов сторонних разработчиков. Часто она так или иначе была связала с доступом к базам данных, которые могли предусматривать достаточно сложную организацию. Для работы ее компонентов требовалось наличие высокопроизводительной вычислительной системы.
Вторая составляющая включала в себя собственно алгоритмы, логику и весь ни тсрфсйс, созданные разработчиками программы. Опа требовала наличие связи с методами доступа к данным, содержащимся в первой составляющей. Требования к вычислительным системам, необходимым для выполнения ее компонентов, были обычно существенно ниже, чем для первой составляющей.
Тогда сложилось понятие приложения, построенного на основе архитектуры «клиент-сервер». В первую (серверную) составляющую такого приложения относят все методы, связанные с доступом к данным. Чаще всего их реализует сер-
вер БД (сервер данных) из соответствующей СУБД (системы управления базами данных) в комплекте с драйверами доступа к нему. Во вторую (клиентскую) часть приложения относят все методы обработки данных и представления их пользователю. Клиентская часть взаимодействует, с одной стороны с сервером, получая от него данные, а с другой стороны — с пользователем, ресурсами при- дожепня и ОС, осуществляя обработку данных и отображение результатов. Результаты обработки клиент опять-таки может сохранить в БД, воспользовавшись функциями серверной части.
Кроме того, со временем па рынке СУБД стали доминировать несколько наиболее известных компаний-производителей. Они предлагали стандартизованные интерфейсы для доступа к создаваемым ими СУБД. На них, в свою очередь, стали ориентироваться и разработчики прикладных программ
Такая ситуация оказала влияние и на структуру систем программирования Многие из них стали предлагать средства, ориентированные на создание приложений в архитектуре «клиент—сервер». Как правило, эти средства поставляются в составе системы программирования и поддерживают возможность работы с широким диапазоном известных серверов данных через один или несколько доступных интерфейсов обм, па данными. Разработчик прикладной программы выбирает одно из доступных средств плюс возможный тип сервера (или несколько воз можных типов) и тогда сто задача сводится только к созданию клиентской части приложения, построенной па ос юве выбранного интерфейса.
Создав клиентскую часть, разработчик может далее использовать и распространять ее только в комплексе с соответствующими средствами из состава системы программирования. Интерфейс обмена данными обычно входит в состав системы программирования. Большинство систем программирования предоставляют возможность распространения средств доступа к серверной части без каких-либо дополнительных ограничении
Что касается серверной части, то возможны два пути: простейшие серверы БД требуют ог разработчиков приобретения лицензий на средства создания и отлад кн БД, по часто позволяют распространять результаты работы без дополнительных ограничений мошныс серверы БД ориентированные на работу десятков н сотен пользователей, требуют приобретения лицензий как на создание, тар п на раслространение серверной части приложения. В этом случае конечный пользователь приложения получает целый комплекс программных продуктов от множества разработчиков.
Более подробно об организации приложений на основе архитектуры «клиент- сервер» можно узнать в |9, 35. 63].
Разработка программ в трехуровневой архитектуре. Серверы приложений
Трехуровневая архитектура разработки приложений явилась логическим продолжением идей, заложенных в архитектуре «клиент—сервер».
Недостатком архитектуры приложений тина «клиент—сервер» стал тот факт, что в клиентской части приложения совмещались как довольно сложные функции,
связанные с обработкой получаемых от сервера данных, так и более простые функции организации интерфейса пользователя. Для выполнения этих функций к вычислительной системе должны предъявляться различные требования, и в том случае когда выполняется сложная обработка данных, эти требования со стороны «клиентской» части приложения могут быть непомерно велики. Кроме того, обработка данных («бизнес-логика» приложения), как правило, изменяется незначительно по мере прохождения жизненного цикла, развития приложения и выхода его новых версий. В то же время интерфейсная часть может серьезно видоизменяться и в предельном случае подстраиваться под требования конкретного заказчика.
Еще одним фактором, повлиявшим па дальнейшее развитие архитектуры «клиент-сервер», стало распространение глобальных сетей и всемирной сети Интернет. Многие приложения стали нуждаться в предоставлении пользователю возможности доступа к данным посредством сети. Возможностей архитектуры «клиент-сервер» для этой цели стало во многих случаях недостаточно, поскольку клиент зачастую мог не иметь никаких вычислительных ресурсов, кроме программы навигации по сети (браузера, browser).
Поэтому дальнейшим развитием архитектуры «клиент—сервер» стало разделение клиентской части в свою очередь еще на две составляющих: сервер приложений (английские термины «application server» или «middleware»), реализующий обработку данных («бизнес-логику» приложения), и «тонкий клиент» («thin client»), обеспечивающий интерфейс и доступ к результатам обработки (далее «топкий клиент» будем называть просто «клиентом»). Серверная часть осталась без изменений, но теперь опа получила название «сервер баз данных» («database server»), чтобы не путаться с сервером приложений.
Разделение клиентской части па две составляющих потребовало организации взаимодействия между этими составляющими. Причем это взаимодействие до. окно с одной стороны, бы ть достаточно тесным, так как клиент должен выполнять отображение получаемых от сервера данных за время с разумной задержкой (желательно минимальной); а с другой стороны, он должен допускать обмен данными по протоколам, поддерживаемым глобальными сетями. Поэтому появилось некоторое число стандартов, ориентированных па организацию такого рода взаимодействия. Стали появляться новые интерфейсы обмена данными. Среди них можно выделить семейство стандартов COM/DCOM, предложенных фирмой Microsoft для ОС семейства Microsoft Windows [103], а также семейство стандартов CORBA (Common Object Request Broker Architecture), поддерживаемое широким кругом производителей и разработчиков программного обеспечения для большого спектра различных ОС [99]. Детальное рассмотрение такого рода стандартов и принципов их организации не входит в рамки данного учебного пособия.
Системы программирования в настоящее время ориентируются на поддержку средств разработки приложений в трехуровневой (трехзвенной) архитектуре. Средства поддержки тех или иных стандартов сейчас появляются в составе многих систем программирования [3, 35, 63, 98, 104, 105]. Принципы их использования и распространения аналогичны принципам, применяемым для средств поддержки архитектуры типа «клиент—сервер». Следует заметить, что существуют
системы программирования, ориентированные па разработку либо клиентской части, либо сервера приложений; и в то же время многие системы программирования стремятся предоставить разработчикам средства для создания обеих частей в трехуровневой архитектуре. Серверная часть (сервер баз данных) в трехуровневой архитектуре по-прежнему чаще всего является продуктом другого разработчика*.
Более подробно об организации приложений на основе трехуровневой технологии можно узнать в [2, 35, G3],
Примеры современных систем программирования
В этом разделе пособия ни в коем случае не ставится задача полного описания тех или иных систем программирования, доступных в настоящее время па рынке современных средств разработки программного обеспечения. Полное описание многих из них ио объему будет значительно превышать весь материал данного пособия. Автор только дает краткий обзор наиболее известных и распространенных в настоящее время систем программирования с точки зрения их компонентов, ложащихся па общую структуру типовой системы программирования.
Для краткого описания автором были выбраны только самые известны# из всего широкого спектра систем программирования, распространенные именно на рынке Российской Федерации. Информация о данных системах программирования дается как на основании соответствующей литературы, так и на основании личного опыта работы автора.
Системы программирования компании Borland/lnprise
Системы программирования компании Borland достаточно широко известны разработчикам в России. Известность и распространенность этих систем программирования определила, прежде всего, простота их использования, поскольку именно в системах программирования этой компании были впервые реализованы па практике идеи интегрированной среды программирования.
Turbo Pascal
Система программирования Turbo Pascal была создана компанией Borland на основе расширения языка Pascal, получившего название Borland Pascal. Отсюда происходит и само название системы программирования.
1 Конечно, сервер БД — это программа, и как всякая программа опа создается с использованием соответствующего компилятора; но в этом случае речь уже не идет о создании части трехуровневого приложения — здесь сервер БД выступает как отдельное законченное приложение.
Сам язык Pascal был предложен Н. Виртом в конце 70-х годов как хорошо структурированный учебный язык. Расширения, привнесенные в язык компанией Borland, преследовали две основные цели:
упрощение обработки в языке структур, представляющих наиболее распространенные тины данных — строки и файлы (например, в язык был внесен новый тип данных string);
реализация в языке основных возможностей объектно-ориентированных языков программирования.
Последнее нововведение потребовало серьезной доработки синтаксиса языка. В него были внесены новые ключевые слова, синтаксические конструкции и тины данных. Однако предложенный вариант языка нельзя признать удачным, хотя бы потому, что в нем не полностью реализованы все механизмы объектно-ориентированного программирования. Например, отсутствую! такие мощные средства, как исключения и шаблоны. Есть и другие сложности в использовании языка.
Компания Borland построила и реализовала эффективный однопроходный компилятор с языка Borland Pascal. За счез этого в данной системе программирования удалось добиться относительно высокой скорости компиляции исходных программ. Для ускорения работы компоновщика компанией Borland был предложен собственный уникальный формат объектных файлов — модулей исходной программы — TPU (Turbo Pascal Unit). По этой причине модули, созданные в системе программирования Turbo Pascal, не могли быть использованы в других системах программирования. Также из них невозможно было создавать библиотеки, ориентированные на другие языки и системы программирования. Обратная задача — использование стандартных объектных файлов и библиотек в системе программирования Turbo Pascal была решаема, но имела серьезные ограничения. В состав системы программирования Turbo Pascal, кроме компилятора с языка Borland Pascal, входил также компилятор с языка ассемблера (а с появлением возможности разработки результирующих программ для среды Microsoft Windows — компилятор ресурсов). Среда программирования позволяла компоновать как единые исполняемые файлы, так и оверлейные программы для ОС типа MS-DOS.
Первоначально система программирования Turbo Pascal строилась па основе библиотеки RTL (run time library) языка Borland Pascal. Эта библиотека не предоставляла пользователю широкого набора функций — в основном она Только реализовывала базовые математические функции и функции языка. Однако можно сказать об одной характерной черте данной библиотеки она включала в свой состав объектный код менеджера памяти для управления раенретеленн- ем динамической памяти («кучей» — heap — в терминах языка Pascal), который автоматически подключался к каждой результирующей программе, созданной с помощью данной системы программирования. Этот модуль получился доволь-
входного языка. Однако существует несколько причин, по которым в состав практически всех компиляторов включают лексический анализ:
О применение лексического анализатора упрощает работу с текстом исходной программы на этане синтаксического разбора и сокращает объем обрабатываемой информации, так как лексический анализатор структурирует поступающий на вход исходный текст программы и выкидывает всю незначащую информацию;
для выделения в тексте и разбора лексем возможно применять простую, эффективную и теоретически хорошо проработанную технику анализа, в то время как на этапе синтаксического анализа конструкций исходного языка используются достаточно сложные алгоритмы разбора;
сканер отделяет сложный по конструкции синтаксический анализатор от работы непосредственно с текстом исходной программы, структура которого может варьироваться в зависимости от версии входного языка — при такой конструкции компилятора для перехода от одной версии языка к другой достаточно только перестроить относительно простой лексический анализатор.
Функции, выполняемые лексическим анализатором, и состав лексем, которые он выделяет в тексте исходной программы, могут меняться в зависимости от версии компилятора. То, какие функции должен выполнять лексический анализатор и какие типы лексем он должен выделять во входной программе, а какие оставлять для этапа синтаксического разбора, решают разработчики компилятора. В основном лексические анализаторы выполняют исключение из текста исходной программы комментариев, незначащих пробелов, символов табуляции и перевода строки, а также выделение лексем следующих типов: идентификаторов, строковых, символьных и числовых констант, ключевых (служебных) слов входного языка, знаков операций и разделителей.
В простейшем случае фазы лексического и синтаксического анализа могут выполняться компилятором последовательно. Но для многих языков программирования на этапе лексического анализа может быть недостаточно информации для однозначного определения типа и границ очередной лексемы. Иллюстрацией такого случая может служить пример оператора программы па языке FORTRAN, когда по части текста DO 10 1=1 невозможно определить тип оператора (а соответственно, и границы лексем). В случае DO 10 1=1.15 это присвоение вещественной переменной DO10I значения константы 1 15 (пробелы в языке FORNTAN игнорируются), а в случае DO 10 1=1.15 — цикл с перечислением от 1 до 15 по целочисленной переменной I до метки 10.
Другим примером может служить оператор языка С, имеющий вид. k=i+++++j Существует только одна единственно верная трактовка этого оператора, k = i++ + ++J: (если явно пояснить се с помощью скобок, то данная конструкция имеет вид k = (1++) * (++)):). Однако найти ее лексический анализатор может, лишь просмотрев весь оператор до конца и перебрав все варианты, причем неверные варианты могут быть обнаружены только на этапе семантического анализа (например, вариант к = (1++)++ + J; является синтаксически правильным, но семантикой языка С не допускается). Конечно, чтобы эта конструкция была в принципе до-
но удачным и нашел свое дальнейшее применение в других системах программирования данной компании разработчика.
Несмотря на недостатки, система программирования Turbo Pascal получила широкое распространение и завоевала свое место на рынке. Основной причиной явилось то, что система впервые была построена в виде интегрированной среды. Данный факт предопределил ее широкое распространение, и, прежде всего, в университетской среде, где требовались простые и понятные в использовании средства разработки.
Первые версии системы программирования были ориентированы только на работу в ОС MS-DOS персональных компьютеров на базе процессоров типа Intel 80x86. На исполнение в среде этой ОС были ориентированы и результирующие программы, разрабатываемые с помощью данной среды программирования.
Система программирования Turbo Pascal получила широкое распространение и дальнейшее развитие. Компания Borland выпустила несколько ее реализаций (наиболее распространенные из них — версии 5.5 и 7.0). Последние реализации данной системы программирования могли создавать результирующие программы, ориентированные на работу как в ОС типа MS-DOS, так и в среде типа Microsoft Windows. В них были реализованы все основные преимущества, предоставляемые интегрированной средой программирования," такие как лексический анализ программ «па лету» и встроенная контекстная подсказка.
По мерс распространения системы программирования Turbo Pascal шла разработка библиотек подпрограмм и функций для нее. Были созданы такие библиотеки, как Turbo Professional (TP), Turbo Vision, Object Window Library (OWL) для среды MS-DOS и ObjectWindows для среды Microsoft Windows. Широкому распространению данных библиотек ио-прежнему мешал тот факт, что в системе программирования Turbo Pascal используется уникальный, нестандартный формат объектных файлов. Отсутствие стандарта языка Borland Pascal во многом сдерживало развитие этой системы программирования и не способствовало ее применению как профессионального средства разработки.
Системе программирования Turbo Pascal здесь уделено много внимания по той причине, что это одна из самых распространенных в настоящее время систем программирования учебного назначения. Кроме того, это первая появившаяся на рынке система программирования, которая полностью реализовала в себе идеи интегрированной среды программирования. Эти идеи, заложенные в системе программирования Turbo Pascal, нашли применение во многих современных системах программирования. По системе программирования Turbo Pascal выпущена огромная масса литературы — можно обратиться, например, к книгам [34, 71, 72].
Borland Delphi
Система программирования Borland Delphi явилась логическим продолжением и дальнейшим развитием идей, заложенных компанией-разработчиком еще в системе программирования Turbo Pascal.
В качестве основных в новой системе программирования можно указать следующие принципиальные изменения:
новый язык программирования — Object Pascal, явившийся серьезной переработкой прежней версии языка Borland Pascal;
компонентная модель среды разработки, в первую очередь ориентированная па технологию разработки RAD (rapid application development).
Язык программирования Object Pascal создавался в то время, когда на рынке средств разработки уже существовало значительное количество объектно-ориентированных языков, включая такие известные, как C++ и Java. Компания Borland попыталась учесть все недостатки существующих языков объектно-ориентированного программирования, а также свой опыт создания языка Borland Pascal. По мнению автора, во многом ей это удалось. Новый язык вышел довольно удачным как с точки зрения синтаксиса, так и с точки зрения предоставляемых возможностей. Этот язык поддерживает практически все основные механизмы объектно-ориентированного программирования.
Компонентная модель среды разработки предусматривает создание основной части программы в виде набора взаимосвязанных компонентов — классов объектно-ориентированного языка. Во время разработки исходной программы (design time) компоненты предстают в виде графических образов и обозначений, связанных между собой. Каждый компонент обладав определенным набором свойств (properties), событий (events) и методов. Каждому из них соответствует свой фрагмент исходного кода программы, отвечающий за обработку метода или реакции на какое-то событие. Разработчик может располагать на экране и связывать между собой компоненты, а также редактировать связанный с ними исходный код программы. Причем поведение компонентов во время выполнения программы (run time) полностью определяется их взаимосвязью, исходным колом программы и объектным кодом самого компонента.
Система программирования Borland Delphi предназначена для создания результирующих программ, выполняющихся в среде ОС Windows различных типов.
Основу системы программирования Borland Delphi и ее компонентной модели составляет библиотека VCL (v sual component library). В »той библиотеке реализованы в виде компонентов все основные органы управления и интерфейса ОС. Также в ее состав входя г классы, обеспечивающие разработку приложений для архитектуры «клиент—сервер» и трехуровневой архитектуры (в современных реализациях Borland Delphi). Разработчик имеет возможность не только использовать любые компоненты, входящие в состав библиотеки VCL, по также и разрабатывать свои собственные компоненты, основанные на любом из классов дайной библиотеки. Эти новые компоненты становятся частью системы программирования и затем могут быть использованы другими разработчиками.
Для поддержки разработки результирующих программ для архитектуры «клиент-сервер» в состав Borland Delphi входит средство BDE (Borland database engine). Оно обеспечивает результирующим программам возможность доступа к широкому диапазону серверов БД посредством классов библиотеки VCL. Посредством BDE результирующая программа может взаимодействовать с серверами БД типа Microsoft SQL Server, Interbase, Sybase, Oracle и т. н. Система программирования Borland Delphi поддерживает также создание результирующих программ. выполняющихся в архитектуре «клиент—сервер», на базе других технологий, например ADO (ActiveX Data Objects).
Система программирования Borland Delphi выдержала несколько реализаций, Последние реализации данной системы программирования (прежде всего, версии 4 и 5) включают широкий набор средств для поддержки разработки результирующих программ в трехуровневой архитектуре приложений. Система программирования Borland Delphi позволяет разрабатывать как серверную, так и клиентскую часть приложения в данной архитектуре. Возможно использование как технологий COM/DCOM (наиболее распространенных в среде ОС типа Microsoft Windows), так и технологии CORBA (но только при разработке клиентской части приложения).
В качестве недостатков данной системы программирования можно указать использование нестандартного формата объектных файлов (сохранился еще от системы Turbo Pascal, но в последней версии Borland Delphi 5 можно использовать стандартный формат), а также нестандартного формата для храпения ресурсов пользовательского интерфейса. Кроме того, сам язык Object Pascal не является признанным стандартом. Этот факт несколько затрудняет использование Borland Deljihi в масштабных проектах в качестве основного средства разработки.
Тем не менее система программирования Borland Delphi получила широкое распространение среди разработчиков в Российской Федерации. Более подробную информацию по данной системе программирования можно найти в технической литературе (например, [35]), либо на сайте компании-производителя [98].
Borland C++ Builder
Система программирования Borland C++ Builder объединила в себе идеи интегрированной среды разработки, реализованные компанией в системах программирования Turbo Pascal и Borland Delphi с Возможностями языка программирования C++. История этой системы программирования начинается с пн титрированной среды разработки Borland Turbo С.
Среда Turbo С представляла собой реализацию идей, заложенных компанией- разработчиком в системе программирования Turbo Pascal для языка программирования С. Компания Borland стремилась перенести удачную реализацию идей интегрированной среды разработки на новую основу. Компилятор Turbo С не был однопроходным, и потому время компиляции исходной программы превы шало время компиляции аналогичной программы в Tuibo Pascal. Кроме того, в системе программирования использовался стандартный компоновщик исполняемых файлов MS-DOS.
Преимущество Turbo С заключалось в том, что эта система программирования строилась на базе стандартного языка программирования С. Данный язык получил широкое распространение среди разработчиков в качестве языка системного программирования, для него существовали компиляторы под многие типы целевых архитектур. В этом было главное отличие системы программирования Turbo С от схожей по организации системы программирования Turbo Pascal, ко- горая строилась на основе поддержки нестандартного расширения языка Pascal.
С разни гнем системы программирования на базе Turbo Pascal развивались и системы программирования на основе Turbo С.
Очевидно, что последовательный вариант организации взаимодействия лексического анализа и синтаксического разбора является более эффективным, так как он не требует организации сложных механизмов обмена данными и не нуждается в повторном прочтении уже разобранных лексем. Этот метод является и более простым. Однако не для всех языков программирования возможно организовать такое взаимодействие. Это зависит в основном от синтаксиса языка, заданного его грамматикой. Большинство современных широко распросграненных языков программирования, таких как С и Pascal, тем не менее позволяют построить лексический анализ по более простому, последовательному методу, что дает ряд определенных преимуществ.
Принципы построения лексических анализаторов
Лексический анализатор имеет дело с такими объектами, как различного рода константы и идентификаторы (к последним относятся и ключевые слова). Язык констант и идентификаторов в большинстве случаев является регулярным — то есть может быть описан с помощью регулярных грамматик (см. главу «Регулярные языки»). Распознавателями для регулярных языков являются конечные автоматы. Существуют правила, с помощью которых для любой регулярной грамматики может быть построен недетерминированный конечный автомат, распознающий цепочки языка, заданного этой грамматикой (см. раздел «Конечные автоматы», глава 10). Конечный автомат для каждой входной цепочки языка дает ответ на вопрос о том, принадлежит или нет цепочка языку, заданному автоматом.
Однако в общем случае задача сканера несколько шире, чем просто проверка цепочки символов лексемы на соответствие ее входному языку. Кроме этого, сканер должен выполнить следующие действия:
четко определить границы лексемы, которые в исходном тексте явно не заданы;
выполнить действия для сохранения информации об обнаруженной лексеме (или выдать сообщение об ошибке, если лексема неверна).
Определение границ лексем
Выделение границ лексем представляет определенную проблему. Ведь во входном тексте программы лексемы нс ограничены никакими специальными символами. Если говорить в терминах программы-сканера, то определение границ лексем — это выделение тех строк в общем потоке входных символов, для которых надо выполнять распознавание. В общем случае эта задача может быть сложной, и тогда гребустся параллельная работа сканера (лексического анал и затора), синтаксического разбора и, возможно, семантического анализа. Для большинства входных языков границы лексем распознаются по заданным терминальным символам. Эти символы — пробелы, знаки операций, символы комментариев, а также разделители (запятые, точки с запятой и т. п.). Набор таких терминальных символов может варьироваться в зависимости от синтаксиса входного языка.
Современная реализация Borland C++ Builder ориентирована па разработку результирующих программ, выполняющихся под управлением ОС Microsoft Windows всех типов. Сама система программирования Borland C++ Builder, как н Borland Delphi, также функционирует под управлением ОС тина Microsoft Windows. Он полностью поддерживает стандарт языка С, что делает возможным создание с помощью дайной системы программпрш алия модулей и библиотек, используемых в других средствах разработки (чего очень сложно достигнуть с помощью Borland Delphi).
По возможностям, внешнему виду и технологиям система программирования Borland C++ Builder схожа с системой программирования Borland Delphi. В ее основу положены те же основные идеи и технологии. Структура классов языка C++ в системе программирования Borland C++ Builder построена в той же библиотеке VCL (visual control library), в которой утроится структура классов Object Pascal в системе программирования Borland Delphi. Правда, разработчик, создающий программы па C++, может пе пользоваться классами VCL и взять за основу любую другую библиотеку, чего нельзя сказать о разработчике, использующем Object Pascal — набор доступных библиотек для последнего языка сильно ограничен.
Успешное распространение систем программирования Turbo Pascal и Boi land Delphi способствовало и виедрепею па рынок системы программирования Borland C++ Builder от той же компании-разработчика. Эта система программирования занимает прочную позицию на рынке средств разработки для языка C++, где существует довольно жесткая конкуренция. Более подробную информацию по данной системе программирования можно найти в технической литературе ([80]), либо на сайте компании-производителя [98].
Системы программирования фирмы Microsoft
Компания Microsoft в настоящее время как производитель операционных систем и программного обеспечения доминирует на рынке персональных компьютеров, построенных на базе процессоров гипа Intel 80x86. Прежде всего, это относится ко всем вариантам ОС гипа Microsoft Windows.
Этот факт явился одним из главных факторов, которые обусловили прочную позицию данной компании на рынке средств разработки программных продуктов для ОС типа Microsoft Windows. Все виды ОС типа Microsoft Windows создавались как закрытые системы. Поэтому безусловное знание компанией-разработчиком структуры и внутреннего устройства «своей» ОС зачастую являлось определяющим в ситуации, когда надо было создать средство разработки приложений для данной ОС. Хорошие финансовые ресурсы и положение компании на рынке позволили ей создать довольно удачные системы программирования, несмотря на то, что она начала их разработку довольно поздно и не являлась «законодателем мод» в данной области.
Microsoft Visual Basic
Это средство разработки прошло долгую историю под руководством компании Microsoft. История языка Basic на персональных компьютерах началась с прими-
тивных интерпретаторов данного языка Сам по себе язык Basic позволял легко организоват ь интерпретацию исходного кода программ, а его синтаксис и семантика достаточно просты для понимания даже непрофессиональными разработчиками.
Система программирования Microsoft Visual Basic также первоначально была ориентирована на интерпретацию исходного кода. Однако требования и условия на рынке средств разработки толкнули компанию-производителя па создание компилятора вошедшего в состав данной системы программирования При этом основные функции библиотеки языка были вынесены в отдельную, динамически подключаемую библиотеку VBRun, которая должна присутствовать в ОС для выполнения результирующих программ, созданных с помощью данной системы программирования. Различные версии системы программирования Microsoft Visual Basic ориентированы на различные версии данной библиотеки. Интерпретатор языка был сохранен и внедрен компанией разработчиком в состав модулей другого программного продукта — Microsoft Office.
Развитие системы программирования Visual Basic потребовало существенного изменения синтаксиса и семантики самого языка. С точки зрения автора, это решение нельзя признать технически удачным, так как изначально простой и достаточно примитивный язык исходного текста (не ориентированный на создание обьектно-ориентированных программ) был изменен так, чтобы иметь возможность решать несвойственные ему задачи. Однако этот ход компании был продиктован скорее маркетинговой политикой, чем техническими требованиями. При всем множестве привнесенных в язык новшеств компании удалось сохранить присущую ему простоту и наглядность всей системы программирования в целом.
Последняя версия данной системы программирования — Microsoft Visual Basic 6.0 — является одним из аффективных средств для создания результирующих программ, ориентированных на выполнение под управлением ОС типа Microsoft Windows. Эта система программирования ориентирована на технологию разработки RAD. Microsoft Visual Basic 6.0 содержит интегрированные средства визуальной работы с базами данных, поддерживающие проектирование и доступ к базам данных SQL Server, Oracle и т. п. К этим средствам относятся Visual Database Tools, ADO/OLE DB, Data Environment Designer, Report Designer и ряд других.
В данной системе программирования поддерживается также создание серверных Web-приложений, работающих с любым средством просмотра на базе новых Web- классов. В новой версии обеспечивается и отладка приложений для сервера 1 IS (Internet information server) производства компании Microsoft. В Microsoft Visual Basic 6.0 возможно создание интерактивных Web-страниц.
Microsoft. Visual Basic 6.0 обеспечивает простое создание приложений, ориентированных на данные. Visual Basic 6.0 позволяет создавать результирующие программы, выполняемые в архитектуре «клиент—сервер», которые могут работать с любыми базами данных. Система программирования Microsoft Visual Basic ориентирована, прежде всего, на создание клиентской части приложений.
Теперь Visual Basic 6.0 поддерживает универсальный интерфейс доступа к данным Microsoft при помощи технологии ADO. Visual Basic 6.0 обеспечивает про-
Различные формы внутреннего представления программы рассмотрены в разделе «Генерация кода. Методы генерации кода», глава 14. Здесь пойдет речь только о связных списочных структурах, представляющих деревья, так как именно эти структуры наиболее просто и эффективно строить па этапе синтаксического разбора, руководствуясь правилами и типом вывода, порожденного синтаксическим распознавателем.
В синтаксическом дереве внутренние узлы (вершины) соответствуют операциям, а листья представляют собой операнды. Как правило, листья синтаксического дерева связаны с записями в таблице идентификаторов. Структура синтаксического дерева отражает синтаксис языка программирования, па котором написана исходная программа.
Син гаксическис деревья могут быть построены компилятором для любой части входной программы. Не всегда синтаксическому дереву должен соответствовать фрагмент кода результирующей программы — например, возможно построение синтаксических деревьев для декларативной части языка. В этом случае операции, имеющиеся в дерене, не требуют порождения объектного кода, по несут информацию о действиях, которые должен выполнить сам компилятор над соответствующими элементами. 13 том случае, когда синтаксическому дереву соответствует некоторая последовательность операции, влекущая порождение фрнг- менга объектного кода, говорят о дереве операций.
Дерево операций можно непосредственно построить из дерева вывода, порожденного синтаксическим анализатором. Для этого достаточно исключить из дерева вывода цепочки не герминальных символов, а также узлы, не несущие семантической (смысловой) нагрузки при генерации кода. Примером таких узлов могут служить различные скобки, которые меняют порядок выполнения операций и операторов, ио после построения дерева никакой смысловой нагрузки не несут, так как им не соответствует никакой объектный код.
То, какой узел в дереве является операцией, а какой — операндом, никак невозможно определить из грамматики, описывающей синтаксис входного языка. Также ниоткуда не следует, каким операциям должен соответствовать объектный код в результирующей программе, а каким — нет. Все это определяется только исходя из семантики — «смысла» — языка входной программы. Поэтому только разработчик компилятора может четко определить, как при построении дерева операций должны различаться операнды и сами операции, а также то, какие операции являются семантически незначащими для порождения объектного кода.
Алгоритм преобразования дерева семантического разбора в дерево операций можно представить следующим образом.
Шаг 1. Если в дереве больше не содержится узлов, помеченных нетерминальными символами, то выполнение алгоритма завершено; иначе — перейти к шагу 2. Шаг 2, Выбрать крайний левый узел дерева, помеченный нетерминальным символом грамматики и сделать его текущим. Перейти к шагу 3.
Шаг 3. Если текущий узел имеет только одни нижележащий узел, то текущий узел необходимо удалить из дерева, а связанный с ним узел присоединить к узлу вышележащего уровня (исключить из дерева цепочку) п вернуться к шагу 1; иначе перейти к шагу 4.
смотр таблиц, изменение данных, создание запросов SQL из среды разработки для любой совместимой с ODBC или OLE DB базы данных. Так же как и в редакторе Visual Basic, синтаксис SQL выделяется цветом и незамедлительно проверяется на наличие ошибок Это делает код SQL легче читаемым и менее подверженным случайным ошибкам.
Новая версия продукта поддерживает коллективную разработку, масштабируемость, создание компонентов промежуточного слоя, пригодных к многократному использованию в любом СОМ-совместимом продукте. Поддержка широкого спектра интерфейсов доступа к данным дает возможность применять эту систему программирования для разработки клиентской части приложений, выполняющихся в трехуровневой архитектуре.
Среда разработки обладает множеством новых возможностей, таких как выделение синтаксиса и автоматическое завершение ключевых слов. Система программирования Microsoft Visual Basic интегрируется с семейством программных продуктов Microsoft BackOffice, которое обеспечивает среду для выполнения и создания сложных приложений масштаба предприятия для работы в локальных сетях или в Интернете. Использование новых интегрированных визуальных средств работы с данными облегчает выполнение рутинных задач но обеспечению доступа к ним; эти средства доступны прямо из среды разработки Visual Basic.
Система программирования Visual Basic неплохо сочетает в себе простоту и эффективность разработки. Все недостатки, присущие данной системе, в большинстве своем проистекают из недостатков используемого исходного языка программирования. Средства языка Basic даже после значительной модификации ограничивают возможности его применения в современных архитектурах взаимодействия приложений, которые в значительной мере основаны на объектно- ориентированном подходе. Кроме того, язык программирования в системе Visual Basic не является признанным стандартом, а потому возникают трудности по использованию созданных па его основе модулей и компонентов в других средствах разработки.
Информацию о системе программирования Visual Basic можно найти в многочисленной технической литературе (например, [3, 63]), либо же па сайте представительства компании [105].
Microsoft Visual C++
Система программирования Microsoft Visual C++ представляет собой реализацию среды разработки для распространенного языка системного программирования C++, выполненную компанией Microsoft. Эта система программирования в настоящее время построена в виде интегрированной среды разработки, включающей в себя все необходимые средства для разработки результирующих программ, ориентированных на выполнение под управлением ОС типа Microsoft Windows различных версий.
Основу системы программирования Microsoft Visual C++ составляет библиотека классов MFC (Microsoft foundation classes). В этой библиотеке реализованы в виде классов C++ все основные органы управления и интерфейса ОС. Также
и ее состав входят классы, обеспечивающие разработку приложений для архитектуры «клиент—сервер» и трехуровневой архитектуры (в современных версиях библиотеки). Система программирования Microsoft Visual C++ позволяет разрабатывать любые приложения, выполняющиеся в среде ОС тина Microsoft Windows, в том числе серверные или клиентские результирующие программы, осуществляющие взаимодействие между собой по одной из указанных выше архитектур.
Классы библиотеки MFC ориентированы на использование технологий СОМ/ DCOM, а также построенной на их основе технологии ActiveX для организации взаимодействия между клиентской и серверной частью разрабатываемых приложений. На основе классов библиотеки пользователь может создавать свои собственные классы в языке C++, организовывать своп структуры данных.
В отличие о г систем программирования компании Borland, система программирования Microsoft Visual C++ ориентирована на использование стандартных средств хранения и обработки ресурсов интерфейса пользователя в ОС Windows. Это не удивительно, поскольку все версии ОС типа Windows разрабатываются самой компанией Microsoft. Microsoft Visual C++ обеспечивает все необходимые средства для создания профессиональных Windows-приложений. От версии к версии продукт становится проще в использовании, расширяются возможности применения, повышается производительность.
Система программирования Microsoft Visual C++ выдержала несколько реализаций. В процессе выхода новых версий системы программирования было выпущено и несколько версий библиотеки MFC, на которой основана данная система.
Сама по себе библиотека MFC является, по мнению автора, довольно удачной реализацией широкого набора классов языка C++, ориентированного на разработку результирующих программ, выполняющихся над управлением ОС тина Microsoft Windows. Это во многом обусловлено тем, что создатель библиотеки — компания Microsoft — одновременно является и создателем ОС тина Microsoft Windows, на которые ориыггирован объектный код библиотеки. Библиотека может быть подключена к результирующей программе с помощью обычного компоновщика либо использоваться как динамическая библиотека, подключаемая к программе во время ее выполнения. Библиотека MFC достаточно широко распространена. Ее возможно использовать не только в составе систем программирования производства компании Microsoft, но и в системах программирования других производителей.
В комплект поставки данной системы программирования входят также новый мастер для поэтапного создания приложений, интегрированный отладчик создаваемых приложений, профилировщик исходного кода, галерея классов и объектов, а также средство для создания дистрибутивов — InstallShield.
Visual C++ 6,0 полностью интегрируется с Visual Studio 6.0 и другими средствами разработки, входящими в состав данного пакета. Эта интеграция, в частности, обеспечивает мгновенный доступ к MSDN (Microsoft developer network) library, содержащей документацию, примеры кода, статьи и другую информацию для разработчиков.
Система программирования Visual C++ 6.0 широко известна. Информацию о ней можно найти в многочисленной технической литературе, а также на сайте пред- ставигельечва компании [104].
Концепция .NET
Концепция .NET (произносится как «dot net» — «дот нет») — это не система программирования, а новейшая технология, предложенная фирмой Microsoft с целью унификации процесса разработки программного обеспечения с помощью различных систем программирования. Концепция .NET разработана компанией совсем недавно, и в настоящее время (2000-2001 годы) происходит ее постепенное внедрение на рынок средств разработки.
Концепция Microsoft .NET — это целостный взгляд компании Microsoft па новую эпоху в развитии Интернета. В рамках этой концепции самые разнообразные программные приложения предоставляются пользователям и разработчикам как сервисы, которые взаимодействуют между собой в соответствии с копкрет ными потребноегямп бизнеса, доступны па самых резных устройствах, имеют понятный и полностью адаптирующийся к потребностям каждого пользователя ин №рфей
Концепция Microsoft .NET — закономерный этап в развитии информационных технологий, приложений и сервисов, ношоляющий разработчикам воспользоваться преимуществами сочетания открытых стандартов и архитектуры новой ОС произволегва компании Microsoft — Microsoft Windows 2000.
Большинство операционных систем, существовавших до настоящего времени, были неспособны предоставить необходимую корпоративным приложениям среду исполнения. Для них приходилось создавать специальные серы ры приложений. Новая ОС производства компании Microsoft — Microsoft Windows 2000 — непосредственно предоставляет многие функций сервера приложенш на уровне системных сервисов. Поэтому в концепции .NET управление приложениями концентрируется на более специфических для корпоративной среды и Интер- нет-служб задачах — управлении множеством объектов, приложений и серверов. Их решает сервер масштабирования приложений.
Концепция .NET содержит много новых идей и предложений, выдвинутых и реализуемых компанией Microsoft. С точки зрения систем программирования основные идеи архитектуры .NET заключаются в том, что в ОС типа Windows 2000 организуется специальная виртуальная машина, исполняющая (интерпретирующая) команды некоторого промежуточного низкоуровневого языка. Любая программа, исполняемая в .NET, предсгавляет собой набор команд данного промежуточного языка При этом сами команды этого языка интерпретируются независимо от архитектуры вычислительной системы и версии ОС, г де они исполняются.
Таким образом, любая система программирования, ориентировано 1я па данную концепцию, сможс! создавать код результирующей программы не в виде последовательности машинных команд, специфичных для архитектуры той или иной ОС, а в виде промежуточного низкоуровневого кода. Этот код будет интерпретироваться единообразно п независимо от архитектуры целевой вычислительной
системы. Кроме того, любая результирующая программа, исполняемая в .NET, сможет воспользоваться сервисными функциями другой программы, доступной в .NET, вне зависимости от того, с помощью какой системы программирования были разработаны обе эти программы. Это значительно расширяет возможности программ, исполняемых в рамках .ХЕТ, особенно с учетом возможности удаленного (сетевого) доступа ко многим предоставляемым сервисам.
В первую очередь на поддержку концепции .NET будут, безусловно, ориентированы системы программирования производства компании Microsoft. Это коснется рассмотренных выше систем программирования Microsoft Visual C++ и Microsoft Visual Basic. Кроме того, компания предлагает новую систему программирования, построенную на базе нового языка C# (произносится «Си-шарп»), которая специально ориентирована на помёржку концепции NET. При успешном развитии концепции и ее внедрении па рынок ОС и средств разработки на нее будут ориентироваться и системы программирования других разработчиков.
Поддержка концепции .NET в системах программирования не отменяет в них поддержку традиционного нуги со дай и я программного обеспечения. Возможность создания результирующих upoipiMM, исполняющихся под управлением .NET, идет в дополнение к возможности создания «традиционных» результирующих программ, построенных из машинных команд, ориентированных на определенную архитектуру целевой вычислительной системы. Однако далеко не все модули существующих в настоящий момент библиотек Moiyr быть использованы в рамках данной концепции. Это в равной степени касается и раснросгра ценных библиотек МЕС и VBRun, используемых в системах программирования от Microsoft.
Концепция .NET ориентирована исключительно на новые ОС типа Windows 2000 производства компании Microsoft (а также на все последующие версии данного типа ОС). Она нс переносима на ОС других типов, а также на архитектуру, не совместимую с традиционной архитектурой персональных компьютеров, построенных на базе процессоров Intel 80x86. Этот факт ограничивает ее широкое распространение, поскольку она поддерживается пока только одной компанией- производителем, хотя и доминирующей на рынке ОС для персональных комнь югеров.
Концепция .NET еще достаточно нова и не нашла пока широкого отражения в технической литературе. Со временем эта ситуация, конечно, изменится, а пока для более подробной информации рекомендуется обратиться на сайт представительства компании Mi :rosoft [106]
Системы программирования под ОС Linux и UNIX
Системы программирования в составе ОС типа UNIX
Вся история ОС UNIX тесно связана с историей языка программирования С. Фактически эти два программных продуста не смогли бы существовать друг без друга.
Язык программирования С появился как базовый язык программирования в ОС типа UNIX. Более того, сама операционная система представляла (и представляет) собой набор файлов, написанных на исходном языке программирования С. То есть практически любая ОС типа UNIX поставляется в виде исходного текста на языке С (кроме незначительной части ядра ОС, ориентированной на особенности архитектуры вычислительной системы, где она выполняется). Всякий раз при изменении основных параметров ОС происходит компиляция и компоновка ядра ОС заново, а при перезапуске ОС вновь созданное ядро активируется. Этот принцип характерен для всех ОС типа UNIX.
Таким образом, ни одна ОС типа UNIX фактически нс может существовать без наличия в ее составе компилятора и компоновщика для языка программирования С (поскольку речь идет о ядре ОС, то роль загрузчика несколько отличается от сто роли в обычной системе программирования). Соответственно, нее производители ОС типа UNIX включают в ее состав и системы программирования языка С. По указанным причинам этот язык программирования стал основным для этого типа ОС (хотя под ОС типа UNIX существуют системы программирования с других языков — например, Lisp, FORTRAN, Pascal, — они используются не так часто, как С).
В ОС типа UNIX сложились основные особенности и характеристики второго этана в развитии систем программирования. Именно здесь системы программирования стали представлять собой комплекс из библиотек языка, компилятора и компоновщика, выполняемых под управлением специальных командных файлов. Функции загрузчика в данном случае большей частью выполняются самой ОС В ОС тина UNIX сложился, стал применяться и был стандартизован командный язык компиляции Makefile. Кроме того, именно в ОС этого типа были разработаны упомянутые в данном учебном пособии методы автоматизации разработки компиляторов, основанные на использовании программ LEX и YACC [11, 31, 39, 67, 68, 90]. К тому же практически все ОС типа UNIX обладают мощными командными процессорами, выполняющими команды пользователя в ОС. Возможности этих командных процессоров, по сути, соответствуют возможностям интерпретаторов языков программирования.
Принцип, по которому распространяется и устанавливается любая ОС тапа LINIX, стал применяться и к прикладным программам, разрабатываемым для исполнения под управлением данного типа ОС. Обычно прикладная программа, ориентированная на выполнение под ОС тина UNIX, поставляется в виде набора исходных кодов (чаще всего па языке программирования С). При установке прикладной программы на конкретную ОС такого тина она автоматически компилируется и компонуется, после чего готова к выполнению. Такой метод распространения прикладных программ в виде исходных кодов значительно снижает их зависимость от архитектуры конкретной вычислительной системы* Однако
Распространение прикладных программ, разработанных для ОС тина UNIX, в виде исходного кода нс отменяет авторского нрава и права собственности на код программы. Пользователь программы, даже получив исходный код, нс имеет нрава изменять его и распространять без санкций разработчика. Совсем иное дело — распространение исходного кода программ в рамках проекта GNU.
он требует обязательного наличия соответствующей системы программирования в составе ОС (вот почему в качестве языка разработки прикладных программ под ОС типа UNIX чаще всего выступает именно С).
Системы программирования в ОС типа UNIX долгое время не выходи ш за рамки второго этапа развития систем программирования. Они продолжают оста ваться такими и по ceil день в той части, которая отвечает за распространение исходного кода самой ОС и прикладных профамм, разработанных для нее Так происходит, поскольку в этом Случае система программирования нс требует наличия интегрированной среды и вполне может быть ограничена командными файлами. Однако развитие графического интерфейса пользователя оказало свое влияние и па системы программирования под ОС данного типа.
Системы программирования, построенные на базе интегрированных сред разработки, стали появляться и под ОС типа UNIX. Характерная черта их заключалась в том, что практически все они строились именно в графической среде на основе стандартного графического интерфейса Так произошло потому что в ОС типа UNIX (в отличие от ОС, ориентированных на персональные компьютеры) практически сразу в качестве стандарта «де-факто» установился стандарт графического интерфейса пользователя на основе среды X Window [31, 67, 68] Это позволило унифицировать библиотеки систем программирования (все они строятся на базе библиотеки Xhb) компиляторы и компоновщики ресурсов пользовательского интерфейса. Широкие возможности командных процессоров в ОС типа UNIX облегчают построение интегрированной среды в системах npoi раммирова- пия. Поэтому сейчас существует широкий выбор таких сред от многих производи гелей (при наличии хотя бы небольшого опыта работы в ОС разработчик имеет возможность сам построить интегрированную среду программирования под управлением ОС).
Описания базовых систем программирования под ОС типа UNIX можно найти в любой технической литературе по этим ОС, которая имеется в настоящее время в достаточном количестве [11 31. 34, 67, 68].
Системы программирования проекта GNU
Проект GNU был начат в 1984 году. Само название «GNU» является рекурсивной аббревиатурой «GNU’s Not UNIX» — «GNU не UNIX». Манифест проекта GNU [102] был написан Ричардом Столлмепом (Ri hard Stallman) в начале работы над проектом GNU и адресован желающим помочь или принять участие в проекте. В течение нескольких первых лет работы над проектом он был незначительно обновлен, чтобы отразить ход работ.
Идея проекта GNU заключается в свободном распрцстранепни всех программ, созданных в рамках этого проекта. Программы распространяются в виде исходного кода, причем пользователь не только получает этот код в пользование, по и имеет право вносить в пего изменения, а также распространять исходны! пли модифицированный код всем желающим участникам проекта. Во многом своему существованию н развитию проект обязан наличию по всему миру большого количества профессионалов-энтузиастов, не возражающих против свободного распространения и использования созданных ими программ. Более точные форму-
лировки сути проекта и условий участия в нем можно найти на сайте |100] или |102|(врусском переводе)
GNU — это название полной UNIX-совместимой прогреммной системы, которая разрабатывается н безвозмездно предоставляется всем желающим се использовать в рамках проекта. Система GNU способна исполнять UNIX-программы, ио нс идентична ОС типа UNIX. Улучшения основаны па имеющемся опыте работы с другими операционными системами. В качестве языка системного программирования в системе доступны как С, так и Lisp. Кроме этого, поддерживаются коммуникационные протоколы UUCP, MIT Chaosnet и сети Интернет.
Безусловно, по самой своей сути проект предполагает наличие в своем составе систем программирования. В противном случае распространение исходных кодов программ в рамках проекта будет просто бессмысленным. Очевидно, базовой системой программирования в проекте GNU стала система программирования на языке С. Хотя GNU и не UNIX, но проект перенял многие положительные черты ОС этого типа, в том числе — создагрге и распространение ядра ОС на основе языка С. Другим фактором, обусловившим создание системы программирования языка С в рамках проекта GNU, стало то, что большинство профессионалов- нтузиасюв, основавших проект, использовали именно этот язык про граммированея.
Со временем система программирования GNU С преобразовалась в более развитую систему программирования GNU C++, поскольку именно язык C++ получил широкое распространение в среде профес ионалов. Затем в рамках проекта стали доступны и системы программирования на основе многих других языков программирования, в час гное гн известного языка Pascal.
По внешнему виду, интерфейсу и функциональным возможностям системы программирования созданные в рамках проекта GNU, мало чем отличаются от систем программирования, ориен тированных на ОС типа UNIX. Главным отличием являются условия, определяющие правила распространения и использования данных систем программирования. Особенности проекта обусловили и тот факт, что большинство созданных в нем систем программирования не ориенгировано па игнорированные среды разработки, а функционируют в рамках командных процессоров И хотя возможности ОС, созданной в проекте GNU, позволяют разработчику, использующему одну из систем программирования, самому создать интегрированную среду разработки (при наличии у него соответствующих профессиональных навыков), эта особенность не является положительной чертой проекта.
Поэтому в последнее время и в рамках проекта GNU стали появляться системы программирования, построенные на основе интегрированной среды разработки. Кроме того, известные компании производители средств, разработки стали обращать внимание на данный проект по причине его широкого распространения Они стали предлагать свои системы программирования, ориентированные на выполнение в ОС, созданной в рамках проекта GNU. Об одной из такой систем программирования (Borland Kylix) несколько слов сказано ниже
Операционная система созданная и поддерживаемая сообществом ра гработчи- ков в рамках проекта GNU, получила название Linux. В ходе работы над Проще-
том было создано большое количество программного обеспечения, в том числе и систем программирования (кроме указанной уже системы GNU C++). Даже краткий обзор их возможностей здесь не представляется возможным Информацию о них можно найти в любой технической литературе, посвященной ОС Linux и проекту GNU, либо в сети Интернет [67, 68, 100).
Положительной чертой проекта можно считать так же гот факт, что многие системы программирования, созданные в рамках проекта, оказались совместимыми и с ОС типа UNIX, которые формально не входят в проект GNU. Та же система программирования GNU C++, созданная в рамках проекта GNU, может с успехом использоваться (и используется) под многими версиями ОС типа UNIX.
Проект Borland Kylix
Проект Borland Kylix представляет собой попытку компании Borland перенести на ОС типа Linux опыт создания систем программирования, который компания накопила за долгое время работы в этой сфере для ОС типа Microsoft Windows.
В рамках проекта Borland Kylix компания Borland создала и распространяет на рынке программного обеспечения одноименную систему программирования, основанную на языке программирования Object Pascal. Данный язык известен в среде разработчиков для системы программирования Borland Delphi, орнеп гированной на ОС типа Windows. Компания Borland реализовывает проект Borland Kylix таким образом, чтобы перенести в созданную систему программирования под ОС Linux все черты, присущие системе программирования Borland Delphi, уже существующей под ОС типа Microsoft Windows (эта система программирования была рассмотрена выше в данной главе).
Основой проекта Borland Kylix стала библиотека компонентов CLX (произносится «клик»), в которой компания Borland реализовала все основные органы управления пользовательского интерфейса и средства обработки данных, ориентированные на ОС Linux. Библиотека построена в виде компонентов на основе иерархии классов языка Object Pascal. Надо сказать, что данная библиотека отлична от библиотеки VCL, которая в настоящее время используется в системе программирования Borland Delphi и построена на основе того же языка. Перенести классы библиотеки VCL в новый проект системы программирования иод ОС Linux оказалось для компании Borland принципиально невозможным.
Следующим шагом в проекте Borland Kylix компания предполагает создание в его рамках системы программирования на основе языка C++. Эта система программирования должна быть построена на основе той же библиотеки CLX, что и система программирования на основе Object Pascal (примерно так же, как Borland Delphi и Borland C++ Builder построены на основе одной и той же библиотеки VCL).
Дальнейшим развитием -проекта Borland Kylix компания Borland видит внедрение библиотеки CLX в существующие системы программирования Borland Delphi и Borland C++ Builder, ориентированные на ОС типа Windows. В случае успешного развития проекта использование одной из указанных систем программирования компании Borland существенно облегчит перенос приложений между двумя принципиально различными типами ОС. В рамках этого проекта разра-
ботчик, пользуясь только средствами библиотеки CLX сможет создавать исходный код па языке Object Pascal или C++ таким образом, что результирующие про раммы, построенные на основе этого кода с помощью Borland Kylix, будут выполняться в ОС типа Linux а построенные с помощью Borland Delphi или Borland C++ Builder — в ОС типа Microsoft Windows. В случае успеха проект Borland Kylix может быть распространен и на различные ОС типа UNIX.
Этот проект был выбран в качестве примера потому, что системы программирования производства компании Borland достаточно широко распространены на рынке России. Внимание, уделяемое компанией Borland ОС типа Linux, говорит о широком распространении проекта GNU в среде разработчиков.
Проект Borland Kylix только начал развиваться. Он еще не нашел широкого отражения в технической литературе, поэтому подробности лучше получить на сайте компании [98].
Разработка программного обеспечения для сети Интернет
Интернет — это всемирная «сеть сетей». Он объединяет в себя вычислительные системы и локальные сети, построенные на базе различных аппаратно-программных архитектур. При осуществлении взаимодействия по сети двух компьютеров один из них выступает в качестве источника данных (Интернет-сервсра), а другой — приемника данных (Интернет-клиента). Сервер подготавливает данные, а клиент принимает их и каким-то образом обрабатывает. Нередко в качестве данных выступают тексты программ, которые подготавливает сервер, а исполнять должен клиент.
В таких условиях определяющим становится требование унифицированного исполнения кода программы вне зависимости от архитектуры вычислительной системы Компиляция и создание обьектиого кода в условиях всемирной сети становятся бессмысленными, потому что заранее не известно, на какой вычислительной системе потребуется исполнять полученный в результате компиляции код. По этой причине сервер не может передавать но сети объектный код или команды ассемблера — вполне может так случиться, что клиент просто не способен их исполнить. Можно попытаться создать сервер таким образом, чтобы он мог подготавливать программы для всех известных типов клиентов. Однако это, во- первых, значительно увеличит нагрузку па сервер, а во-вторых, не гарантируел от силуации, когда связь с сервером установит клиент нового, еще не известною серверу типа (поскольку количество возможных архитектур компьютеров непостоянно и ничем в принципе не ограничено)
Поэтому основной особенностью программирования в сети Интернет является использование в качестве основного средства программирования интерпретируемых языков. При интерпретации исполняется не объектный код, а сам исходный код программы, и уже непосредственно интерпретатор па стороне клиента отвечает- за то, чтобы этот исходный код бы л исполнен всегда одним и тем же образом вне зависимости от архитектуры вычислительной системы. Тогда сервер готовит код программы всегда одним и тем же образом вне зависимости от типа
клиента
В такой ситуации нагрузка на Интернет-клиента может возрасти. По задачу можно несколько упростить: Интернет-сервер может готовить нс высокоуровневый код исходной программы, а некий унифицированный промежуточный код низкого уровня, предназначенный для исполнения на стороне клиента Тогда в обмене данными участвуют еще две дополнительные программы: компилятор (точнее — транслятор) на стороне сервера, транслирующий исходный код программы на некотором языке высокого уровня в промежуточным низкоуровневый код; и интерпретатор на стороне клиента, отвечающий за исполнение промежуточного кода вне зависимости от архитектуры вычислительной системы клиента.
Во многих случаях при использовании исполнения программ в глобальной сети применяется именно такая схема.
Для реализации такого рода схем существует много технических и языковых средств. Существует также значительное количество языков программирования, ориентированных на использование в глобальной сети [10, 16, 91]. Иптерпет- программировапие — это отдельная и довольно интересная область разработки программ, но в целом вопросы, затрагиваемые в ней, лежат за пределами данного учебного пособия. Далее дается только очень краткий обзор принципов, положенных в основу тех или иных языков программирования с глобальной сети.
Язык HTML. Программирование статических Web-страниц
Язык HTML (hypertext markup language, язык разметки гипертекста) во многом определил развитие и широкое распространение сети Интернет по всему миру. Сам по себе язык достаточно прост, а для овладения им нужны только самые примитивные знания в области программирования.
Язык позволяет описывать структурированный текст (гипертекст), содержащий ссылки и взаимосвязи фрагментов; графические элементы (изображения), которые могут бы ть (вязаны как с текстовой информацией, так и между собой; а также простейшие элементы графического интерфейса пользователя (кнопки, списки, поля редактирования). На основе описания, построенного в текстовом виде на HTML, эти элементы могут располагаться на экране, им могут присваиваться различные атрибуты, определяющие используемые ресурсы интерфейса пользователя (такие, как цвет, шрифты, размер и т. п.). В результате получается графический образ - Web-страница (от «web» — «наугина» — слова, входящего в состав аббревиатуры WWW — World Wide Web — Всемирная паутина). Опа в принципе может содержать различные мультимедийные элементы, включая графику, видео и анимацию.
Широкому распространению HTML послужил принцип, па основе которого этот язык стал использоваться в глобальной сети. Суть его достаточно проста: Интернет-сервер создает текст на языке HTML и передает его в виде текстового файла на клиентскую сторону сети по специальному протоколу обмена данными HTTP (hypertext transfer protocol, протокол передачи гипертекста). Клиент, получая исходный текст на языке HTML, интерпретирует его и в соответствии с результатом интерпретации строит соответствующие интерфейсные формы и изображения па экране клиентского компьютера.
Таблица 13.1. Лексемы программы
Лексема
Тип лексемы
Значение
begin
Ключевое слово
X!
for
Ключевое слово
Х2
i
Идет ификатор
i: 1
:-
Знак присваивания
:“
1
Целочисленная константа
to
Ключевое слово
хз
N
Идентификатор
N :2
do
К печеное слово
Х4
fg
Идентификатор
fg:3
:-
Знак присваивания
:■
fg
Идентификатор
fg:3
*
Знак арифметической операции
*
0.5
Вещественная константа
0.5
Поле «Значение» в габл. 13.1 подразумевает некое кодовое значение, которое будет помещено в итоговую таблицу лексем в результате работы лексического анализатора. Конечно, значения, которые записаны в примере, являются условными. Конкретные коды определяются при реализации компилятора. Важно отметить также, что для идентификаторов устанавливается связка таблицы лексем с таблицей идентификаторов (в примере это отражено некоторым индексом, следующим после идентификатора за знаком :, а в реальном компиляторе все опять же определяется его реализацией).
Шаг 4. Если текущий узел имеет нижележащий узел (лист дерева), помеченный терминальным символом, который не несет семантической нагрузки, тогда этот лист нужно удалить из дерева и вернуться к шагу 3; иначе — перейти к шагу 5.
Шаг 5. Если текущий узел имеет один нижележащий узел (лист дерева), помеченный терминальным символом, обозначающим знак операции, а остальные узлы помечены как операнды, то лист, помеченный знаком операции, пади удалить из дерева, текущий узел пометить этим знаком операции и перейти к шагу 1; иначе — nepei ти к шагу 6
Шаг 6. Если среди нижележащих узлов для текущего узла есть узлы, помеченные нетерминальными символами грамматики, то необходимо выбрать крайний левый среди этих узлов, сделать его текущим узлом и перейти к шагу 3: иначе — выполнение алгоритм" Завершено.
Этот алгоритм всегда работает с узлом дерева, который считает текущим, и стремится исключить из дерева все узлы, помеченные нетерминальными символами. То, какие из символов считать семантически незначащими, а какие считать знаками операций, решает разработчик компилятора*. Если семантика языка задана корректно, то в результате работы алгоритма из дерева будут исключены все нетерминальные символы.
Возьмем в качестве примеров синтаксические деревья, построенные для цепочки (а+а)*Ь из языка, заданного различными вариантами грамматики арифметических выражений. Эти деревья приведены выше в качестве примеров на рис. 12.3, 12.7 и 12.9. Семантически незначащими символами в этой грамматике являются скобки (они задают порядок операций и влияют на синтаксический разбор, ио результирующего кода не порождают) и пустые строки. Знаки операций заданы символами +, -, / и *, остальные символы (а и Ь) являются операндами.
В результате применения алгоритма преобразования, деревьев синтаксического разбора в дерево операций к деревьям, представленным па рис. 12.3, 12.7 и 12.9, получим дерево операций, представленное на рис. 13.9. Причем, несмотря на то что исходные синтаксические деревья имели различную структуру, зависящую от используемой грамматики, результирующее дерево операций в результате всегда имеет одну и ту же структуру, зависящую только от семантики входного языка.
Рис. 13.9. Пример дерева операций для языка арифметических выражений
Дерево операций является формой внутреннего представления программы, ко- горой удобно пользоваться на этапах синтаксического разбора, семантического анализа и подготовки к генерации кода, когда еще нет необходимости работать
1 Повтор того, что сказано до начала Шагов. Примвч. рей.
Грамматика HTML проста (она относится к регулярным грамматикам), а потому не составляет сложности построить соответствующий интерпретатор. Такими интерпрегаторами явились программы-навигаторы в сети Интернет (браузеры, browser), которые, по сути, минимально должны были содержать две составляющих: клиентскую часть для обмена данными по протоколу HTTP и интерпретатор языка HTML. Некоторое время на рынке существовало огромное количество такого рода программ, в настоящее время преобладают две — Internet Explorer (производства компании Microsoft) и Netscape Navigator (производства компании Netscape). Первый из них доминирует на архитектуре персональных компьютеров па базе процессоров типа Intel 80x86 под управлением ОС типа Microsoft Windows.
Гораздо разнообразнее программное обеспечение серверной части. Это вызвано тем, что в протоколе HTTP нигде строго не специфицирован источник HTML- текста. Им может быть обычный текстовый файл, и тогда клиент будет видеть у себя статическую картинку всякий раз, когда устанавливает соединение с данным сервером. Но может быть и так, что сервер будет порождать новый HTML- текст всякий раз, когда клиент устанавливает с ним соединение, или даже менять текст по мере работы клиента с сервером. Тогда и изображение на стороне клиента, зависящее от интерпретируемого текста HTML, будет динамически изменяться по мере изменения текста. Последний вариант представляет гораадо больший интерес с точки зрения предоставляемых возможностей. Вопрос только в том, как организовать динамическое изменение HTML-текста. Вот в этом направлении и шло развитие основных средств Интернет-программирования.
Описание языка HTML можно найти на многих сайтах в глобальной сети, а также в многочисленной литературе по Интерпет-программиронапию (например, в |16, 91]).
Язык HTML прост и тем удобен. Однако отсюда проистекают и основные его недостатки. Во-первых, он не предоставляет средств динамического изменения содержимого интерфейсных форм и изображений, поэтому основной метод — динамическое изменение самого текста HTML. Во-вторых, данный язык не предоставляет никаких методов поддержки современных архитектур тина «клиент- сервер» или трехуровневой архитектуры. Он не позволяет обмениваться данными ни с серверами БД, ни с серверами приложений как на стороне сервера, где готовятся тексты HTML, так и на стороне клиента, где эти тексты интерпретируются. Наконец, этот язык имеет очень ограниченные средства для реакции на действия пользователя в интерфейсных формах, созданных с сто помощью.
Для устранения этих недостатков были предложены различные средства. Некоторые из них рассмотрены ниже.
Программирование динамических Web-страниц
Основная идея динамической генерации Web-страниц заключается в том, что большая часть HTML-страши tw не хранится в файле на сервере, а порождается непосредственно каждый раз при обращении клиента к серверу. Тогда сервер формирует страницу и сразу же по готовности передает се клиенту. Таким образом, всякий раз при новом обращении клиент получает новый текст HTML, и нс ис-
ключено, что тексты могут значительно различаться между собой даже при обращении к одному и тому же серверу.
Вопрос только в том, как обеспечить динамическую генерацию HTML-кода на стороне сервера.
Самое очевидное решение заключается в том, чтобы разработать некоторый исполняемый файл (приложение), который будет динамически строить новый HTML-код. Тогда на вход такого исполняемого файла поступают некоторые параметры (например, данные, которые пользователь ввел в форме или командной строке), а па выходе он должен порождать HTML-код в виде текста. Для этой цели служат специальные приложения, называемые CGI-приложениями.
CGI (common gateway interface, общедоступный шлюзовой интерфейс) — это интерфейс для запуска внешних программ на сервере в ответ на действия клиента, установившего соединение с ним через глобальную сеть. Пользуясь этим интерфейсом, приложения могут получать информацию от удаленного пользователя, анализировать се, формировать HTML-код и отсылать его клиенту. CGI-приложения могут получать данные из заполненной формы, построенной с помощью HTML, либо из командной строки описания URL (universal resource locator, универсальный указатель ресурса). Строка URL вводится в программе-навигаторе, осуществляющей доступ к серверу со стороны клиента через глобальную сеть но протоколу HTTP. То, какие CGI-приложения по каким действиям пользователя должны выполняться па сервере, указывается непосредственно в коде HTML- страницы, которую сервер передаст клиенту.
Кроме интерфейса CGI существуют и другие варианты интерфейсов, позволяющие динамически создавать HTML-код путем запуска на сервере приложений в ответ на действия клиента. Например, можно выделить интерфейс ISAPI (Internet server application programming interface, интерфейс прикладных программ Интернет-ссрвера). Отличие ISAPI от CGI заключается в том, что для поддержки CGI «задаются отдельные приложения, выполняющиеся в виде самостоятельных программ, a ISAPI поддерживается с помощью библиотек, динамически подключаемых к серверу. 1SAPI библиотеки исполняются непосредственно в адресном пространстве сервера, имеют большие возможности и обеспечивают более высокую производительность сервера, в то время как CGI-приложения исполняются в ОС сервера как отдельные процессы и вынуждены определенным образом организовывать обмен данными с самим сервером (что снижает производительность). Но, с другой стороны, ошибка в библиотеке ISAPI может привести к выходу всего сервера из строя и его длительной неработоспособности. В то же время ошибка в коде CG1-приложения может, в худшем случае, привести только к аварийному завершению выполнения этого приложения, а сам сервер при этом сохранит работоспособность. Тогда в результате ошибки будет неверно отображена только какая-то одна HTML-страница либо часть страницы, а все остальные час ти сервера будут продолжать исправно работать.
Современные системы программирования, в том числе и те из них, что были рассмотрены в качестве примеров в данном пособии, позволяют создавать приложения и библиотеки, рассчитанные па работу в i лобальной сети в соответствии со стандартами CGI или ISAPI При этом создание исходного кода приложения практически ничем не отличается от создания обычной исполняемой програм-
мы, компилятор по-прежнему порождает объектные файлы, но компоновщик собирает исполняемый файл пли библиотеку с учетом того, что они будут исполняться в архитектуре серверу глобальной сети. Функции загрузчика выполняет ОС по команде сервера либо сам Интернет-сервер.
Этот метод удобен, но имеет один серьезный недостаток: при изменении содержимого динамической HTML-страницы или же при изменении логики ее реакции на действия Интернет-клиента требуется создать новые код CGI или ISAP1- прнложения. А для этого нужно выполнить полностью весь цикл построения результирующей программы, начиная от изменения исходного кода, включая компиляцию и компоновку. Поскольку содержимое Web-страниц меняется довольно часто (в отличие от обычных программ), то такой подход нельзя признать очень эффективным. Кроме того, может потребоваться перенос Интерпет-ссрве- ра с одной архитектуры вычислительной системы на другую, а это также потребует перестройки всех используемых CGI-приложений и ISAPI-библиотек.
Лучших результатов можно добиться, если не выполнять на сервере уже скомпилированный и готовый объектный код, а интерпретировать код программы, написанной на некотором языке. При интерпретации исходного кода сервер, конечно, будет иметь производительность ниже, чем при исполнении готового объектного кода, по чаще всего этим можно пренебречь, поскольку производительность серверов Интернета ограничивает чаще всего не мощность вычислительной системы, а пропускная способность канала обмена данными. Тогда зависимость кода сервера от архитектуры вычислительной системы будет минимальной, а изменить содержимое порождаемой НТМЕ-страшщы можно будет сразу же, как только будет изменен порождающий ее исходный код (без дополнительной перекомпиляции).
Существует несколько языков и соответствующих им интерпретаторов, которые нашли применение в этой области и успешно служат цели порождения HTML- страниц. Среди них можно назвать язык Perl, лежащий в основе различных версий Web-техноло! ни PHP (Personal home pages), и язык сценариев, на котором основана Web-технология ASP (Active server pages), — последний предложен и поддерживается известным производителем иртраммиого обеспечения — фирмой Microsoft. Компонент, интерпретирующий ASP, появился в составе версии IIS (Internet information server) 3.0 В результате разработчики получили более простые в применении и вместе с тем более мощные средства создания Web- приложений.
Текст на интерпретируемых языках, которые поддерживаются такими Web-технологиями, как ASP или PHP, представляет собой часть текста обычных HTML- страниц со встроенными в них сценариями (script). Эти сценарии можно писать па любом языке, поддерживаемом сервером; Интернет-сервер обрабатывает их при поступлении запроса о URL-адресе соответствующего файла. Он разбирает текст HTML страницы, находит в нем тексты сценариев, вырезает их и интерпретирует в соответствии с синтаксисом и семантикой данного языка. В результате интерпретации получается выходной текст на языке HTML, ко торый сервер вставляет непосредственно в то место исходной страницы, где встретился сценарий. Так обрабатывается динамическая Web-страница на любом питерпрети- руемом языке, ориентированном на работу в глобальной сети. Естественно, для
работы со страницей сервер должен иметь в споем составе интерпретатор соответствующего языка.
Все эти языки сценариев обладают присущими им характерными особенностями. Во-первых, они имеют мощные встроенные функции и средства для работы со строками, поскольку основной задачей программ, написанных с помощью таких языков, является обработка входных параметров (строковых) и порождение HTML-кода (который также является текстом). Во-вторых, все они имеют средства для работы в архитектуре «клиент сервер» для обмена информацией с серверами БД, а многие современные версии таких языков (например, язык, под- держиваемый Wcb-тсхнологией ASP) — средства для функционирования в трехуровневой архитектуре для обмена данными с серверами приложений.
Технологии интерпретируемых языков сценариев, отделившие процесс написания кода HTML от процесса создания прикладных компонентов, способствовали более »([х|>ективному сотрудничеству между дизайнерами Web-страниц и разработчиками программ. Благодаря тому, что динамические данные и приложения генерируются с помощью интерпретируемых языков непосредственно на Интер- нет-серверс, информация остается там, где ею легче всего управлять.
Недостаток всех перечисленных методов заключается в том, что сначала сервер вынужден строить HTML-оппсапие, собирая его некоторым образом из каких-то своих данных, а затем Интернет-клиент на своей стороне разбирает (интерпретирует) полученное описание HTML-страницы. Таким образом, выполняется как бы двойная работа по генерации, а затем интерпретации текстов языка HTML. Кроме того, между клиентом и сервером по сети передаются довольно громоздкие описания HTML-страниц, что может значительно увеличивать трафик сети.
Тем не менее, несмотря на недостатки, данные методы довольно распространены в глобальной сеги, поскольку они очень просты, а кроме того, не требуют от клиентского компьютера ничего, кроме способности интерпретировать тексты HTML Эта особенность довольно существенна.
Есть и другие варианты генерации динамических HTML-страниц. Рассмотренные далее методы, хотя и снижают нагрузку на сеть, но предъявляют определенные требования к клиентской части, что не всегда приемлемо.
Информацию по перечне ченным выше языкам, поддерживающим такие методы, можно найти в соответствующей технической литературе (опа частично имеется в [10, 91 [), по лучше обратиться па соответствующие сайты во Всемирной сети [103, 107, 108]. ‘
Языки программирования Java и Java Script
Язык Java был разработан компанией Sun в качестве средства Web-программи- рования Этот язык, в отличие от языка описания гипертекста HTML, является полноценным языком программирования. Он содержит в себе все основные операторы, конструкции и структуры данных, присущие языкам программирования. Синтаксические конструкции и семантика языка Java большей частью были заимствованы из языков программирования С и C++.
Основная идея, отличающая язык Java от многих других языков программирования, не орпептированных на применение в глобальной сети, заключается в том,
что Java не является полностью компилируемым языком. Исходная программа, созданная на языке Java, нс преобразуется в машинные коды. Компилятор языка порождает некую промежуточную результирующую программу на специальном низкоуровневом двоичном коде (эта результирующая программа называется Java- апплетом, а код, на котором опа строится, — Java байт-кодом). Именно этот код интерпретируется при исполнении результирующей Java-нрограммы. Такой метод исполнения кода, основанный на интерпретации, делает сто практически независимым от архитектуры целевой вычислительной системы.
Таким образом, для исполнения Java-программы необходимы две составляющие: компилятор промежуточного двоичного кода, порождающий его из исходного текста Java-нрограммы, и интерпретатор, Исполняющий этот промежуточный двоичный код. Такой интерпретатор получил название виртуальной Java- маишиы. Описание основных особенностей реализации виртуальной Java-мапш- ны (шггернретатора промежуточного низкоуровневого кода) с точки зрения системы программирования описаны в [74, 91].
Одной из отличительных особенностей данного языка является использование специального механизма распределения памяти (менеджера памяти). В языке Java не могут быть использованы функции динамического распределения памяти и связанные с ними операции над адресами и указателями, поскольку они зависят от архитектуры вычислительной системы. Динамическая память в языке может выделяться только под классы и объекты самого языка. Для этого менеджер памяти должен сам организовывать своевременное выделение областей памяти при создании новых классов и объектов, а затем освобождать области памяти, которые уже больше не используются. В последнем случае должна решаться непростая задача сборки мусора — поиска неиспользуемых фрагментов в памяти и их освобождение. Причем, поскольку в языке Java за распределение памяти отвечает не пользователь, а интерпретатор кода, эти задачи должны решаться независимо от хода выполнения самой Java-программы.
Менеджер памяти входит в состав виртуальной Java-машины и, безусловно, зависит от архитектуры вычислительной системы, где функционирует эта машина, но ин герпретируемые ею программы при этом остаются независимыми от архитектуры. Хороший менеджер памяти — важная составляющая виртуальной Java- машины наряду с быстродействующим интер]гретатором промежуточного низкоуровневого кода.
При выполнении Java-нрограммы в глобальной сети компилятор, порождающий промежуточный низкоуровневый код, находится на стороне Ингериет-сервера, а интерпретатор, выполняющий этот код, — иа стороне клиента. По сеги от сервера к клиенту передается только уже скомпилированный код. С этой точки зрения использование языка Java для исполнения программ в сети и организации динамических Wcb-cTpainm дает преимущества по сравнению с использованием других языков, выполняемых на стороне сервера, как описано выше. Преимущество заключается в том, что но сети не надо передавать громоздкие HTML-описания страниц, что значительно снижает трафик в сети.
Однако отсюда проистекают и основные недостатки, присущие языку Java. Главный из них заключается в том, что на клиентской стороне должна присутство-
вить виртуальная Java-машина для интерпретации поступающего из сети кода Эго значит, что так или иначе интерпретатор языка Java должен входить в состав архитектуры целевой вычислительной системы, а без его наличия функционирование такой схемы становится невозможным Кроме того промежуточный код языка исполняется на стороне клиента а значит, скорость его выполнения и возможности Java-программы во многом зависят от производительности клиентского компьютера, которая может оказаться недостаточно высокой у машины, ориен гированной только на подключение к глобальной сети Речь идет не только о скорости интерпретации команд, но и о том, что клиентская система должна быть способна работать со всеми базовыми классами, присущими языку Java. Таким образом, требования к производительности клиентского компьютера для выполнения на ней Java программ могут оказаться непомерно высокими.
Еще одна особенность связана с необходимостью обеспечения безопасности при исполнении Java-программ. Поскольку код исполняемой программы поступает и ) глобальной сети, то нет никакой гарантии, что этот код не содержит фатальную ошибку. Кроме того, такой код может быть преднамеренно создай со злым умыслом. Имея все возможности языка программирования (в отличие от текста HTML) и исполняясь в среде вычислительной системы клиента, он может привести к выходу из строя этой системы или к повреждению хранящихся в ней даштых Потому при построении интерпретатора виртуальной Java-Mainmibi нужно целенаправленно выделять и отслеживать потенциально опасные команды (такие, как выполнение программ или обращение к файлам на клиентской машине). Несмотря на то что большинство производи гелей виртуальных Java-машин считаются с этим правилом, проблема безопасности остается актуальной при использовании языка Java.
Язык Java быстро завоевал популярность и занял значительное место на рынке языков программирования и связанных с ними средств разработки. По сути, этот язык явился первой удачной реализацией двухэтапной модели выполнения программ, пос троенной на использовании компилятора промежуточного кода с последующей интерпретацией полученного кода. Независимость выполнения Java- программ от архитектуры целевой вычислительной системы способствовала росту популярности языка, который стал применяться не только как удобное средство Web-программпрования, по и как средство разработки прикладных программ, ориентированных на различные вычислительные архитектуры.
Росту популярности языка программирования Java и построенных на его основе систем программирования способствовал также тот факт, что в его состав входят встроенные средства поддержки интерфейса с серверами БД, ориентированные на разработку приложений, выполняющихся в архитектуре «клиент—сервер». Системы программирования языка Java совместимы также с известной группой стандартов, ориентированной на разработку результирующих программ, выполняющихся в трехуровневой архитектуре — CORBA.
Первоначально язык поддерживался только одним разработчгком — компанией Sun. По со временем и с ростом популярности языка в его поддержку стали включаться и другие известные компании производители программных продуктов. Среди них можно выделить уже упоминавшуюся в данном пособии компанию Borland, создавшую на базе языка Java систему программирования Borland
JBuilder. В настоящее время компании, поддерживающие системы программирования на основе языка Java, образуют специальное сообщество для поддержк и распространения средств разработки на исповеданного языка. Всю необходимую информаций можно получить па сайте [101] и во многих других местах всемирной сети Интернет, которая во многом способствовала развитию этого языка*.
Основной проблемой языка Java остается его производительность. При прочих равных условиях интерпретируемая программа, построенная па основе системы программирования для Java, будет уступать в скорости откомпилированной программе, созданной в любой другой современной системе программирования. Поэтому язык Java практически не применяется в программах, требующих сложных расчетов и математических функций. Но во многих прикладных программах, где производительность не играет столь принципиального значения, он вполне может быть испольювап, так как дает преимущества в независимости результирующей программы от архитектуры целевой вычислительной системы. Большинство современных ОС допускают наличие в своем составе виртуальных Java-машин для интерпретации результирующего кода Java-программ.
Необходимость иметь в составе архитектуры вычислительной системы клиентского компьютера виртуальную Java-машниу, а также довольно высокие требования к производительности компьютеров на клиентской стороне в ряде случаев ограничивают возможности применения языка Java. С другой стороны зачас тую для организации динамической Web-страницы достаточно лишь выполнить ряд простых действий, не укладывающихся в рамки статического HTML. Для этого нет необходимости создавать, компилировать и передавать по сети полноценную Java-npoi рамму.
Чтобы решить эти проблемы, был предложен командный язык Java Script, который можно назвать упрощенным вариантом языка Java.
Фрагменты кода, написанные па Java Script, передаются непосредственно внутри текста HTML-страниц Синтаксис и семантика Java Script в целом соответствуют языку Java, но возможности его выполнения сильно ограничены — они не выходят за рамки roii программы, которая интерпретирует HTML-страницу В таком случае эта программа выступает и в роли виртуальной Java-машины для выполнения операторов Java Script. Чаще всего это программа навигации по сети — браузер. Он находит в тексте HTML-страницы операторы языка Java Script, выделяет их и исполняет по всем правилам языка Java. Однако выполнение Java Script происходит только в рамках адресного пространства программы- навигатора. Это, с одной стороны, ограничивает его возможности, по, с другой стороны, увеличивает безопасность выполнения операторов, так как в худшем
Известная компания — производи гель ОС и систем программирования Microsoft — некоторое время тоже способствовала распространению языка Java и занималась его поддержкой. Однако затем, к сожалению, отказалась от использования данного средства разработки по причинам маркетинговых и других разно! часпй с компанией Sun. Многие идеи, первоначально заложенные в Java, шинли спою реализацию в языке C# (Сп-шарп). предложенному этой компанией и продвигаемой ею в настоящее время на рынок средств разработки.
случае при наличии некорректных операторов языка неработоспособной окажется только запущенная программа-навигатор, но не вся клиентская система в целом. Операторы Java Script сейчас широко используются для организации динамических Web-страниц. Их выполнение (интерпретация) обеспечивается всеми современными программами-навигаторами в глобальной сети. Более подробную информацию о языке Java Script и его возможностях можно найти в [10] или на многих сайтах, посвященных языку Java [101].
Контрольные вопросы и задачи
Вопросы
Почему различные функции, выполняемые программными модулями в составе системы программирования, разнесены по различным модулям, а не включены в состав компилятора? Какие для этого есть исторические и технические причины?
Используя схему, приведенную в учебном пособии, объясните функционирование системы программирования иа различных этапах развития:
О в виде комплекса программных модулей, выполняемых по командам Makefile;
О в виде единой интегрированной среды разработки;
О в виде единой интегрированной среды разработки, объединенной со средствами проектирования программного обеспечения.
Каким целям служит командный файл Makefile? В чем его отличие от обычных командных файлов? В каких случаях в настоящее время продолжают использоваться средства разработки па основе Makefile?
Что такое Application Program Interface (API)? Какие задачи решаются в прикладных программах с помощью API? Какие средства реализации API существуют? (При необходимости обратитесь к первой части данного учебного пособия.)
Что такое «ресурсы прикладной программы»? Относятся ли к понятию «ресурсы прикладной программы» следующие типы данных:
О текст сообщения, выдаваемого программой при возникновении ошибки;
О тексты пунктов главного меню программы;
О файлы, обрабатываемые прикладной программой;
О цвет фона главного окна программы,
О с труктуры данных, используемые программой в процессе работы;
О файл подсказки, выдаваемой программой ио запросу пользователя?
В каких случаях иа современном рынке программных средств встречаются компиляторы и интерпретаторы, не входящие в состав систем программирования? Приведите примеры.
Если в процессе создания современной системы программирования встанет вопрос о разработке более эффективного компилятора либо о развитии сервисных средств и интерфейса пользователя, то какому направлению будет отдано предпочтение? Почему?
Какие специфические функции выполняет текстовый редактор в составе системы программирования? Как он должен быть взаимосвязан с другими модулями системы?
В чем особенности функционирования компилятора в составе системы программирования но сравнению с его функционированием в виде отдельного программного модуля?
Почему компоновщик носит также название «редактор связе#»? В чем заключается процедура разрешения внешних ссылок объектного файла?
В современных системах программирования загрузчик, как правило, отсутствует. Его функции выполняет ОС. Расскажите, как эти функции выполняются в различных ОС (при необходимости обратитесь к первой части учебного пособия). Какую информацию должна получит ь ОС от системы программирования для выполнения загрузки исполняемого (файла программы?
Современные отладчики используют средства ОС и возможности архитектуры целевой вычислительной системы для отладки прикладных программ. Из материала первой части данного учебного пособия вспомните, какие средства отладки существуют.
Если система программирования выполняет отладку результирующей программы путем моделирования архитектуры целевой вычислительной системы, то чем ограничены возможности такого отладчика? Чем отличается такое выполнение программы от работы интерпретатора?
В чем преимущества и недостатки динамически загружаемых библиотек по сравнению с обычными (статически подключаемыми) библиотеками? Если подключение к результирующей программе статической библиогеки выполняет компоновщик, то какая часть системы программирования или ОС ответственна за подключение динамически загружаемой библиотеки?
Как вызов функции динамически загружаемой библиотеки обрабатывается ком нилятором? Какие действия выполняет в этом случае компоновщик? Должна ли динамически загружаемая библиотека быть доступна системе программирования в момент компоновки результирующей программы?
Какие дополнительные возможности п]х?доставляет пользователю лексический анализ исходного текста про1"раммы «на лету'»? Как влияет реализация такого лексического анализа в системе программирования на эффективность работы компилятора, на количество проходов, выполняемых компилятором?
Какие преимущества имеет приложение (результирующая программа), функционирующее в составе архитектуры «клиент—сервер» по сравнению с обыч ным приложением? Какие дополнительные требования предъявляет такое приложение к йычислитёльпым ресурсам? Какие требования предъявляются к системе программирования, позволяющей создавать приложения, функции нируютцие в составе архитектуры «клиент—сервер»?
Клиентскую часть приложения, функционирующего в составе архитектуры «клиент—сервер», часто называют «толстым клиентом». Сравните функции и характеристики «толстого клиента» в архитектуре «клиент—сервер» с функциями и характеристиками «тонкого клиента» в трехуровневой архитектуре В чем их принципиальное различие?
Почему требования к вычислительным ресурсам у «топкого клиента», как правило, ниже, чем у «толстого клиента»?
Сравните функции сервера приложении и сервера данных В чем различие между ними?
Какие основные особенности определили успех и долгое существование системы программирования Turbo Pascal производства компании Borland на российском рынке средств разработки? В чем вы видите недостатки этой системы?
Сравните между собой системы программирования Borland Delphi и Borland C++ Builder. В чем преимущества и недостатки каждой из этих систем?
Какие особенности систем программирования компании Microsoft позволили им, на ваш взгляд, завоевать свое место па рынке средств разработки?
Сравните между собой системы программирования Microsoft Visual Basic и Microsoft Visual C++. В чем преимущества и недостатки каждой из этих систем? Есть ли у каждой из них характерные черты общие с системами программирования производства компании Borland?
В чем сходство и в чем различие принципов распространения прикладного программного обеспечения, созданного для ОС типа UNIX и для ОС Linux проекта GNU? Почему системы программирования (и прежде всего, системы программирования для языков Си и Си++) являются неотъемлемой частью этих ОС?
26 В чем заключаются основные особенности разработки программного обеспечения для глобальной сети Интернет? Почему в этой сети часто используются интерпретируемые языки?
В чем принципиальная разница функционирования динамических Web-страниц, организованных с помощью программного обеспечения на основе интерфейса CGI (или ISAPI), и динамических Web-страниц, построенных на основе языков типа Perl, PHP или ASP?
В чем принципиальная разница функционирования динамических Web-страниц, организованных с помощью программного обеспечения па основе интерфейса CGI (или ISAP1), и динамических Web-страниц, построенных с использованием JavaScript? В чем принципиальная разница выполнения команд на языке JavaScript и команд на языках типа Perl, PHP или ASP?
Задачи
Функция F1 содержится в библиотеке, построенной с помощью компилятора в некоторой системе программирования При попытке использовать эту функцию при построении исполняемого файла в другой системе программирования компоновщик выдает сообщение об ошибке вида «функция F1 не
непосредственно с кодами команд результирующей программы. Дерево операций четко отражает связь всех операций .между собой, поэтому его удобно использовать также для преобразований, увязанных с перестановкой и переупорядочива- нпем операций без изменения конечного результата (таких, как арифметические преобразования). Более подробно эти вопросы рассмотрены в [5, 6, т. 2, 82].
Автоматизация построения синтаксических анализаторов (программа YACC)
При разработке различных прикладных программ часто возникает задача синтаксического разбора некоторого входного текста. Конечно, ее можно всегда решить, полностью самостоятельно построив соответствующий анализатор. И хотя задача выполнения синтаксического разбора встречается не столь часто, как задача выполнения лексического разбора, ио все-таки и для ее решения были предложены соответствующие программные средства.
Автоматизированное построение синтаксических анализаторов может быть выполнено с помощью программы YACC.
Программа YACC (Yet Another Compiler Compiler) предназначена для построения сиигаксического анализатора контекстно-свободного языка. Анализируемый язык описывается с помощью грамматики в виде, близком форме Бэкуса—Наура (нормальная форма Бэкуса—Наура — ИФБН). Результатом работы YACC является исходный текст программы синтаксического анализатора. Анализатор, который порождается YACC, реализует восходящий LALR(l) распознаватель [5, 6, т. 1, 15, 65].
Как и программа LEX, служащая для автоматизации построения лексических анализаторов, программа YACC тесно связана с историей операционных систем типа UNIX. Эта программа входит в поставку многих версий ОС UNIX или Linux. Поэтому чаще всего результатом работы YACC является исходный текст синтаксического щтспозпавателя на языке С. Однако существуют версии YACC, выполняющиеся под управлением ОС, отличных от UNIX, и порождающие исходный код па других языках программирования (например, Pascal).
Принцип работы YACC похож па принцип работы LEX: на вход поступает файл, содержащий описание грамматики заданного КС-языка, а на выходе получаем текст программы синтаксического распознавателя, который, естественно, можно дополнять и редактировагь, как и любую другую программу на заданном языке программирования.
Исходная грамматика для YACC состоит из трех секций, разделенных символом %%, — секции описаний, секции правил, в которой описывается грамматика, и секции программ, содержимое которой просто копируется в выходной файл. Например, ниже приведено описание простейшей грамматики для YACC, которая соответствует грамматике арифметических выражений, многократно использовавшейся в качестве примера в данном пособии:
Xtoken а
Xstart е
XX
найдена». Какова может быть причина появления такого рода ошибки? Что следует предпринять разработчику программы?
Функция F1 содержится в библиотеке, построенной с помощью компилятора в некоторой системе программирования. Функция F1 отлажена и проверена, однако при использовании ее в исполняемом файле, созданном в другой системе программирования, возникает ошибка, связанная с защитой памяти. Результирующая программа нс может выполняться корректно. Какова может быть причина появления такого рода ошибки? Что следует предпринять разработчику программы?
Функция F1 является виртуальной функцией некоторого класса Ct. Па основе одного и того же исходного кода в одной системе программирования построены динамически загружаемая библиотека и исполняемый файл. Исполняемый файл создает экземпляр класса С1 и передает его в качестве параметра в динамически загружаемую библиотеку, где вызывается функция F1. Приведет ли такая схема к возникновению ошибок во время выполнения программы? Если ошибки возможны, то что следует предпринять разработчику программы? Как изменится ситуация, если предположить, что для создания исполняемого модуля и динамически загружаемой библиотеки использовался один и тот же исходный код, но разные системы программирования?
Имеется класс С1. На основе одного и того же исходного кода в одной системе программирования построены динамически загружаемая библиотека и исполняемый файл. Исполняемый файл создает экземпляр класса С1 и передает его в качестве параметра в динамически загружаемую библиотеку. Динамически загружаемая библиотека проверяет экземпляр класса на принадлежность к типу С1. Приведет ли такая схема к возникновению ошибок во время выполнения программы? Если ошибки возможны, то что следует предпринять разработчику программы?
ПРИЛОЖЕНИЕ А
Тексты программы параллельных взаимодействующих задач
Здесь приведены тексты программы, реализующей параллельное функционирование задач, взаимодействующих между собой в соответствии с рис. 6.6.
program MultiThreads;
uses Forms.
ThrForni in ThrForm.pas' {Formi) .
Threads in 'Threads.pas’ :
($R *RES)
begin
Application.Initialize.
Application.CreateForm (TForml.Formi).
Application.Run:
end.
unit ThrForm.
interface
uses
Windows, Messages. SysUtils. Classes. Graphics. Controls. Forms. Dialogs. Gauges.
StdCtrls. Threads
type
TForml - class (TForm)
StrButtonl: TButton:
GaugeA: Tgauge: GaugeB Tgauge; GaugeC: Tgauge GaugeD: Tgauge
GaugeE Tgauge; GaugeF: Tgauge: GaugeG: Tgauge: Memo! ; TMemo : Label 1: Tlabel; Label2: Tlabel. Label3: Tlabel; Iabel4: Tlabel: Label5 Tlabel: Label6. Tlabel: Label) Tlabel: ScrollBarl: TScrollBar; Label8: Tlabel Label9: TLabel;
procedure StrButtonlClicKSender TObject): { Запуск задач ] procedure FormCreate(Sender: TObject):
procedure ScrollBarlChange(Sender: TObject): private
{ Private declarations ) public
{ Public declarations } end:
var
Formi TForml. inpiementation {$R *.DFM)
procedure TForml .StrButtonlCL1ck(Sender: TObject) { Запуск задач } begin
StrButtonl,Enabled:~false:
Memol.Clear:
GaugeA.Progress:-0:
GaugeB.Progress:=0:
GaugeC.Progress:=0;
GaugeD.Progress:-0:
GaugeE.Progress:-0;
GaugeF. Progress :-=0:
GaugeG.Progress:-0:
Al 1 Starts(GaugeA.GaugeB.GaugeC.GaugeD.GaugeE.GaugeF.GaugeG). End:
procedure Tforml FormCreate(Sender TObject): begin
StartBtn -StrButtonl:
Del ay:-Scrol1Bar1.Positi on:
MessageL1st:-Memo1 end
procedure TForml,ScrollBarlChange(Sender: TObject): begin
Delay -ScrollBarl Position;
end: end
unit Threads:
{ Описание тредов } interface
uses
Classes,Gauges StdCtr1s.Syncobjs.
type
TParams - record { Параметры для инициализации треда ) OnnName:char;
ParentName:char:
Length integer;
end:
{ Основной обьект-родитель }{ Строка состояния }{ Длина задачи }( Текуцее состояние задачи }{ Признак завершения задачи ]{ Имя задачи }{ Имя запустившей задачи }{ Строка для вывода сообщений }{ Выполнение задачи }{ Прорисовка строки состояния }{ Вывод сообщения }{ Конечная часть задачи }{ Основной обьект-родитель }{ Строка состояния }{ Длина задачи }( Текуцее состояние задачи }{ Признак завершения задачи ]{ Имя задачи }{ Имя запустившей задачи }{ Строка для вывода сообщений }{ Выполнение задачи }{ Прорисовка строки состояния }{ Вывод сообщения }{ Конечная часть задачи }ThreadProgress = class(TThread) private
{ Основной обьект-родитель }
{ Строка состояния }
{ Длина задачи }
( Текуцее состояние задачи }
{ Признак завершения задачи ]
{ Имя задачи }
{ Имя запустившей задачи }
{ Строка для вывода сообщений }
{ Выполнение задачи }
{ Прорисовка строки состояния }
{ Вывод сообщения }
{ Конечная часть задачи }
{ Основной обьект-родитель }
{ Строка состояния }
{ Длина задачи }
( Текуцее состояние задачи }
{ Признак завершения задачи ]
{ Имя задачи }
{ Имя запустившей задачи }
{ Строка для вывода сообщений }
{ Выполнение задачи }
{ Прорисовка строки состояния }
{ Вывод сообщения }
{ Конечная часть задачи }
FGauge TGauge;
FLength:integer FProgress:Longint; Fimshed:boolean FName:char: FParentName.char FText: string; protected
procedure Execute override procedure DoVisualProgress;
procedure WriteToMemo: procedure Finish; virtual abstract
public
constructor create(Gauge.TGauge Params TParams)
end
TThreadA - class(ThreadProgress) protected
procedure Finish: override ; end;
TThreadB - dass(ThreadProgress) protected
procedure Finish; override;
end;
TThreadC - class(ThreadProgress) protected
procedure Finish; override;
end:
TThreadD = class(ThreadProgress)
protected
procedure Finish: override end.
TThreadE - class(ThreadProgress)
protected
procedure Finish; override :
end:
TThreadF - class(ThreadProgress)
protected
procedure Finish: override: end;
TThreadG - class(ThreadProgress )
protected
procedure Finish; override:
end:
procedure Al1 Starts(GaugeA.GaugeB.GaugeC.GaugeD.GaugeE.GaugeF. GaugeG:TGauge);
var StartBtn TButton;
Messagelist.TMemo
Delay:integer:
Params.TParams:
ThrA:TThreadA;
ThrB:TThreadB:
ThrC:TThreadC:
ThrD TThreadD:
ThrE.TThreadE;
ThrF TThread.F
ThrG TThreadG
CriticalSection: TCriticalSection; { Данный обьект позволяет организовать монопольный доступ к общим ресурсам }
implementation
procedure AlIStarts(GaugeA.GaugeB.GaugeC.GaugeD.GaugeE.GaugeF.GaugeG:TGauge):
{ Начало выполнения задач ) begin
Params OwnName - A"
Params.ParentName -" U ' :
Раrams.Length =2:
ThrA.-TThreadA Create(GaugeA Para s);
Params.OwnName -'B'
Params.ParentName 4 --’U'
Params Length:=4;
ThrB:=TThreadB.Create(GaugeB.Params)
Params OwnName:- C":
Params ParentName.='A
Params Length =2.
ThrC:=TThreadC.Create(GaugeC.Params):
Pa rams. OwnName: -' 0 "•:
Params.Pa ren LName;='A1
Params.Length -2:
ThrD -TThreadD CreaLeCGaugeD.Params):
Params.OwnName -"E":
Params.ParentName:-'A':
Params.Length.=4:
ThrE =TThreadE.Create(GaugeE.Params):
Params OwnName:='F';
Params ParentName:=:
Params.Length:-2
ThrF:-TThreadF,Create(GaugeF.Params).
Params.OwnName:-'G'
Params.ParentName -' -'
Params.Length -1;
ThrG =TThreadG.Create(GaugeG.Params):
CriticalSection =TCriticalSection Create
FhrA.Resume:
ThrB.Resume.
end:
{ ThreadProgress }
procedure ThreadProgress.Execute.
var i:integer:
begin
FText -'Запущена задача '+Fname+' (задачей +FParentName+')'
Synchronize(WriteToMemo);
for i:=l to FLength do
begin
FProgress:=FProgress+l:
SynchronizefdoVisualProgress) end: Finish;
Destroy:
end:
constructor ThreadProgress create(Gauge;TGauge;ParamsTParams); begin
inherited create(true).
FGauge.'Gauge;
FLength -delay*Params.Length
FGauge MaxValue:-FLength;
FGauge.MinValue:=O:
FGauge.Progres s:-0;
FProgress:=D;
FName;=Params.OwnName
FParentName:-Parains.ParentName:
Finished:-false;
end:
procedure ThreadProgress DoVi sualProgress:
begin
FGauge.Progress:-FProgress;
end;
procedure ThreadProgress.WriteToMemo:
begin
MessageList. ines.Add(FText):
end:
{ TThreadA }
procedure TThreadA.Finish:
begin
ThrC Resume;
ThrD.Resume:
ThrE.Resume:
FText;- 'Задача А завершила работу и запустила задачи C.D.E .
Synchronize(WriteToMemo): Finished -true end;
{ TThreadB }
procedure TThreadB.Finish;
begin
FText:- Задача В завершила свою работу";
Synchronize(WriteToMemo):
Finished:-true:
CriticalSection.Enter:{ Начало защищенного блока команд )
try
if (ThrC.Finished-false)and(ThrD.Finished-false) then begi n
ThrF.FParentName:-'8
repeat
Synchronize(DoVisualprogress):
unti1 (ThrC.Finished)and(ThrD.Finished):
ThrF Resume.
FText -'Задача В запустила задачу F .
Synchronize{WriteToMemo):
end:
finally CriticalSection Leave:( Конец защищенного блока команд )
end:
end:
{ TthreadC )
procedure TThreadC.Finish:
begin
FText:-'Задача С завершила свою работу
Synchronize(WriteToMenro):
Flnishec -true;
CriticalSection.Enter:
try
if (ThrB.Flnished-false)and(ThrD.Finished-false) then begin
ThrF.FParentName:-'C:
repeat
Synchronize(DoV1 sualprogress}:
until (ThrB Finished)and(ThrD Finished):
ThrF.Resume;
FText— Задача С запустила задачу F':
Synchronize(WriteToMemo);
end.
finally
CriticalSection Leave.
end;
end;
{ TthreadD }
procedure TThreadD.Finish;
begin
FText:-1 Задача D завершила свою работу'
Synchronize{WriteToMemo)
Finished:-true:
CriticalSection.Enter: try
if (ThrC.Finished=false)and{ThrB Finished-false) then begin
ThrF.FParentName =' D ' , repeat
Synchronize(DoVisualprogress}: until (ThrC.Finished)and(ThrB.Finished) ThrF.Resume;
FText -‘Задача D запустила задачу F'
SynchronizefWri teToMemo) end;
finally
CriticalSecti on.Leave;
end: end:
{ TThreadE )
procedure TThreadE.Finish;
begin
Finished:-true;
Critical Section Enter try
if ThrF.Finished then begin
ThrG.FParentName - E‘:
ThrG.Resume:
FText -'Задача E завершила работу и запустила задачу G Synchro™ ze{Wri teToMemo):
end else begin
FText:-’Задача E завершила свою работу1
Synchronize{WriteToMemo): end;
fi nally
CriticalSection.Leave:
end;
end;
{ TThreadF )
procedure TThreadF.Finish;
begin
Finished:-!rue:
CriticalSection.Enter:
try
if ThrE.Finished then
begin
ThrG.FParentName:- ' F* ;
ThrG.Resume;
FText -'Задача F завершила работу и запустила задачу G'
Synchronize(WriteToMemo).
end
else
begin
РТехЬ;-'Задача F завершила свою работу':
Synchronize{WriteToMemo):
end:
finally
CriticalSection.Leave;
end;
end:
{ TThreadG )
procedure TThreadG.Finish:
begin
Finished -true:
FText:=’3ana4a G завершила работу':
Synchronize{WriteToMemo);
StartBtn Enabled;-true
CriticalSection.Free:
end:
end.
(• е '<' m I е - т | m
■и m *‘ t j w /' t j t
i a | ' - e ■ । ■ ;
IX
Секция описаний содержит информацию о той что идентификатор а является лексемой (терминальным символом) грамматики, а символ е ■ ее начальным не- герм иная ы i ы м с им волом.
Грамматика записана обычным образом — идентификаторы обозначают терминальные и нетерминальные символы; символьные константы типа ' + ' и -' считаются терминальными символами. Символы :, |, ; принадлежат к метаязыку YACC и читаются согласно НФБН «есть но определению», «или» и «конец правила» соответственно.
В отличие от LEX, который всегда способен построить лексический распознаватель, если входной файл содержит правильное регулярное выражение, YACC пе всегда может построить распознаватель, даже если входной язык задан правильной КС-грамматикой. Ведь заданная грамматика может и пе принадлежать к классу LALR(l). В этом случае YACC выдаст сообщение об ошибке (наличии неразрешимого LALR(l) конфликта в грамматике) при построении синтаксического анализатора. Тогда пользователь должен либо преобразовать грамматику, либо задать YACC некоторые дополнительные правила, которые могут облегчить построение анализатора. Например, YACC позволяет указать правила, явно задающие приоритет операций и порядок их выполнения (слева направо или справа налево).
С каждым правилом грамматики может быть связано действие, которое будет выполнено при свертке по данному правилу. Оно записывается в виде заключенной в фигурные скобки последовательности операторов языка, на котором порождается обходный текст программы распознавателя (обычно это язык С). Последовательность должна располагаться после правой част и соси вотс гвующего правила. Также YACC позволяет управлять действиями, которые будут выполняться распойиайател ем в том случае, если входная цепочка не принадлежит заданному языку. Распознаватель имеет возможность выдать сообщение об ошибке, остановиться либо же продолжить разбор, предприняв некоторые действия, связанные с попыткой локализовать либо устранить ошибку во входной цепочке.
Болес подробные сведения о программе автоматизированного построения синтаксических распознавателей YACC можно получить в [5, 11, 39, 68].
Контрольные вопросы и задачи
Вопросы
Что такое трансляция, компиляция транслятор, компилятор? Из каких процессов состоит компиляция? Расскажите об общей структуре компилятора.
ПРИЛОЖЕНИЕ Б
Тексты программ комплекса параллельных взаимодействующих приложений
Здесь приведены тексты следующих программ. А, В, D и G. Эти программы отражают алгоритмы взаимодействия, заданные па рис. 6.7. Остальные программы содержат аналогичные алгори тмы взаимодействия.
Текст программы А
^define INCL_BASE
^define INCL_DOS
^include <os2.h>
^include «string.h>
#include <stdio.h>
#include <stdlib.h>
#include "progr.h"
#1nclude "func.h"
/•Глобальные переменные*/
char mywordt]- "PROGR_A.EXE":
BYTE atrib[2]:
/♦Главная функция*/
int mainO
{/♦Локальные переменные*/
int i.j: PipeS1ze=250. strcpy(Word.myword); atrib[0]=' '. atrib[l]-Qx02f.
VioScrollUp(0.0.25.80.25.&atrib.0):
/♦Фиксация времени начала процесса*/
DosGetDateTime(&Тime):
strcpy(BegTime.itoa(Time.hours.MyBuffer.lO));strcat(BegTime. "):
strcat(BegTime.1toa(Ti me.minutes.MyBuffer.10)): streat(BegTime.":").
streat(BegTime.1toa(Time.seconds.MyBuffer.10)): streat (BegTime. C");
streat(BegTime.i toa(T1me.hundredths.MyBuffer.10)).
/♦Установка одинакового приоритета всех подпроцессов*/
rc=DosSetPriority(PRTYS_PRDCESSTREE,2 10 0);
if (rc!-0) (HaltSystemCSet Priority ”,rc): }
/* Создание файла, доступного всем подпроцессам */
гс - Dos0penCresult.txt",&File0pen.8Action 0.0 0x001010x0002 0x0002]0x004010x4000.0), if (rc!-0) {HaltSystemCDosOpen ".rc): }
/♦Создание семафоров*/
rc - DosCreateEventSemC’WSEM32WPIPESEM". SPipeSem. DC_SEM_SHARED. 0);
if (rc!-0) {HaltSystemCCreate Sem PipeSem “.rc). }
rc - DosCreateEventSemC WSEM32WEXITSEM . SExitSem. DC_SEM_SHARED 0):
if (rc!-0) { HaltSystemCCreate Sem (ExitSem) ".rc):)
rc - DosCreateEventSem("\\SEM32\\WRITEFlLE'. 8FileSem. DC_SEM_SHARED. 0)
if (rc!-0) { HaltSystemCCreate Sem (FileSem) ”.rc):)
rc - DosCreateEventSemC\\SEM32\\P0INT2SEM”,8Point2Sem. DC_SEM_SHARED. 0).
if (rcl-0) { HaltSystemCCreate Sem (Point2Sem) *.rc):}
rc - DosCreateEventSemC\\SEM32\\P0INT3SEM" 8Point3Sem, DC_SEM_SHARED. 0).
if (rc!-0) { HaltSystemC"Create Sem (Point3Sem) ".re).}
rc - DosCreateEventSemC '\\SEM32\\P0INTlSEM".8PowtlSem. DC_SEM_SHARED. 0).
if (rc'=0) { HaltSystemCCreate Sem (PointlSem) ".rc).}
rc = DosPostEventSem(PipeSem); if (rc!=0) (HaltSystemC"PostSem (PipeSem)" rc) )
rc - DosPostEventSem(FileSem): if (rcl=0) {HaltSystemCPostSem (FileSem)".rc): )
/‘Создание Pipe (транспортера)*/
rc-DosCreatePipe(8ReadHandle.8WriteHandle.PipeSize).
if (rcl-0) (HaltSystemCCreate Pipe'.rc);)
/♦Задание строк аргументов*/
for (i-0:i<=strlen(myword):i++ ) { Arguments ]=myword[i]; }
j-i; Argument2-itoa(ReadHandle.My8uffer.lO).
for (i=0;i<-strlen(Argument2);i++) { Argument[j+i]-MyBuffer[i] }
j+-i: Arguments-1]-'
Argument2-itoa(UriteHa ndle MyBuffer.10)
for (i-0 i<=strlen(Argument2):i++) { Argument[i+j]=MyBuffer[i] )
J+-1: Argument[j-l]='Argument2=itoa(FileOpen,MyBuffer. 10).
for (i-0;i<-strlen(Argument2):i++) { Argument[i+j]-MyBuffer[i] )
/♦Вывод сообщений и выполнение некоторого процесса (цикла)*/
strcpy(PMessege.'Программа "): strcat(PMessege myword)
VioWrtNAttr(8atr.80*25.0 0.0) VioWrtCharStr(PMessege.sizeof(PMessege),2.1 0):
strcpy(PMessege "Запущена в "). strcat(PMessege BegTime)
VioWrtNAttr(8atr 80*25.0.0.0): VioWrtCharStr(PMessege.sizeof(PMessege) 3.1 0).
strcpyCFMessegel myword): strcatCFMessegel." "): strcat (FMessege: PMessege
strcpy(PMessege."Программой "): strcatCPMessege,"");
VioWrtNAttr(&atr.80*25.0.0 0). VioWrtCharStr(PMessege.sizeof(PMessege).4.1.0)
strcpy(FMessege2.myword). strcat(FMessege2." "): strcat (FMessege2.PMessege).
for(i-0;i<22:i+*)
{ V i oWrt NAtt г (&а t г.80*25.0.0.0): VIoWrtCharStr(*|-.1.5.1*1,0):
for (j-0;j<(12500*Speed):j++); }
/♦Получение времени загрузки программ потомков*/ DosGetDateTime(STime).
strcpy(MyTi me.i toa(Т ime.hours.MyBuffer.10)), strcat(MyTime.‘;“):
strcat(MyTime.itoa(T1me.minutes.MyBuffer. 10)): strcat(MyTime.":*);
strcat(MyTime.itoa(Time,seconds,MyBuffer.10)): strcat(MyTime." "): strcat(Myfime.itoa(Time hundredths.MyBuffer.10)): strcpy(Inform.LaunchTime.MyTime); strcpy(Inform.ParentName myword). Inform.Number=4: strcpy(PMessege.“Завершена в "): strcat(PMessege.MyTime)
VioWrtNAttr(Satr.80*25.0.0.0) VioWrtCharStr(PMessege.s1zeof(PMessege) 6.1.0):
strcpy(FMessege3.myword): strcat(FMessege3." "): strcat (FMessege3 PMessege); /♦Ожидание доступности записи в файл и немедленная запись в файл*/
do{ DosWaitEventSem(FileSem.-l):
rc-DosResetEventSem(Fi1eSem.SBytesWrltten):
) while (rc!-0);
DosWrlte(Fi1eOpen.(PVOID)SFMessegel.sizeof(FMessegel).SBytesWrltten)
DosWrl te (Fi 1eOpen.(PVOID)SFMessege2.sizeof(FMessege2).SBytesWrltten);
DosWrlte(FileOpen,(PV0ID)SFMessege3.sizeof(FMessege3).SBytesWritten DosPostEventSem(FileSem),
/♦Запись сообщения в Pipe: имя предка и время запуска программ*/
re- DosWrite(WriteHandle.(PVOID)SInform,sizeof(Inform). SBytesWritten):
if (rc!-0) { HaItSystemt“(DosWrite)".re): }
/*3апуск программ - потомков*/
rc-DosExecPgm(Fai1Fi1eb.sizeof(Fa11Fi1eb).1.Argument.0.SResCodeb. progr b.exe"):
if (rc!-0) (HaltSystem("(DosExecPgm) B".rc):)
rc-DosExecPgm(Fa11F i1eb.s1zeo f(Fa11Fi1eb).1.Argument.0.SResCodeb."progr_c.exe");
•if (rc!-0) {HaltSystem(”(DosExecPgm) C'.rc).}
rc-DosExecPgm(Fai1Fi1eb.si zeof(Fai 1Fi1eb).1.Argument.0.SResCodeb."progr_i.exe"):
if (rc!-0) (HaltSystem("(DosExecPgm) Г.гс):)
rc-DosExecPgni(FailFileb.sizeof(FailF1leb).l.Argument.0.SResCodeb."progr_j.exe");
if (rc!-0) {HaltSystem("(DosExecPgm) J".re).)
/*0жидание загрузки семафоров в программах-потомках*/
DosWaitEventSem(ExitSem -1);
DosCloseEventSem(ExitSem):
return(O):}
Текст программы В
^define INCL BASE
#define INCLDOS
#include <os2.h>
#include <strmg .h>
#include <stdio.h>
#i nd ude <stdhb h>
#include "progr.h"
^include "func.h"
/♦Глобальные переменные*/
char myword[]= " PROGR_B EXE":
/♦Главная функция*/
int main(int argc, char *argvEL char *envp[])
{/‘Локальные переменные*/
mt l.j: strcpy(Word.myword):
/♦Инициализация семафоров*/
rc = Dos0penEventSemC\\SEM32\\PIPESEM". SPipeSem)
if (rc!-0) (HaltSystemCOpen Sem (PipeSem) ".rc) )
rc - DosOpenEventSem("\\SEM32\\EXITSEM". &ExitSem);
if (rc!-0) { HaltSystemCOpen Sem (ExitSem) ",rc);)
rc = Dos0penEventSem("\\SEM32\\WRITEFILE". &FileSem).
if (rc!-0) { HaltSystemCOpen San (FileSem) *.rc) }
rc = Dos0penEventSemC\\SEM32\\P0INTlSEM".&PointlSem);
if (rc'=0) { HaltSystemCCreate Sem (PointlSem) ‘.rc):}
rc = Dos0penEventSem("\\SEM32\\PDINT2SEM" &Point2Sem);
if (rc!=0) { HaltSystemCCreate Sem (Point2Sem) ".rc);)
rc - Dos0penEventSemC\\SEM32\\P0INT3SEM" &Point3Sem);
if (rc!-0) { HaltSystemCCreate Sem (Point3Sem) ".rc):)
/♦Проверка количества аргументов */
if (argc!-4) { HaltSystem( "Ошибка командной строки" rc).} /♦Инициализация переменных для записи в транспортер и файл*/
WriteHandle-atoi(argv(2]) ReadHandle=atoi(argvED): FileDpen^atoKargvO]) /♦Ожидание свободного транспортера*/ do { DosWaitEventSem(PipeSem.-l):
rc=DosResetEventSem(PipeSem.&NPost): } while (rc!-0);
/* Работа с транспортером*/
rc-DosRead(ReadHandle. (PVOIDl&Oldlnform, sizeof(OlcInform). &BytesReaden):
DosPostEventSem(PipeSem): if (rc!=0) (HaltSystemC"Read Pipe'Vc) } /♦Уведомление предка о завершении инициализации*/ if (01 dinform Number==l)
{ rc - DosPostEventSem(ExitSem);
if (rc!-0) { HaltSystemCPostSem (ExitSem) ".re);} }
else { do { Oldlnform Number--:
OosWaitEventSem(PipeSem.-1):rc=DosResetEventSem(PipeSem.&NPost);} while (rc!=0):
rc- DosWnte(WnteHandle. (PV01D)801dInform.sizeof(01dInfcrm) SBytesWritten) DosPostEventSem(PipeSem): if (rc!-0) (HaltSystem("Write Pipe .rc);)}
/‘Задание строк аргументов*/
for (i=0:i<-strlen(myword),i++ ) ( ArgumentEilnnywordEi]; )
j-i; Argument2=itoa(ReadHandle MyBuffer 10);
for (i=0;i<-strlen(Argument2); i++) ( ArgumentEj+D-MyBufferEi]. }
j*-i: ArgumentEj-1]-’ '. Argument2=itoa(WriteHgndle.HyBuffer.lO):
for (i=0;i<=strlen(Argument2):i++) { ArgumentEi+j]-MyBufferEi] }
J+-1: ArgumentEj 1]-' ', Argument2=itoa(FileOpen.MyBuffer.10);
for (1-0 1<-strlen(Argument2):i++) ( ArgumentEi+j]-MyBufferEi] } /♦Выполнение процесса - цикл */
strcpytPMessege."Программа ”): strcattPMessege.myword), V1oWrtNAttr(8atr.80*25.0.0.0). VioWrtCharStr(PMessege.sizeof(PMessege).2.27.0); strcpytPMessege "Запущена в "). strcattPMessege.Oldlnform LaunchTIme): VioWrtRAttrt&atr.80*25.0.0.0) VioWrtCharStrtPMessege.sizeoftPMessege).3.27 0): strcpytFMessegel myword) strcat(FMessegel." "): strcat (FMessegel.PMessege). strcpytPMessege."Программой “); strcat'PMessege.Oldlnform.ParentName) V1oWrtNAttr(8atr.80*25.0.0.0): ViokrtCharStr(PMessege.sizeof(PMessege).4.27.0): strcpy(FMessege2.myword). strcat(FMessege2 " “). strcat (FMessege2,PMessege): fort -0:1<22:i++)
{VioWrtNAttrt&atr.80*25 0,0.0): VioWrtCharStrC|".1.5.27+1.0): for (j-0:J<(25000*Speed):j++).};
/‘Получение времени загрузки программ-потомков*/
DosGetDatefimet&Time):
strcpy(MyT1me.itoa(T1me.hours.MyBuffer.10)); strcattMyTime " ’) strcat(MyTime.itoatTime.minutes.MyBuffer.10)), strcattMyTime. “). strcattMyTime.itoa(Time.seconds.HyBuffer 10)). strcattMyTime," "). strcat(MyTime.1toa(Time.hundredths.MyBuffer.10)). strcpy(Newlnform.LaunchTIme.MyTime). strcpytNewInform ParentName.myword): NewInform.Number=3;
strcpytPMessege."Завершена в ") strcattPMessege MyTime):
V oWrtNAttrt&atr.80*25.0.0.0) VioWrtCharStr(PMessege.sizeoftPMessege).6.27 0):
strcpy(FMessege3.myword). strcat(FMessege3." ") strcat (FMessege3.PMessege /*0жидание доступности записи в файл и немедленная запись в файл*/
do{ Dosk'aitEventSemtFileSem.-1);
rc=0osResetEventSem(F11eSem.SBytesWr1tten).
} while (rc!—0):
DosWr’te(FileOpen.(PV0ID)8FMessegel.sizeoftFMessegel).&BytesWntten)
DosWrite(File0pen.(PV0ID)&FMessege2.sizeof(FMessege2).&BytesWritten): DosWritetFileOpen.(PV0ID)&FMessege3.sizeof(FMessege3),&8ytesWrltten): DosPostEventSemtFi1eSem);
/♦Алгоритм прохождения точки 2*/ rc-DosPostEventSem(PointlSem);
if (rc--0) (
/♦Запись сообщения в Pipe имя предка и время запуска программ*/
re- Oosh'ritetWriteHandle. (PVOIDl&NewInfcrm.s zeof(Newtnform) &BytesWritten);
if (rcl-0) { HaltSystem("(DosWrite)".rc) }
/♦Создание семафора ожидания инициализации ресурсов потомками*/
гс - DosCreateEventSem("\\SEM32\\EXITSEM2". &ExitSem2. DC_SEM_SHARED. 0):
if (rc!-0) { HaltSystemt"Create Sem (ExitSem2) ".re):}
/♦Запуск программ-потомков*/■ ■ ■<■
rc=DosExecPgm(Fai1 Filet).sizeoftFaiIFileb).!,Argument.O.&ResCodeb."progr_d.exe")
if (rc!=0) (HaltSystem("(DosExecPgm)".rc).}
rc=DosExecPgm(FailF1leb sizeof(FailFileb) 1.Argument.0.RResCodeb "progr e exe");
if (rc!=0) (HaltSystem("(DosExecPgm)"-.rC):}
rc*DosExecPgm(FailF1leb.sizeof(FaiIFileb)Л>.Argument.O.iResCodeb,"progr f exe");
If (rc!-0) {HaltSystem("(DosExecPgm)".rc).} /*0жидание инициализации ресурсов потомками*/
DosWaitEventSem(ExitSem2.-1):
DosCloseEventSem(ExitSem2).
return(O):}
Текст программы D
^define 1NCL_BASE
#defme 1NCL_DOS
#include <os2.h>
#include «string.h>
# include «stdio.h> include «stdlib h>
#1nclude "progr.h"
#include "func h"
/*глобальные переменные*/
char myword[]- "PROGR_D.EXE"
/‘Главная функция*/
mt mainfint argc. char *argv[], char *envp[])
{/‘Локальные переменные*/
mt i.j strcpy(Word.myword):
/*Инициализация семафоров*/
rc - Dos0penEventSem("\\SEM32\\PlPESEM" SPipeSem)
if (rc!-0) {HaltSystemCOpen Sem (PipeSem) ",rc) }
rc - Dos0penEventSem("\\SEM32\\WRITEFILE". SFileSem)
if (rcl-0) { HaltSystemCOpen Sem (FileSem) ".rc).}
rc - Dos0penEventSemCWSEM32\\P0INT2SEM".&Point2Sem)
if (rc!=0) { HaltSystemt Create Sem (Point2Sem) " rc).}
rc - Dos0penEventSem("\\SEM32\\P0INT3SEM".№oint3Sem)
if (rc!-0) { HaltSystemt"Create Sem (Point3Sem) ".rc):}
rc - Dos0penEventSemC\\SEM32\\P0INTlSEM" &PomtlSem):
if (rc!-0) { HaltSystemt"Create Sem (PointlSem) ".rc):}
rc - Dos0penEventSemf\\SEM32\\EXITSEM2". &ExitSem2):
if (rc'-O) { HaltSystemCCreate Sem (ExitSem2) ".rc):}
/‘Проверка количества аргументов */
if (argc*-4) { HaltSystemt "Ошибка командной строки" rc):} /‘Инициализация переменных для записи в транспортер и файл*/ File0pen=atoi(argv[3]):WriteHandle-atoi(argv[2]) ReadHandle-atoi(argv[l]): /‘Ожидание свободного транспортера*/
do { DosWaitEventSemtPipeSem.-1)
rc-DosResetEventSem(PipeSem.&NPost).
} while (rc!-0):
I* Работа c Pipe каналом*/
rc-DosRead(ReadHandle (PVOIOl&Oldlnform. sizeof(Oldlnform). &BytesReaden)
DosPostEventSemtPipeSem). if (rc!-0) {HaltSystemt"Read Pipe",rc):}
/♦Уведомление предка о завершении инициализации*/
if (01dinform Number==l)
{ rc - DosPostEventSem(ExitSem2):
if (rc!-0) { HaltSystem("PostSem (ExitSem2) ".rc);) } else { do { Oldlnform Number--;
DosWaitEventSem(PipeSem.-l)■ rc=DosResetEventSem(PipeSem.&NPost); } while (rc!-0)
rc- OosWritetWrlteHandle.(PV0ID)801dInform. sizeof(Oldlnform). SBytesWritten);
DosPostEventSemtPipeSem).
if (rc!=0) {HaltSystemt"Write Pipe".rc):} }
/♦Задание строк аргументов*/
for (1-0;i<-str1en(myword):i*+ ) { Argument[i]-myword[i]: }
j=i; Argument2-itoa(ReadHandle.MyBuffer. 10):
for (i=0;i<=strlen(Argument2):i++) { Argument[i+i]-MyBufferLi]; }
j+-i; Argument[j-1]- ' Argument2=itoa(Writehandle.MyBuffer,10);
for (i-0:1<=strlen(Argument2) ++) ( Argument[i+j]-MyBuffer[i]: }
j+-i Argument[J-l]= '. Argument2-itoa(F1leOpen.MyBuffer.10)
for (i-0;1<=strlen(Argument2),i++) { Argument[i+j]-MyBuffer[i]; }
/♦Выполнение процесса цикл */
strcpytPMessege."Программа "). strcattPMessege.myword):
VioWrtNAttr(8atr,80*25.0.0.0). VioWrtCharStrtPMessege.sizeoftPMessege).8,54.0);
strcpytPMessege.'Запущена в ”): strcattPMessege.Oldlnform.LauncnTime) V1oWrtNAttr(8atr.80*25.0.0.0). VioWrtCharStrtPMessege.sizeof(PMessege) 9.54,0)- strcpytFMessegel.myword): strcattFMessegel " "): strcat (FMessegel.PMessege): strcpytPMessege.'Программой "): strcattPMessege.01dinform ParentName) VioWrtNAttr(8atr,80*25.0.0,0); VioWrtCharStrtPMessege.sizeoftPMessege).10.54.0). strcpy(FMessege2.myword): strcat(FMessege2." "); strcat (FMessege2.PMessege);
for(i-0.i<22;i++)
(VioWrtNAttrtSatr 80*25.0.0 0): VioWrtCharStr("|''.l 11.54+1.0);
for (j-0.j<(15000*Speed):j++): }:
/♦Получение времени загрузки программ-потомков*/
DosGetOateTimet&Time):
strcpy(MyT1me.itoa(Time.hours MyBuffer.10)): strcattMyTime ':"):
strcattMyTime.itoa(T1me minutes.MyBuffer.10)): strcattMyT me," "):
strcat(MyTi me.1toa(T1me.seconds.MyBuffer.10)): strcat(MyTime." “);
strcattMyTime.itoa(Time.hundredths.MyBuffer,10)), strcpy(Newlnform LaunchTIme.MyTime); strcpy(NewInform.ParentName.myword): NewInform.Number-2;
strcpytPMessege "Завершена в "): strcat(PMessege.HyTime) VioWrtNAttr(&atr.80*25.0.0.0).VioWrtCharStrtPMessege.sizeof(PMessege).12.54.0); strcpy(FMessege3.myword). strcat(FMessege3 " "): strcat (FMessegej.PMessege): /♦Ожидание доступности записи в файл и немедленная запись в файл*/
do{ DosWaitEventSem(F1leSem.-1).
rc-DosResetEventSem(Fi1eSem.SBytesWritten)
} while (rc1-0):
DosWnte(FiIeOpen.(PVOIDl&FMessegel.sizeoftFMessegel).SBytesWritten). DosWritetFileOpen.(PV0ID)&FMessege2.sizeof(FMessege2).KBytesWritten):
DosWr т tе(F11eOpen.(PVO10)&FMessege3.sizeof(FMessegeS).&BytesWritten) DosPostEventSemtFileSem);
/‘Алгоритм прохождения точки 3*/ rc-0osPostCventSem(Point2Sem): if (rc=*0)
( dc{ DosQueryEventSem(Point2Sem.8BytesWr tten);
jwhlle (BytesWritten<=2):
/‘Запись сообщения в Pipe: имя предка(Progr_A) и время запуска программ*/ re- DosWrite(Kr1teHand1e.(PV0ID)&Ne*'InforT,s1zeof(Ne>Jnform RBytesWritten)
if (rc!-0) { HaltSystemC'tOoshrite)".rc). }
/‘Создание семафора ожидания инициализации ресурсов потомками*/
rc - DosCreateEventSen("\\SEM32\\EXITSEM3". SExitSemS. DC_SEM_SHARED. 0);
if (rc!=0) { HaltSystemt"Create Sem (ExitSem3) " rc);}
/‘Запуск программ-потомков*/
rc=DosExecPgm(Fai 1Fileb.si zeof(Fai 1Fi1eb).1.Argument.0.SResCodeb."progr_g.exe"):
if (rc!-0) {HaltSystem("(DosExecPgm)",rc):}
rc-DosExecPgm(Fai 1 Ft 1eb.sizeof(Fa i1Fi1eb),1. Argument.0.SResCodeb."progr_h.exe");
if (rc!-0) {HaltSystemt'(DosExecPgm)".rc);}
/‘Ожидание инициализации ресурсов потомками*/ 0osWaitEventSem(Exit5em3.-1): DosCloseEventSem(ExitSem3);
}
return(O):}
Текст программы G
#define INCLBASE
#define INCLOOS
^include <os2 h>
^include «string h>
^include <stdw h>
#include <stdlib.h>
#include "progr.h"
#indude "func.h"
/‘Глобальные переменные*/
char myword[]= 'PROGR_G.EXE":
/‘Главная функция*/
int maintmt argc. char *argv[], char *envp[J)
{/‘Локальные переменные*/
int i.j strcpyDford.myword):
/‘Инициализация семафоров*/
rc - DosOpenEventSem("\\SEM32\\PIPESEM". KPipeSem);
if (rc!=0) {HaltSystemt"Open Sem (PipeSem) ”.rc): }
rc = Dos0penEventSem("\\SEM32\\EXITSEM3". &ExitSem3)
if (rc'-O) ( HaltSystemt"Open Sem (ExitSem3) ".rc):}
rc - DosOpenEventSem("\\SEM32\\WRlTEFILE". SFileSem)
if (rcf=0) ( HaltSystemt"Open Sem (FileSem) ".he);}
те = DosOpen ventSemt WS M32WPO1NT2SEM' &Point2Sem).
f (rc'-O) { HaltSystemCCreate Sem (Point2Sem) .rc).}
rc - 0os0penEventSem("\\SEM32\\P INT3SEM* &Point3Sem);
if (rc!=0) ( HaltSystemCCreate Sent (Point3Sem) ’ rc):}
rc - DosOpeiiEventSem("\\SEM32\\PDINTlSFM" SPointlSem).
if (rc'-O) { Hal System!“Create Sem (PointlSem) " rc): /♦Проверка количества аргументов */
if (argc1 4) { Ha tSystem “Ошибка командной строки .rc);} /♦Инициализация еременных для за иси в ранспортер и файл*/ Wr teHandle atoi(argv[2]);ReadHandle=atoT(argv[l]):File0pen-atoi(argv[3]);
/♦Ожидание свободного транспортера*/
do { OosWaitEventSem(P1peSen. 1):
rc-DosResetEventSemt Р ipeSem.SNPost);
} while (rc =0)
/* Работа Pipe аналом*/
rc=DosRead(ReadHandle. (PVOTD)8OldInform, s zeof(Oldlnform). SBytesReaden) DosPostEventSem(PipeSem)
if (rc!-0) {HaltSystemt“Read Pipe".rc) }
/♦Уведомление поедка о завершении инициализации*/
if (Oldl form Nunbei—1
{ rc - Dos ostEventSem ExitSemS)
if (rc'-O) { HaltSys em(‘PostSem (ExitSem3) " rc) } } else { do { Oldlnform Number
DoskaitEventSem PipeSem. 1);
rc DosRe etEventSem PipeSem.SNPost) } while (rd—0);
rc- DosKri e(WriteHand e. PVOIDl&Oldlnform. s zeof(01 Inform ,&BytesWritten) DosPostrvertSemlPi peSem);
if (rc 0) {HaltSystemCHrite Pipe'.rc).} }
/♦Задание строк аргументов*/
for (1=0- <-strlen(myword) i++ ) { Arguments ]-niyword[1] }
j-1 Argument2-1toa(ReadHandle MyBuffer 0)
for (i=0:i<=strlen(Argument2) ♦♦) { ArgumentEjr]=MyBuffer[i], } j+-1: ArgumenL[j-l]- ' • Argument2= oa(WriteHandle MyBuffer 10). for ( =0 i<=str cn Argument2) ++) { Argument[ +j]-MyBuffer[ ], j*-i. Argument[j 1]= ' Arguirent2=itoa(File0pen.MyBuffer.10) for ( •О.тт-strlen Argiraicnt2) +*) { Argument[ + ]-MyBuffer[ : } /♦Выполнение процесса - цикл */ strcpy(PMessege. Программа '). strcat(PMessege myword) VToHrtNAtti &a 80*25.0.0.0): VioWrtCharSt PMessege.s zeof(PMessege).14 54 0,. strepytPMessege,"Запушена в " . strcattPMessege.Oldlnform.LaunchTime) VloWrtNAttr(&atr 80*25.0.0 0).
V1oWrtCharStr(PMessege sizeof(PMessege) 15.54 0):
strepytFMessegel myword). strcat(FMessegel." "); strcat (FMessegel PMessege) strepytPMessege ' 1рограммой "): strcattPMessege.Oldlnform ParentNarne).
VIoWrtNAttrt&atr.80*25.0.0,0)
VioWrtCr.arStrtPMessege ;izeof(PMessege), 16 54.0)
strcpy(FMessege2,myword): strcat(FMessege2." "); strcat (FMessege2.PMessege): for(i-0:1<22:1++)
{ Vi oWrtNAttr(8at r.80*25. 0.0.0): VioWrtCharStr("|".1.17.54+i.0):
for (j-0;j<(25000*Speed);J++): }:
/‘Получение времени загрузки программ-потомков*/
0osGetDateTime(8Time):
strcpy(МуТi me.1toa(Time.hours.MyBuffer.10)); strcat(MyT1me.":"):
strcat(MyTime,itoa(Time.minutes. MyBuffer. 10)): strcat(MyTime.":"):
strcat(MyTime.itoa(Time.seconds.MyBuffer.10)): strcat(МуТ1 me.":").
strcat(MyTime.itoatTime.hundredths.MyBuffer.10)): strcpy(Newlnform LaunchTime.MyTime):
strcpy(Newlnform.ParentName.myword): Newlnform.Number=l:
strcpy(PMessege."Завершена в "): strcat(PMessege.MyTime):
VioWrtNAttr(8atr.80*25.0.0.0):
VioWrtCharStr(PMessege.sizeof(PMessege).18.54 0):
strcpy(FMessege3.myword): strcat(FMessege3.' *):
strcat (FMessege3.PMessege)
/♦Ожидание доступности записи в файл и немедленная запись в файл*/
do{ DosWaitEventSemfFileSem.-1):
rc=DosResetEventSem(Fi1eSem.8BytesWritten);
} while (rc!-0):
OosWr ite( Fi 1 eOpen. (PVOID)&FMessegel. sizeof (FMessegel).8BytesWri tten):
OosWrite(Fi1eOpen.(PVOI0)8FMessege2 sizeof(FMessege2) &BytesWritten).
DosWritetFil eOpen.(PV0ID)8FMessege3.sizeof(FMessege3),8BytesWritten).
OosPostEventSem(FileSem):
/♦Алгоритм прохождения точки 4*/
do { OosWaitEventSem(PointlSem.-l):
rc-DosResetEventSemtPoint1Sem.8BytesReaden)
} while (rcl-0):
OosPostEventSemtPoint3Sem);
DosOueryEventSemtPoint3Sem.8BytesWritten):
DosPostEventSem(PointlSem):
if (BytesWritten==4)
( /‘Запись сообщения в Pipe: имя предка(Ргодг_А) и время запуска программ*/ re- OosWrite(WriteHandle.(PV0ID)8NewInform.sizeof(Newlnform).8ВуtesWritten): if (rc!=0) { HaltSystem("(DosWrite)".rc): }
/‘Создание семафора ожидания инициализации ресурсов потомками*/
rc - DosCreateEventSem("\\SEM32\\EXITSEM4" 8ExitSem4. DC_SEM_SHAREO. 0):
if (rc'-O) { HaltSystemt"Create Sem (ExitSem4) "rc):}
/‘Запуск программ-потомков*/
rc-0osExecPgm(Fai 1Fi1eb.sizeof(Fai1Fi leb).1.
Argument.0.8ResCodeb."progr_k.exe"):
if (rc!-0) (HaltSystemt"(0osExpcPgm)",rc):)
/‘Ожидание инициализации ресурсов потомками*/
DosWaitEventSem(ExitSem4. 1).
DosCToseEventSem(ExitSem4):
}
return(O);}
Какую роль выполняет лексический анализ в процессе компиляции? Как мо- iyi быть связаны между собой лексический и синтаксический анализ?
Компилятор можно построить, не используя лексический анализатор. Объясните, почему все-таки подавляющее большинство современных компиляторов используют фазу лексического анализа.
Постройте общую схему работы И1псрпрстатора. В чем се отличие от общей схемы работы компилятора, приведенной в этом пособии?
Постройте общую схему работы компилятора с языка ассемблера. Какие особенности присущи данному компилятору? Какие фазы компиляции в нем могут отсутствовать?
От чего зависит количество проходов, необходимых компилятору для построения результирующей объектной программы на основе исходной программы? Как влияют па необходимое количество проходов синтаксис исходного языка программирования, семантика этого языка, архитектура целевой вычислительной системы?
Почему в глобальной сети Интернет используются в основном языки программирования, предусматривающие интерпретацию исходного кода? Какие проблемы проистекают из этого ({такта?
Почему для языка ассемблера, который имеет простую (часто — регулярную) грамматику, чаще всего используется двухпроходпып компилятор? Что мешает свести компиляцию исходного кода на языке ассемблера в один проход?
Зачем в описании макрокоманды «#definc f2(a) ((а) + (а))» параметр «а» взят в скобки, хотя операция этого не требует?
Что такое таблица идентификаторов и для чего она предназначена? Какая информация может храниться в таблице идентификаторов?
Какие цели преследуются при организации таблицы идентификаторов? Исходя из каких характеристик оценивается эффективность того или иного метода организации таблицы?
Какие существуют способы организации таблиц идентификаторов?
Что такое коллизия? Почему она происходит при использовании хэш-фупк- ций для организации таблиц идентификаторов? В чем заключаются преимущества и недостатки метода цепочек?
Метод логарифмического поиска позволяет значительно сократить время поиска идентификатора в таблице. Однако он же значительно увеличивает время на помещение нового идентификатора в таблицу Почему тем не менее можно говорить о преимуществах этого метода по сравнению с поиском методом прямого перебора?
Проблемы создания хорошей хеш-функции связаны с ограниченной разрядной сеткой компьютера и, следовательно, ограниченным размером доступного адресного пространства Но, как сказано в первой части данного учебного пособия, размер адресного пространства можно увеличить, используя внешние накопители данных (прежде всего, жесткие диски) и механизм виртуальной памяти. Почему эти возможности не используются компиляторами при орган11зации хэш функций?
Список литературы
Абрамова И. А. и др. Новый математический аппарат для анализа внешнего поведения и верификации программ — М.: Институт проблем управления РАН, 1998. - 109 с.
Александров Е. К., Рудня Ю. Л. Микропроцессор 80386: как он работает и как работают с ним: Учебное пособие / Под ред. проф. Д. В. Пузапкова. — С-Пб.: Элмор, 1994. — 274 с.
Эпплман Д. Win32 API и Visual Basic. — СПб.: Питер, 2001.
Кэнту М. Delphi 5 для профессионалов. — СПб.: Питер, 2001
Афанасьев А. Н. Формальные языки и грамматики: Учебное пособие, — Ульяновск: УлГТУ, 1997. - 84 с.
Ахо А., Ульман Дж. Теория синтаксического анализа, перевода и компиляции. — М.: Мир, 1978. — т.1, 612 с. — т.2, 487 с.
Бартеньев О. В. Фортран для студентов. — М.: Диалог-МИФИ, 1999. — 342 с.
Павловская Т. А. C/C++: Учебник. — СПб.: Питер, 2001.
Богумирский Б. С. Руководство пользователя ПЭВМ: В 2-х ч. — С-Пб.: Ассоциация OILCO, 1992. — 357 с.
Бранденбау Дж. JavaScript: сборник рецептов для профессионалов. — СПб.: Питер, 2000. — 416 с.
И. Браун С. Операционная система UNIX. — М.: Мир, 1986. — 463 с.
Бржезовский А. В., Корсакова Н. В., Фильчаков В. В. Лексический и синтаксический анализ. Формальные языки и грамматики. — Л.: ЛИАН, 1990. — 31 с.
Бржезовский А. В., Фильчаков В В. Концептуальный анализ вычислительных систем. - СПб.: ЛИАН, 1991. - 78 с.
Вирт Н. Алгоритмы и структуры данных. — М: Мир, 1989. — 360 с.
Волкова И А., Руденко Т. В. Формальные языки и грамматики. Элементы теории трансляции. — М.: Диалог-МГУ, 1999. — 62 с.
Гончаров А. Самоучитель HTML. — СПб.: Питер, 2000 — 240 с.
Гордеев /1. В., Молчанов А. Ю. Применение сетей Петри для анализа вычислительных процессов и проектирования вычислительных систем: Учебное пособие. - Л.: ЛИАП, 1993. - 80 с.
Гордеев А В., Никитин А В , Фильчаков В. В. Организация пакетов прикладных программ: Учебное пособие. — Л.: ЛИАП, 1988. — 78 с.
Гордеев А. В., Кучин II В Проектирование взаимодействующих процессов в операционных системах: Учебное пособие — Л.: ЛИАП, 1991. — 72 с
Гордеев А. В. Решетникова Н. Н„ Соловьев А. П. Дисковая операционная система реального времени. — СПб.: ГААП, 1994 — 44 с.
ГордеевА. В., Штепен В. А. Управление процессами в операционных системах реального времени: Учебное пособие. — Л : ЛИАП, 1988. — 76 с.
Григорьев В. Л. Микропроцессор i486. Архитектура н программирование (в 4-х кн.). — М.: Гранал, 1993.
Грис Д. Конструирование компиляторов для цифровых вычислительных машин. — М. Мир, 1975. — 544 с
Гудмэн Дж. Секреты жесткого диска — Киев: Диалектика, 1994. — 256 с
Д. Ван Тассел. Стиль, разработка, эффективность, отладка и испытание программ. — М : Мир, 1985.332 с
Дворянкин А. И. Основы трансляции: Учебное пособие. — Волгоград: ВолгГТУ, 1999 - 80 с.
Дейкстра Е. Взаимодействующие последовательные процессы//Языки программирования (под ред. Ф. Жепюи) — М.: Мир 1972.
Дейтел Г. Введение в операционные системы. В двух томах/Пер. с англ. Л. А. Теп- лицкого, А. Б Ходулсва, В. С. Штаркмана под ред. В. С Штаркмана. — М Мир, 1987.
Джордейи Р. Справочник программиста персональных компьютеров типа IBM PC, XT и АТ/Пер. с англ. — М.: Финансы и статистика, 1991. — 544 с.
Донован Дж Системное программирование. М Мир, 1975.— 540 с.
Дунаев С. UNIX System V. Release 4.2. Общее руководство. — М.: Диалог-МИФИ, 1995. - 287 с.
Жаков В И, Коровинский В. В, Фильчаков В В. Синтаксический анализ и генерация кода. - СПб.: ГААП, 1993. — 26 с.
Жаков В И. Корсакова II. В., Никитин А В., Фильчаков В. В Структуры данных: Учебное пособие. — Л.: ЛИАП 1989. — 76 с.
Немнюгин С., Нерколаб Л. Изучаем Turbo Pascal. СПб.: Питер, 2000.
Калверт Ч. Delphi 4. Энциклопедия пользователя. — Киев: ДиаСофт, 1998.
Кастер X. Основы Windows NT н N ITS /Пер. с англ. — М.: Изд. отдел «Русская редакция» ТОО «Channel Trading Ltd.», 1996.440 с.
Кейлиигерт П Элементы операционных систем. Введение для пользователей/ Пер. с англ. Б. Л Лисса и С. П Тресковой. — М.: Мир, 1985. — 295 с.
Кейслер С. 11роектнрованпс операционных систем дня малых ЭВМ. — М Мир, 1986 - 680 с.
Керниеаи Б., Пайк Р. UNIX — универсальная среда программирования. — М.: Финансы и статистика, 1992. — 420 с.
Клочка В. И. 'Геория вычислительных процессов и структур: Учебное пособие. — Краснодар: Изд-во КубГТУ, 1999. — 118 с.
Коваленко И. II. QNX: Золушка в семье UNIX — http://www.lgg.ru/~nigl/QNX/ doc/Kovalenko_cinderella.html, 1995.
Компанией, Р. И., Манъков Е. В., Филатов II. Е. Системное программирование. Основы построения трансляторов/Учсбпое пособие для высших н средних учебных заведений. СПб.: КОРОНА принт, 2000. — 2 56 с.
Концептуальное моделирование информационных систсм/Под ред. В. В. Фильчакова. - СПб.: СПВУРЭ ПВО, 1998. - 356 с.
Краковяк С. Основы организации и функционирования ОС ЭВМ. — М.: Мир, 1988. - 480 с.
Льюис Ф. и др. Теоретические основы построения компиляторов. — М.: Мир, 1979. - 483 с.
Майерс Дж. Надежность программного обеспечения. — М.: Мир, 1987. — 360 с.
Мельников Б. Ф, Подклассы класса контекстно-свободных языков. — М.: Изд- во МГУ, 1995. - 174 с.
Микропроцессоры 80x86, Pentium: Архитектура, функционирование, программирование, оптимизация кода/В. М. Михальчук, А, А. Рондо, С. В. Рыжиков. — Мп.: Битрикс, 1994. — 400 с.
Мурата Т. Сети Петри: Свойства, анализ, приложения (обзор) // ТИИЭР, 1989, № 4. С. 41-85.
Мчдник С., Донован Дж. Операционные системы. — М.: Мир, 1978. — 792 с.
Непомнящий В. А., Рякии О. М. Прикладные методы верификации программ/ Под ред. А. П. Ершова. — М.: Радио и связь, 1988. — 256 с.
Нортон П. Персональный компьютер фирмы IBM и операционная система MS-DOS /Пер. с англ. — М.: Радио и связь, 1992. — 416 с.
Нортон П., Гудмен Дж. Внутренний мир персональных компьютеров. Изд. 8-е. Избранное от Питера Нортона /Пер. с англ. — К.: Диасофт, 1999. — 584 с.
Обухов И. QNX: Как надо делать операционные системы / PC Week RE № 7, 1998. С. 58-59.
Озеров В. Советы по Дельфи (Версия 1.3.1 от 1.07.2000) — http://www. webmachme.ru/delphi.
Олифвр И. А., Олифер В. Г. Сетевые операционные системы. — СПб.: Питер, 2001.
Операционная система СМ ЭВМ РАФОС: Справочник /Иод ред. В. П. Семика — М.: Финансы и статистика, 1984. — 207 с.
Операционные системы — от PC до PS/2 / Ж. Фодор, Д. Бонифас, Ж. Танги. Пер. с франц. — М.: Мир, 1992. — 319 с.
Орловский Г. В. Введение в архитектуру микропроцессора 80386. — СПб: Сеанс-Пресс Ltd, Инфокон, 1992. — 240 с.
ОС QNX: Обзор системы. — http://www.lgg.ru/~nigl/QNX/doc/about_qnx.html.
Red Hat Lintix 6.2/Под ред. А. Пасечника — СПб.: Питер, 2000.
Петзолд Ч. Программирование для Windows 95 /Пер. с англ. СПб.: BIIV, 1997. В 2-х т.
Петруцос Э„ Хау К. Visual Basic 6 п VBA - СПб. и др.: Питер, 2000. — 425 с.
Питерсон Дж. Теория сетей Петри и моделирование систсм/Пер. с англ. — М Мир, 1984. - 264 с.
Полетаева И. А. Методы трансляции: Конспект лекций. — Новосибирск: Изд- во НП'У, 1998. Ч. 2. - 51 с.
Пратт Т., Зелковиц М. Языки программирования: реализация и разработка. — СПб.: Питер, 2001.
Рассел Ч., Кроуфорд Ш. UNIX и Linux: книга ответов. — СПб.: Питер, 1999. — 297 с.
Рейчард К., Фостер -Джонсон Э. UNIX: справочник. — СПб.: Питер, 2000. — 384 с.
Ресурсы Microsoft Windows NT Workstation 4.0 /Пер. с англ. — СПб. BHV, 1998. - 800 с.
Робачевский А. М. Операционная система UNIX. — СПб.: BHV, 1997. — 528 с
Ренаткин Д„ Федоров A. Borland Pascal в среде Windows. — Киев: Диалектика, 1993. - 511 с.
Рудаков П. И., Федотов М. А. Основы языка Pascal: Учебный курс. — М.: Радио л связь: Горячая линия — Телеком, 2000 — 205 с.
Рудаков П. И„ Финогенов К. Г. Программируем на языке ассемблера IBM PC. Ч 3. Защищенный режим — М.: Энтроп, 1996. - 320 с.
Серебряков В. И. Лекции по конструированию компиляторов. — М.: МГУ, 1997. - 171 с.
Соловьев Г. Н., Никитин В. Д. Операционные системы ЭВМ: Учебное пособие. — М.: Высшая школа, 1989. — 255 с.
Страуструп Б. Язык программирования Си++. — М.: Радио и связь, 1991. — 348 с.'
Стрыгин В. В., [Царев Л. С. Основы вычислительной, микропроцессорной техники и программирования. — М.: Высшая школа, 1989. — 478 с.
Студнев А. Boot-менеджеры — кто они и откуда?//Ву1с Россия, 1998, № 4. С. 70-75.
Тревеннор А. Операционные системы малых ЭВМ /Пер. с англ. А. Г. Васильева. — М.; Финансы и статистика, 1987. — 188 с.
Уинер Р. Язык Турбо С. — М.: Мир, 1991. — 380 с.
Уокерли Дж. Архитектура и программирование микро-ЭВМ. — М. Мир 1984. - 486 с.
Федоров В. В. Основы построения гране ляторов: Учебное пособие. — Обнинск: ИАТЭ, 1995. - 105 с.
Финогенов К. Г. Основы языка ассемблера - М Радио и связь, 1999. — 288 с.
Фролов А. В., Фролов Г. В. Защищенный режим процессоров Intel 80286, 80386, 80486. Практическое руководство но использованию защищенного режима. — М Диалог-МИФИ, 1993. — 240 с.
Фролов А. В., Фролов Г. В. Операционная система OS/2 Warp. — М.: Диалог- МИФИ (Библиотечка системного программиста; т. 20). 1995. — 272 с.
Фролов А. В., Фролов Г. В. Программирование для IBM OS/2 Warp: 4.1. — М.: Диалог-МИФИ, 1996. — 288 с
Фролов А. В., Фролов Г. В. Программирование для Windows N4'. — М.: Диа- лог-МИФИ (Библиотека системного программиста; т. 26, 27), 1996.
Хоар Ч. Взаимодействующие последовательные процессы. — М . Мир, 1989. — 264 с.
Цикритлис Д. Бернстайн Ф Операционные системы/Пер с англ. В Л. Ушковой и И. Б. Фейгсльсон. — М.: Мир, 1977. — 336 с.
Чернышов А. В. Инструментальные средства программирования из состава ОС UNIX н их применение в повседневной практике. — М.: Изд-во МГУП, 1999. - 191 с.
Шапошников И. В. Интернет-программирование. — СПб : БХВ — Санкт-Петербург, 2000. — 214 с.
Шоу А. Логическое проектирование операционных систем /Пер. с англ. В. В. Макарова и В. Д. Никитина. — М : Мир, 1981. — 360 с.
Юров В Assembler: Учебник. — СПб. и др.: Питер, 2000 — 622 с.
Ющенко С. В. ОС QNX — реальное время, реальные возможности /Мир ПК, № 5 -6, 1995.
Windows 2000 для системного администратора. Microsoft Windows 2000 Server и Professional. Русские версии / Под общ. рсд. А. Ы Чекмарева и Д Б. Вишнякова. — СПб.: BHV, 2000 — 1056 с.
OS/2 Warp изнутри. В 2-х томах. / М. Мипаси, Б. Камарда и др. Пер. с англ. С. Сокорповой. — C-Пб.: Питер, 1996. Т. 1 — 528 с. Т. 2 — 512 с.
Understanding Windows NT POSIX Compatibility by Ray Cort Microsoft Corporate Technology Team, Created: May/June 1993.
http//www.borland.ru/.
http://www.corba.ru/.
http//www.gnu.org/.
http://java.sun.com/
http://www.linux.zp.ua:8100/philosophy/philosophy.ru.html.
http://www.microsoft.com/RUS/lnternet/intranets.html
104 http- 7www.microsoft.com/RUS/mssol99/prod/dev/vc.htm
105 http ywww microsoft.com/RUS/mssol99/prod/dev/vb.htm.
http:/ www microsoft.com/rus/net/.
http 7www.perl.org/.
http://www php.net/.
Алфавитный указатель
А
ACL (access control list, список прав доступа) 170
API (application program
interface), 207, 658, 667
Application server, 671
ASP, 691
c
CGI-приложение 690
CPU
throughput (пропускная способность процессора), 61
utilization, 61
CS (critical section), критическая секция, 226
D
DPB (disk parameter block, блок параметров диска), 160
E
EAs (extended attributes, расширенные атрибуты), 167
EGIL) (effective group ID) 314
EPL (effective privilege level), 115
EU1D (effective user ID), 314
F
FAT (file allocation table), 156
FCFS (first come — first served), 55
FixPak — пакет исправлений
и обпоилс инй, 327
FSGID (file system access group ID), 314
FSUID (file system access user ID), 314
G
GDT (global descriptor table), 105
CDTR (global descriptor table), 103
GID (group identifier), идентификатор группы, 305
н
HPFS (high performance file system), 167
I
IDT (interrupt descriptor table), 123
IDTR (interrupt descriptor table register), 120
IEEE, 213
IES (installable file system) 174. 178
Internet, 671, 681, 685, 687, 688, 695
1NTR (interrupt request, запрос
на прерывание), 121
IRQ (interrupt request, запрос
на прерывание), 121
J
JCL (job control language), 56
JFS (journaling file system), 333
L
LDT (local descriptor table), 105
LFU (least frequently used), 79
LRU (least recently used), 79
LVM (logical volume manager), 333
M
MFT (master file table), 181
Middleware, 671
MPE (MultiProcess Executing), 214
P
PDE (page directory entry), 109
PHP, 691
P1D — Process Identificator, 31
POSIX, 179, 213
PTE (page table entry), 109
R
RAD (Rapid Application Development), 659, 675, 678
request (запрос), 252
release (освободить), 239, 252
RPC (remote procedure call), 205, 323
RPL (requested privilege
level), 106, 114
RR (round robin), 57
RTL (run time library), 207, 209
s
secondary MBR — SMBR, 153
SJN (shortest job next), 56
SOM (system object model), 329
SRT (shortest remaining time), 56
SSTF (shortest seek time — first), 145
super-FAT, 147
system software, 17
T
TSS (task state segment), 102
u
UCB (unit control block), 136
UID (user identifier) — идентификатор пользователя, 305
V
VDM-матипа, 195
Virtual Memory Manager (VMM) — диспетчер памяти, 97
VMS (virtual memory system), 214
w
wait (ожидать), 239, 252
Web-страннца, 678, 688, 691
динамическая, 689, 691, 693, 695
WinAPI, 213
WPS (workplace shell), 331
A
Аванцепочка, 475
аварийное замещение (секторов). 173
Адрес
абсолютный, 597, 663
виртуальный, 68
возврата, 604, 606
линейный, 107
относительный, 597, 602, 663
активная сессия (foreground session), 177
активное ожидание, 235
Алгоритм
к-ирсдсказывающнй, 472
банкира Дейкстры, 289
Деккера, 231
Кока—Я11 сера— Касам и, 456
лексического анализатора, 570
метода цепочек, 561
преобразования
в дерево операций, 580
в обратную польскую запись, 621
в объектный код, 624
в триады, 628
раз м с 1 це и и я э л см с 11 та
с рехэшированием, 558
расповпавателя
Е1.(1)-трамматик, 475, 485
БЦк)-грам.матик, 475
ЬК(к)-грамматнк, 491
рекурсивного спуска, 463, 465
с подбором альтернатив, 445 459, 463, 472, 474
сдвиг-свертка, 450, 452, 459, 486, 490, 492, 494, 497, 501, 505, 511, 515
устранения левой рекурсии, 438
Эрли, 457
Алфавит 348, 355, 377, 391, 397, 404-405,
409,411,418
магазинных символов, 418
Архитектура
вычислительной системы, 648, 663, 665, 681, 687
клиент—сервер, 669-670, 675, 678, 680,
689, 692, 694
трехзвенпая, 671
трехуровневая, 670-671, 675, 679, 680.
689, 692, 694
Ассемблер, 542, 614, 632
атрибуты файла, 165, 184
Б
Базовый тип данных, 597
Базы данных 669, 675, 678
безопасное состояние, 285
Библиотека
времени выполнения, 207, 209
подпрограмм и функций, 596, 612, 655,
662, 665, 677
CLX, 686
MFC, 679, 680
VBRun, 678
VCL, 675, 677, 686
Xlib, 684
Библиотека (продолжение) динамически зафужаемая. 612, 663 666-667, 669, 678, 690 языка, 544, 627, 665-666
Borland Pascal, 673
бинарное дерево (B-tree), 171, 184 бит
обращения, 80, 109
присутствия, 77, 109
блокировка памяти, 227
браузер (browser), 671, 689, 695
буфер сообщении, 253
В
вектор прерывания, 119
взаимное исключение, 225
взаимодействующие процессы (cooperating processes), 222
виртуальная машина, 194
виртуальное адресное
пространство, 68
внесистемный зафузчик (noii-system bootstrap), 149
Внутреннее представление
нрофаммы, 536, 579, 581, 589, 591, 596, 612,615,619, 623, 632, 637
обрат фя польская запись. 541. 615.
619, 620
тетрады, 615-617, 619
триады, 615, 618-619, 628, 637, 639
время ожидания (rotational
latency), 167
Вь зов
fork, 307
inline-функции, 644
удаленных процедур, 323
Вычисление выражений в обратной польской занпсп, 620
вычислитель! i ые процессы взаимодействующие, 222 независимые, 222 параллельные, 222 последовательные, 24
Г
Генератор языка, 537
Генерация кода, 537, 548, 597, 612, 614,
623, 627, 630
Гипертекст, 688
главная загрузочная запись (master boot record), 149
глобальное адресное пространство, 105
Грамматика, 353, 372, 539, 613 арифметических выражений, 372, 374, 432, 439, 445, 452, 479, 502, 512, 581-582, 621 без скобок, 468 десятичных чпесм с плавающей точкой, 406 контекстно-зависимая, 361, 366 контекстно-свободная. 362, 365-366, 420, 423, 429, 449, 455, 457, 459, 463. 465, 468, 473, 519, 578-579, 582
LL(k), 472-475, 479, 488, 521, 523 LR(k), 487, 489, 492, 494, 497, 508, 521,523
в нормальной форме Грейбах, 440 в нормальной форме
Хомского, 433-434, 437, 456 левоапализируемая, 522 леворекурсивная, 438-440, 443, 474 пелеворекурсивиая, 445 неоднозначная, 518 orpai 111 ч ci 11 юго 11 рано го контекста, 521 однозначная, 458, 473. 488. 499, 509 операторная, 508 операторного
предшествования, 498, 509, 512. 518, 622
пополненная, 489, 492, 494 правоаналнзируемая, 522 11раворскурсииная, 438 преобразован не, 426, 434 435, 4.38. 442, 450, 479, 496
приведенная. 427, 434-4.35, 437 простого предшествования. 498, 508,518
расширенного предшествования, 498 слабого предшествования, 498 смешанной стратегии предшествования, 498 леволииейиая, 362, 366, 387-388, 403-406, 409, 410, 571
автоматная, 387-388, 409-410 неоднозначная, 373, 376 исукорачнвающая, 361,366 однозначная, 373-374, 414 праволинепная, 362, 366, 387, 403 автоматная, 387 регулярная, .362, 366, 387-388, 390, 403-404, 408, 410, 414, 476, 544, 568, 571 578,689
Грамматика (продолжение)
формальная, 354, 361, 382
। юлоч! 1сл ei । пых koi re Tai гт
дд# языка С, 571
целых десятичных чисел, 355-356, 365, 368
языка программирования, 354
д
дед-ток (deadlock — смертельное
объятие), 269
Дерево
вывода, 370, 372, 448, 453, 472.
483-484, 487 506-507, 516-517,
579 581.615
операций, 580 581, 614, 623, 626, 628
дескриптор
задачи, 33, 106
прерывания, 123
процесса, 29, 31
сегмента, 77 104
страницы, 83, 109
шлюза, 116
диспетчер виртуальной памяти 97
Дисплей памяти процедуры
(функции). 603
динамический, 604, 606
статический, 604
стековая организация. 604. 606 607.
609
дисциплина обслуживания, 28
Дополнение программы неявными
функциями, 589, 5Д1
дорожка (track), 148
Ж
Жизненный цикл программного
обеспечения, 655, 659
журналирование файловых операций, 179
3
Заголовочные файлы, 666
загрузочная запись (boot record), 156
Зшрузчпк, 656, 658, 663
задача (task) 24, 34, 51, 97
диск-резиде!пт тя, 32
ОЗУ-резидептная, 33
запрос на ввод/вывод, 132
зарезервированная память (reserved
memory), 98
зона с каталогами (directory
band), 171
И
Идентификация лексических элементов, 537. 544 594-595, 665
Инвариантные операнды. 642
Индуктивная переменная, 646 инициативное устройство, 30, 132 Интегрированная среда разработки, 657, 660, 664. 674, 676, 684 685
Интернет, 542
Интернет-клиент, 687-688, 692
И нтерпет-нрограммированпе, 688-689
Ингернет-ссрвер, 687 688 691
Интерпретатор, 532, 585 540 541,620, 678, 681, 683, 687-688 691, 693 интерфейс
СО. 690
ISAP1. 690
обмена данными, 670, 671
пользовательский, 23, 657, 665, 671
графический, 657, 659, 684, 688
на основе среды X Window, 684 прикладного
ирограм м прова) 11ш, 207 программный, 23
Информация о типах во время выполнения, 610 исключение, 130 липших операций для линейного участка, 639
Исходная программа, 530, 532, 536-537. 540 545, 547 566. 568, 576, 578, 580, 589, 597, 600 610 612, 614, 616 630, 633, 660. 668, 675, 687
Итерация цепочки, 348, 397, 424
К
квант времени (timeslice), 57
кластер, 156
Клиентская часть приложения 670-671
676, 678, 680
Коллизия, 556, 562-563
Команда
B PS (BTR.BTC). 234
языка ассемблера, 543 командные файлы (shell
scripts), 305
Командный язык Makefile 656-657, 662, 683
коммутатор задачи (task gate), 124, 126 перехвата (trap gale), 124 прерывания (interrupt gate), 124
Как могут быть скомбинированы различные методы организации таблиц идентификаторов?
Чем различаются таблица лексем и таблица идентификаторов? В какую и этих таблиц лексический анализатор не должен помещать ключевые слова, разделители и знаки операций?
Какие проблемы необходимо решить при построении сканера па основе конечного автомата? Чем отличается функционирование лексического анализатора в составе компилятора от работы обычного КА?
В чем различия между деревом вывода и деревом операций, если и то и другое получается в результате синтаксического разбора?
Задачи
В программе на языке С макрокоманда Incl(a,b) и функция 1пс2(а,Ь), выполняющие действие а+2*Ь, описаны следующим образом:
^define incl(a.b) ((а) + (b) - (Ь))
int Inc2(lnt a. int b) { return a ♦ b ^ b: }
к каким результатам приведут следующие вызовы для целочисленных переменных i, j и к:
>-1псШ.1): k-Incl(i.j). i.-Incin.j+l); k-lncKi j++). k-IncHi** lnc2(j.D): f k-lnc2(i,i): k-I<t2(i J) k=Inc2(i.j< . k-Inc2U j+<-): k=lnc2(i**.l Kj.D):
В каких из перечисленных случаев будет получен логически неверный результат?
Изменится ли результат, полученный при выполнении макрокоманды lncl(a,b) из предыдущей задачи, если ее определение будет дано следующим образом: define Incl(a.b) ((а) ^ (2*(Ь)))
Приведет ли это к логически правильным результатам во всех вышеприведенных случаях?
Будет ли верным следующее определение макрокоманды:
^define Incl(a.b) {(а) * (2*Ь))
Если оно содержит ошибку, укажите, в каком случае выполнение данной макрокоманды даст неверный результат
Напишите программу на языке Pascal или С, реализующую метод логарифмического поиска в упорядоченном массиве строк В качестве исходных данных' для заполнения массива возьмите любой текстовый файл, считая, что все слова в нем являются идентификаторами.
Напишите программу на языке Pascal или С, реализующую метод построения бинарного дерева. Используйте динамические структ)ры данных для организации дерева. В качестве исходных данных для заполнения дерева возьмите любой текстовый файл, считая, что все слова в пом являются идентификаторами. Проверьте эффективность метода на случайных текстовых файлах, а также попробуйте в качестве источника данных файл с упорядоченным текстом.
Компилятор, 345, 353, 382, 454, 521, 524 531, 534-535, 539, 541, 54G, 549, 554, 560, 563-564, 569-570, 576-577. 579-581 588, 591, 594, 596-597, 600- 607, 609 615, 619-620, 623, 627, 630 -633, 635,641-642,646-649 655,657 658, 661, 663, 666, 668, 676, 683, 687-688 691, 693
генерация предупреждений, 593, 614 двухпроходны® 540 м ногсч । роход! । ый, 540 однопроходный, 540, 673 ресурсов, 658, 662, 673, 684 с языка ассемблера, 544, 546
Компонентная модель, 675
Компоновщик, 655, 658, 662, 664, 666, 683, 691
Конечный автомат (КА), 382, 391, 403, 407-409, 411, 568, 570, 576
граф переходов, 392, 395, 412 детерминированный, 392-393, 395, 412 572
полностью определенный, 572 минпмп мщпя, 395 недетерминированный, 392, 412 ноЖюстыо определенный, 391
Конкатенация цепочек, 348, 397, 424, 614 623
конкурирующие процессы, 223 контекст задачи, 31
копцепрпя
.NET, 681-682
концепция виртуальных задач, 343 корневой каталог (root
directory), 156 коэффициент мультипрограммирования, 73 критическая секция, 226
Л
Лексема (лексическая единица), 351, 382, 537, 564, 579, 589, 596-597, 599 600, 668
выделение границ, 568
Лексический анализ, 382 537, 544, 548, 564, 566, 568, 570, 576, 579, 595 на лету, 668, 674 непрямой, 569 параллельный, 566, 568 носледоватстьный, 566, 568 прямой, 569
ленивая запись (lazy write), 143, 175
Линейный участок, 645
программы, 634
Лишняя операция, 635
логический диск. 149
локальное адресное пространство, 102. 105
м
Макрогенератор, 546
М акрогеис ра ц 11 я, 54 5
Макрокоманда, 545 -548
Макроопределение, 545-547
с параметрами, 546
сложное, 547
М акроподста в он ка, 545
Макропроцессор, 546
Макрорасш и реп не, 545
Матрица предшествования, 504, 508,
510-511. 514, 518
Менеджер
загрузки, 149, 154
памяти, 603, 673, 693
Метасимвол грамматики, 358, 468
метафайл, 180
Метод
рекурсивного спуска. 464-465, 467, 473
цепочек, 561, 563
механизм шлюзования, 115
микроядро, 199
Мнемокод, 533. 542
М11 огоадpec 11 ы й код
с неявно именуемым результатом, 615, 617
с явно именуемым результатом, 615-616 многопоточное гь, 34 многопоточный (multi-threaded), 203 Множество
FIRST(l), 476-477, 479, 482
FIRST(k), 474-475
FOLLOW(l), 476, 478, 480, 482
FOLLOW(k), 474-475
крайних левых символов, 500, 503, 510, 512
крайних левых wpMiiiKMibiibix символов, 510, 513
крайних правых символов, 500, 503, 510, 512
крайних правых герминальных символов, 510, 513
модель
клиент-сервер, 201
Холта, 270
модуль, 191
монитор Хоара, 250
МП-антомаг, 381, 418, 441, 443, 455, 463, 467, 472, 475-476, 482, 486, 522, 588 детерминированный, 381, 423, 522 недетерминированный, 441, 444, 449 расширенный, 420, 449-450, 492, 494, 497, 501. 511, 516, 522
М11-преобразователь, 578
мульт!1программирование, 27 мьютекс (mute х), 242
н
независимые процессы (independing processes), 222
ненадежное состояние, 289
Область
видимости, 595
памяти, 597, 600, 602, 604, 606, 663, 693
выравнивание границ, 599
Обнаружение п локализация ошибок, 521,
536, 614, 657, 662
оболочка (командный интерпретатор, shell), 304-305
Обьекшая программа, 531, 612, 616, 624,
631-632, 644
Объектный
код, 541, 580, 616, 619, 623, 627, 631,
635-636, 641-642, 649, 665, 687, 691
файл, 531, 656, 662, 665 673 676, 691
оверлейная структура, 71
операционная
система, 17
среда, 22, 112
Оптимизация кода, 538, 540, 544, 616, 620,
627,630-631 633
для вызовов процедур и функций, 633,
643
для линейных участков
программы, 633
арифметические
преобразования, 636
исключение лишних операций, 635
перестановка операций, 636
свертка объектного кода, 635, 637
удаление бесполезных присваиваний, 634 для логических выражений, 633 641 для параллельного выполнения
операций, 649
Оптимизация кода (продолжение) для циклов, 633, 645
вынесение инвариантных вычислений, 645
замена операций с индуктивными переменными, 645 646
слияние и развертывание циклов, 645, 647
критерии оптимизации, 631
машинно-зависимая, 632, 633, 648
машинно-независимая, 632-633
Оранжевая книга, 197
ОС UNIX, 577, 582, 682
ОСРВ (операционная система реального времени), 49
QNX, 201, 214, 334
Отладка программы, 596, 633, 664
Отладчик, 656, 664 665
Отношения иредшоствовапня, 498-499,
501,509, 511
очередь сообщений (queue), 257, 320
ошибка страницы (page fault), 97
п
Память
выдслейая, 98
глобальная, 600, 603-604
динамическая, 544, 602, 604, 606, 611,
673, 693
выделяемая компилятором, 603
выделяемая пользователем, 602
локальная, 595, 600, 603-604
статическая, 544, 602, 611
папка (folder), 178
11 рограмм 11 ый модуль, прнвплс! ировапный, 45
пассивное ожидание, 236
перенаправление
ввода/вывода, 310
плоская модель памяти, 92. 111
Подготовка к генерации
кода, 537, 613
подкачка (paging), 98
Позднее связывание, 610
। юл ьзовате тьс кая мас ка
(файла), 316
поток
(stream), 182, 310
(thread), 33
потомство вершины, 295
почтовый ящик, 254
правила использования шлюзов, 118
Правило грамматики (продукция), 354-355, 357, 361, 376, 388, 390, 409-411, 428, 434, 438, 464, 468, 473, 483, 498, 510, 579, 622
лямбда-правила (пустые), 389, 427, 429, 434, 450, 452, 464, 479, 508 ценные правила, 389, 427, 431-432, 434, 450, 452, 516
использования шлюзов, 117
Преобразования типов, 592
Препроцессор языка, 547 прерывание, 37, 119
внешнее (асинхронное), 39, 121 внутреннее (синхронное), 40, 120 маскируемое, 41, 121 немаскируемое, 121 программное, 40
принцип иерархического выделения ресурсов, 288
Проблема
преобразования грамматик, 367, 375, 426, 459, 488, 499, 509
эквивалентности грамматик, 375, 414, 426, 442, 473, 488
пробуксовка, 84
11рограмма
LEX, 577, 582, 683
YACC, 582, 683
абсолютная двоичная, 69
нативная, 23, 112
резидентная, 74
программный
канал (pipe), 255, 319
модуль
повторно пходимый, 47 реентерабельный, 46 системный, 23 граи пгтый, 192
Проект GNU, 684
производитель — потребитель (produces — consumer), 223
Промежуточный код компиляции, 542, 681, 688, 693
Протокол обмена данными HTTP, 688, 690
Проход компиляции, 539, 616
процедура начальной загрузки (bootstrap loader), 154
процесс (process). 24, 33, 97
внешний, 141
внутренний, 141
легковесный, 33
пул, 225
нерезидентный (paged pool), 98
памяти (шелюгу pool), 98
резидентный (nonpaged pool), 98 пучок (в ориентированном графе), 295
Р
рабочее множество, 85
раздел (partition), 149
активный, 149
первичный (primary partition), 149
расширенный (extended partition), 149
Распределение памяти, 544
Регулярное
множество, 397
режим
супервизора, 113
Распознаватель, 350, 363, 377, 382, 413, 418, 431, 433, 438, 458, 537, 576, 578-579, 582, 588, 622
для языка ассемблера, 544
линейный, 381, 459, 463, 465, 471, 520
ЕЕ(1)-грамматикн, 482, 494
ЕЕ(к)-грамматикн, 475
ЕК(0)-грамматики, 494
ЕК(к)-грамматикн, 490 восходящий левосторонний, 486, 496, 505, 520
нисходящий левосторонний, 474, 496, 520
операторного предшествования, 509. 518 простого предшествования, 505 с возвратом, 441
восходящий, 449
нисходящий, 443-444, 446
табличный, 455-456, 458 управляющая таблица, 475, 490, 492, 494
Распределение памяти 537, 597-598, 600, 601
расширенные атрибуты, 167
регистр задачи (task register), 33, 106
Регис тры процессора
адресация параметров, 606
базовый регистр, 607
обозначение, 543
общего назначения, 648 передача параметров, 610, 643 распределение, 649
регистр стека, 607
регистр флагов, 120
Регулярное
выражение, 398-399, 403-405, 407
множество, 399, 403-404, 407, 413
Редактор
ресурсов, 658
связен, 662
редукция графа повторно используемых ресурсов, 292
режим
виртуальный, 111
защищенный (protected mode), 101
ленивой (отложенной) записи 143, 175
мультизадачный, 26, 48
мультипрограммный, 26, 48
обмена с прерываниями, 133- 134
опроса готовности 133- 134
пользователя, 113, 130
привилегированный, 27
супервизора, 130
Результирующая программа, 521, 530-532
Рекурсивный вызов процедур
и функций, 605-606, 609
Рекурсия в правилах грамматики, 356, 438, 464
ресурс, 25
111псрфейса п рикладпой программы, 658, 669, 676, 680
критический, 222
повторно используемый (reusable
resource), 270
расходуемый (consumable resource), 270
Рсхэшироваине, 558 560
С
сбалансированное двоичное дерево, 171
С вертка объектного кода, 635-636, 638
для линейного участка, 637
своппинг (swapping — замена, замещение), 74
сегмент состояния задачи (task slate
segment), 33, 106
сектор (sector), 148
селектор
дескриптора, 116
сегмента, 105
Семантические соглашения языка, 589,
590
Семантический анализатор, 354, 537, 544,
548, 588-589, 594, 597, 631
семафор, 236
и UNIX, 317
Сентенциальная форма 369, 427
Сервер данных, 670-671, 675 689 приложений, 671, 676, 680, 689 сети Петри, 276 теоретико-множественное представление, 277 сигналы, 317, 337 Скрипт Web-страницы, 691 Символ, 347 бесплодный,427-428 недостижимый, 427, 429 нетерминальный, 354, 371, 388 405, 409-410, 438, 443, 449, 463, 465, 468, 473, 477-478, 498, 503, 508, 510-511, 515, 518, 580-581, 583 левая факторизация, 464, 477 левый контекст, 492 рекурсивный, 437 терминальный, 354, 371 372, 409-410, 443, 449, 474, 498, 509, 568, 583 Синтаксический
анализ, 520, 537, 544, 564, 577-578, 582, 588, 597, 613-614
анализатор; 382, 566 разбор, 577 578, 588, 599, 613, 615, 623 Синтаксическое дерево, 580, 589, 613-616 система
программирования, 21, 534, 542 реального времени (СРВ), 202 управления файлами, 19 146 инсталлируемая, 178
Система программирования, 658, 661-663, 665 666, 668, 670-671, 682
Borland C++ Builder, 676
Borland Delphi 674-675, 677, 686
Borland J Builder, 694
Borland Kylix, 685 686
GNU C++, 685
Microsoft Visual Basic, 678 Microsoft Visual C++, 679-680 Turbo Pascal, 657, 672- 673 в сети Интернет, 687, 690 для ОС UNIX, 683-684 проекта GXU, 685 система подсказок и справок 669 системный вызов ехес( ), 307 Скалярный тип данных, 597 Сканер, 564, 568, 570-571, 576 Сложные структуры данных, 598 Соглашение о вызове и передаче параметров, 609
сотрудничающие процессы, 223
спулинг (spooling), 135
снул-файл, 135
Стек параметров процедуры
(функции), 607, 609
Стиль программирования, 633, 636, 612
страничный
файл. 82
фрейм (page frame), 97
Строка символов, 347
СУБД, 670
СУ компиляция 614, 616, 621-622
супервизор (supervisee), 27
ввода/вывода, 132
прерываний, 42
СУ-перевод, 613-614, 622, 626, 629-630
суперпользователь, 305
т
таблица
RTTI, 610-612, 644
векторов прерываний, 91, 120
виртуальных (логических)
устройств, 137
дескрйпторов прерываний, 123
идентификаторов. 536. 538, 549,
558 -559. 562-563, 589, 597, 613, 615,
665, 668
бинарное дерево, 552
линейный список, 550
метод цепочек, 560, 563
хэш-функция, 554
каталогов таблиц страниц, 104. 109
лексем, 566, 578, 589
оборудования, 136
разделов (partition table), 149
размещения файлов, 156
ешмеп гов, 78
страниц, 83
Текстовый редактор, 655, 660, 665, 668
том (volume), 169, 180
Гонкий клиент, 671
Транслятор, 529. 532, 540, 662, 688
с языка ассемблера, 542, 673
Трансляция адресов, 663
тред (thread), 33
У
уплотнение памяти, 76
упреждающее чтение (read ahead), 143
Уравнение с регулярными
коэффициентами, 399, 401,405-406
уровень
безопасности С2, 97
привилегий, 113
установка тайм-аута, 134
утилиты, 21
учетная завись (account). 304
Ф
Фаза компиляции, 536, 538, 540, 549, 589, 614, 616
файл, 146
подкачки (pagefile.sys) 98
файловая
система, 146
ссылка (file reference), 182
файловый узел (F-node), 170
фиксированное состояние, 296
флагакишиосп! раздела (hoot indicator), 150 фоновая сессия (background session), 177 Форма Бэкуса—Наура. 354, 382, 438, 469,
571
фрагмент файла (extent), 167
фрагментация
памяти, 74
файлов, 158, 172,
функции микроядра, 200
X
хэш-адресация, 554-555, 564
хэширование, 556
хэш-таблица, 560
«эш-фуикция, 554, 556-557, 561, 563
ц
целевая вычислительная система, 207
Целевой символ грамматики, 354-355,
371, 389, 406, 475, 489
Цепочка
вывода, 367, 369-370, 372, 374, 427,
457 458,474,579
левосторонняя, 44.3, 448, 483-484 правосторонняя, 450, 453, 493-494, 496, 506-507, 516-517
рекурсивная, 437
циклическая, 431, 437
символов, 347. 378, 391, 444, 450. 465,
474, 499, 545, 569, 579, 588, 612 пустая, 348
цилиндр (cylinder), 148
ч
Числа зависимости, 639
ш
шлюз (gate), 116, 124
э
экс гепт, 167
Я
ядро (ОС). 192
Язык
HTML, 542, 688
Java Script, 695
REXX, 328
арифметических выражений, 525 ассемблера, 531, 533, 535, 542-543, 545, 578, 596, 600, 607, 612, 615 619, 624, 626-627, 632, 637, 662
машинных кодов, 531, 608, 612, 614-616,619
машинных команд, 681 программирования, 351, 4 25, 489, 521, 533, 535, 541, 547, 565, 568, 582, 588, 594, 597, 600, 607, 609, 624, 632, 642
Borland Pascal, 390, 413, 468, 548, 566, 572, 590-591, 598, 600, 657, 672
С, 465, 547, 548, 565, 569, 571-572, 582, 593, 598, 643, 676, 683, 685
С#, 535, 682
C++, 603, 643-644. 676, 679, 685, 686
FORTRAN, 535, 565, 569, 666
Java, 535, 542, 692
Object Pascal. 603, 675, 686
Язык ^продолжение')
Perl, 691
Visual Basic, 535, 677-678
в глобальной сети Интернет, 688 объектно-ориентированный, 603, 610, 612, 673, 675
семантика, 352, 539, 544, 580, 589, 591, 595, 613, 627
семантические нормы, 589, 592 синтаксис, 423, 474, 537, 543, 547.
568, 578, 580, 596-597
способы определения, 351, 382 формальный, 349, 363, 370, 391
LL, 521, 524, 614
LR, 521, 524, 614
детерминированный, 381, 423-424, 474, 488, 509, 519, 524
контекстно-зависимый, 364, 366, 380, 588
контекстно-свободный, 364, 366, 381, 418, 423, 425 442, 458, 486, 508, 519, 524, 578. 582, 588, 614
лексика, 351
операторного предшествования, 519, 525 простого предшествования, 519, 525
регулярный, 365, 382, 387, 403, 405, 408,413-414,568
семантика, 350, 356, 530
синтаксис, 350, 381
способы определения, 349, 382 четвертого поколения (4GL), 659
Гордеев Александр ВладимировичМолчанов Алексей Юрьевич
Системное программное обеспечение
Главный редактор Е. СтрогановаОтвете!венный редактор И. КорнеевРуководитель проема А. ВасильевЛитературный редактор Е. ВаШинаХудожник Р ЯцкоИллюстрации В. ШендеронаКорректор В. ЛистонаВерстал К). Сергиенко
Лицензия ИД № 05784 от 07.09.01.
Подписано в печать 26.12.02. Формат 7ОХ1ОО7|6.
Уел. и л. 59.34. Доп. тираж 4500экз. Заказ № 2061.
ООО «Питер Принт».
196105, Санкт-Петербург, ул. Благодатная, 67.
Налоговая льготе общероссийский классификатор продукцииОК 005-93, том 2; 953005 - литература учебная
Отпечатано с фотоформ в ФГУП «Печатный двор» им. А. М Горького Министерства РФпо делам печати. телерадиовещания и средств миссовых коммуникаций-
197110, Саикт-Пстсрбур! Чкаловский пр., 15.
А. В. Гордеев
А. Ю. Молчанов
Темы, рассмотренные в книге:
СИСТЕМНОЕ ПРОГРАММНОЕОБЕСПЕЧЕНИЕ
• управление задачами
и ресурсами в операционных системах
• архитектура операционных систем
• интерфейсы прикладногопрограммирования
Базовый курс для студентов, обучающихся в высших учебных заведениях по специальностям направления «Информатика и вычислительная техника», преподавателей и профессионалов, связанных с системным программированием.
Особое внимание в этом учебнике уделяется операционным системам, средам и системам программирования. Его основой стал учебный материал, в течение нескольких лет преподаваемый в Санкт-Петербургском государственном университете аэрокосмического приборостроения.
Помимо общетеоретических вопросов в учебнике описаны конкретные реализации различных системных программ, поэтому он может быть полезен не только студентам, детально изучающим системное программное обеспечение но и тем кто собирается самостоятельно разрабатывать его отдельные компоненты.
СОЛИТЕР
WWW PITER COM
• проектирование параллельных взаимодействующих процессов
• проблема тупиков и методы борьбы с ними
• формальные языкии грамматики
• основные принципы построения трансляторов
• генерация
и оптимизация кода
современные операционные системы и системы программирования
Посетите наш web-магазин: http: /www.piter.com
5. Напишите программу па языке Pascal или С, создающую таблицу идентификаторов с помощью хвш-функций па основе метода простого рехыпировппия. В качестве исходных данных для заполнения дерева возьмите любой текстовый файл, считая, ч то все слова в нем являются идентификаторами. Организуй те программу таким образом, чтобы в ней можно было бы легко подменять используемую хэш-функцию. Подсчитывая чпе ю коллизий и среднее количество срав пений для поиска идентификатора, сравните ретультаты для рал шчных хэш- функцпй. В качестве исходных данных для хэш-функцин можис предложить;
О коды первых двух букв идентификатора;
О коды последних двух букв идентификатора;
О код первой и код последней букв идентификатора,
О коды первой, последней и средней букв идентификатора.
G. Выполните действия, описанные в задаче №5, для таблицы идентификаторов, организованной па основе метода цепочек. Сравните результаты.
В языке C++ возможны два типа комментариев, обычный комментарий, ограниченный символами «/*» и **/"*, и строчный комментарий начинающийся с символов «//» и закап швающинся концом с роки. Постройте сканер, который бы находил и исключал из входного текста все комментарии я зыка C++.
Постройте сканер, который выделял бы из текста входной программы на языке Borland Pascal все содержащиеся в пей строковые кощтаиты и записывал их в отдельный файл. Строковые константы в я ыю Pascal юс .и л из строк, одиночных символов и кодов символов, которые могуз быть объединены между собой знаком «+». Строки ограничены символами ”" (двойные кавычки), одиночные символы — символами " (одинарные кавычки), я коды символов начинаются со знака # (диез) и содержат цифры.
Даны две грамматики (см. задачи к предыдущей главе):
G,({ (.)/.&,-.a], {S.R.T.G.E }. Рн S)
Р.:
S -» TR
R -» X | Ч [ ATR
Т ■-> EG
G —* X | &Е | &FG
l -> -Е | (S) | a
G2({(.)/,&.-,a}. {S.T.E}, P2. S)
Pz:
S —► SAT | T
Т -> Т&Е | Е
Е —> —Е | (S) | а
Постройте для каждой из них цепочку вывода для цепочки символов: «аАа&-а&(аА—а)», постройте деревья вывода для каждой из полученных не почек символов. Преобразуйте полученные деревья вывода в дерево операций. Проделайте то же самое для других цепочек, допустимых в языке, заданном этими грамматиками.
ГЛАВА 14 Генерация и оптимизация кода
Семантический анализ и подготовка к генерации кода
Назначение семантического анализа
Мы уже неоднократно упоминали, что практически вся языки программирования, строго говоря, не являются КС-языками. Поэтому полный разбор цепочек символов входного языка компилятор не может выполнить в рамках КС-языков с помощью КС-грамматик и МП-автоматов. Полный распознаватель для большинства языков программирования может быть построен в рамках КЗ языков, поскольку все реальные языки программирования контекстно-зависимы.
Итак, полный распознаватель для языка программирования можно построить на основе распознана геля КЗ-языка. Однако известно, что такой распознаватель имеет экспоненциальную зависимость требуемых для выполнения разбора цепочки вычислительных ресурсов от длины входной цепочки [6, т. 1|. Компилятор, построенный па основе такого распознавателя, будет неэффективным с точки зрения либо скорости работы, либо объема необходимой памяти. Поэтому такие компиляторы практически не используются, а все реально существующие компиляторы на этапе разбора входных цепочек проверяют только синтаксические конструкции входного языка, не учитывая его семантику.
С целые повысить эффективность компиляторов разбор цепочек входного языка выполняется в два этапа: первый — синтаксический разбор па основе распознавателя одного из известных классов КС-языков; второй — семантический анализ входной цепочки.
му этапу и продолжает рассматривать входной поток символов с того места, на котором остановился сканер;
при неуспешном распознавании выдастся сообщение об ошибке, а дальнейшие действия зависят от реализации сканера — либо его выполнение прекращается, либо делается попытка распознать следующую лексему (идет возврат к первому этапу алгоритма).
Рассмотрим пример анализа лексем, представляющих собой целочисленные константы в формате языка С. В соответствии с требованиями языка, такие константы могут быть десятичными, восьмеричными или шестнадцатеричными. Восьмеричной константой считается число, начинающееся с 0 и содержащее цифры от 0 до 7; шестнадцатеричная константа должна начинаться с последовательности символов Ох и может содержать цифры и буквы от а до f. Остальные числа считаются десятичными (правила их записи напоминать, наверное, не стоит). Константа может начинаться также с одного из знаков + или -, а в конце цифры, обозначающей значение константы, в языке С может следовать буква или две буквы, явно обозначающие ее тип (u. U — unsigned; h, Н — short; 1, L — long).
При построении сканера будем учитывать, что константы входят в общий текст программы па языке С. Для избежания путаницы и сокращения объема информации в примере будем считать, что все допустимые буквы являются строчными (читатели легко могут самостоятельно расширить пример для прописных букв, которые язык С в константах не отличает от строчных).
Рассмотренные выше правила могут быть записаны в форме Бэкуса—Наура в грамматике целочисленных констант для языка С.
G((S. U. U. L. V. D.G((S. U. U. L. V. D.G. X. Q. Z. N), (0...9. x. a...f. и. 1. h. ±}. P. S)
G((S. U. U. L. V. D.
G((S. U. U. L. V. D.
Р:
S ->W -►U -» L ->V -> О ->S ->W -►U -» L ->V -> О ->G± | Z1 | D1G± | Z1 | D1G ->G ->X ->О -►Z -»N -»X ->О -►Z -»N -»Lu GuG1Gh 1 I N1DOZ8XOXa GO GaZx ZO 000 I* ILu GuG1Gh 1 I N1DOZ8XOXa GO GaZx ZO 000 I* IVuZuZ1Zb2 IN2DIZ9XIXbG1GbVuZuZ1Zb2 IN2DIZ9XIXbG1Gb33UI Hu Hl HhI 4 N3 02 08 X2 Xc G2 GcUI Hu Hl HhI 4 N3 02 08 X2 Xc G2 GcZ1 | Z201 | 02NOZ1 | Z201 | 02NO| 0± | U± | LI | VI | W1UhOu01Oh| 5 | 6 | 7 | 8 | 9 |N4 | N5 | N6 | N7 | N8 | N9 |D3 | D4 | D5 | D6 | D7 | D8 | D9 09X3 | X4 | X5 | X6 | X7 | X8 ] X9Xd | Xe | Xf |G3 | G4 | G5 | G6 | G7 | G8 | G9Gd | Ge | GfZ3 | Z4 | Z5 | Z6 | Z7 |03 I Q4 I Q5 I 06 I 07| 0± | U± | LI | VI | W1UhOu01Oh| 5 | 6 | 7 | 8 | 9 |N4 | N5 | N6 | N7 | N8 | N9 |D3 | D4 | D5 | D6 | D7 | D8 | D9 09X3 | X4 | X5 | X6 | X7 | X8 ] X9Xd | Xe | Xf |G3 | G4 | G5 | G6 | G7 | G8 | G9Gd | Ge | GfZ3 | Z4 | Z5 | Z6 | Z7 |03 I Q4 I Q5 I 06 I 07
S ->
W -►
U -» L ->
V -> О ->
S ->
W -►
U -» L ->
V -> О ->
G± | Z1 | D1
G± | Z1 | D1
G ->
G ->
X ->
О -►
Z -»
N -»
X ->
О -►
Z -»
N -»
Lu Gu
G1
Gh 1 I N1
DO
Z8
XO
Xa GO Ga
Zx ZO 00
0 I
* I
Lu Gu
G1
Gh 1 I N1
DO
Z8
XO
Xa GO Ga
Zx ZO 00
0 I
* I
Vu
Zu
Z1
Zb
2 I
N2
DI
Z9
XI
Xb
G1
Gb
Vu
Zu
Z1
Zb
2 I
N2
DI
Z9
XI
Xb
G1
Gb
3
3
UI Hu Hl Hh
I 4 N3 02 08 X2 Xc G2 Gc
UI Hu Hl Hh
I 4 N3 02 08 X2 Xc G2 Gc
Z1 | Z2
01 | 02
NO
Z1 | Z2
01 | 02
NO
| 0± | U± | LI | VI | W1
Uh
Ou
01
Oh
| 5 | 6 | 7 | 8 | 9 |
N4 | N5 | N6 | N7 | N8 | N9 |
D3 | D4 | D5 | D6 | D7 | D8 | D9 09
X3 | X4 | X5 | X6 | X7 | X8 ] X9
Xd | Xe | Xf |
G3 | G4 | G5 | G6 | G7 | G8 | G9
Gd | Ge | Gf
Z3 | Z4 | Z5 | Z6 | Z7 |
03 I Q4 I Q5 I 06 I 07
| 0± | U± | LI | VI | W1
Uh
Ou
01
Oh
| 5 | 6 | 7 | 8 | 9 |
N4 | N5 | N6 | N7 | N8 | N9 |
D3 | D4 | D5 | D6 | D7 | D8 | D9 09
X3 | X4 | X5 | X6 | X7 | X8 ] X9
Xd | Xe | Xf |
G3 | G4 | G5 | G6 | G7 | G8 | G9
Gd | Ge | Gf
Z3 | Z4 | Z5 | Z6 | Z7 |
03 I Q4 I Q5 I 06 I 07
Эта грамматика является леволипенной автоматной регулярной грамматикой. По ней можно построить КА, который будет распознавать цепочки входного
Для проверки семантической правильное ги входной программы необходимо иметь вето информацию о найденных Лексических единицах языка. Эта информация помещается в таблицу лексем на основе коне трукций, найденных синтаксическим распознавателем. Примерами таких конструкциями являются блоки описания констан г и идентификаторов (если они предусмотрены семантикой языка) или операторы, где тот или иной мдептифнкетор встречается впервые (если «писание происходит по факту первого использования). Полому полный семантический анализ входной программы может быть ирон веден только после полного завершения ее синтаксического разбора.
Таким образом, входными данными для сематического анализа служат:
таблица идентификаторов;
результаты разбора синтаксических конструкций входного языка.
Результаты выполнения синтаксического разбора могут быть представлены в одной из форм внутреннего представления программы в компиляторе. Как правило, на этане семантического анализа используются различные варианты деревьев синтаксического разбора, поскольку семантический анализатор интересует прежде всего структура входной программы.
Семантический анализ обычно выполняется па двух этапах компиляции: па этапе синтаксического разбора и в начале этапа подготовки к генерации кода. В первом случае всякий раз но завершении распознавания определенной синтаксической конструкции Входного языка выполняется ее семантическая проверка па основе имеющихся в таблпцеттдептификаторов данных (такими конструкциями, как правило, являются процедуры, функции и блоки операторов входного языка). Во втором случае, после завершения Всей фазы синтаксического разбора, выполняется полный семантический анализ программы на основании данных в таблице идентификаторов (сюда попадает, например, поиск неописанных идентификаторов). Иногда семантический анализ выделяют в отдельный этап (фазу) компиляции.
В каждом компиляторе обычно присутствуют оба варианта семантическою анализатора. Конкретная их реализация зависит от версии компилятора и семантики входного языка [6, т. 2, 40, 74, 82].
Этапы семантического анализа
Семантический анализатор выполняет следующие основные действия:
проверка соблюдения во входной программе семантических соглашений входного языка;
дополнение внутреннего представления программы в компиляторе операторами и дейстнями, неявно предусмотренными семангикон входного языка;
проверка Элементарных семантических (смысловых) норм языков программирования, напрямую не связанных с входным языком.
Проверка соблюдения во входной программе семантических соглашений входного языка заключается в сопоставлении входных цепочек программы с требованиями семантики входного языка программирования. Каждый язык программи-
рования имеет четко заданные и специфицированные семантические соглашения, которые не могут быть проверены на этапе синтаксического разбора Именно их в первую очередь проверяет семантический анализатор.
Примерами таких соглашений являются следующие требования:
каждая метка, на которую есть ссылка, должна один рйз присутствовать в программе;
каждый идентификатор должен быть описан один раз, и пи один идентификатор не может быть описан более одного раза (с учетом блочной структуры описании);
все операнды в выражениях и операциях должны иметь типы, допустимые для данного выражения или операции;
типы переменных в выражениях должны быть согласованы между собой;
при вызове процедур и функций число и тины фактических параметров должны быть согласованы с числом и типами формальных параметров.
Эго только примерный перечень такого рода требований. Конкретный состав требований, которые должен проверять семантический анализатор, жестко связан с семантикой входного языка (например, некоторые языки допускают не описывать идентификаторы определенных типов) Варианты реализаций такого рода семантических анализаторов детально рассмотрены в {6, т 2, 74]
Например, если мы возьмем оператор языка Pascal, имеющий вид
а :- b + с:
то с точки зрения синтаксического разбора это будет абсолютно правильный оператор. Однако мы не можем сказать, является ли этот оператор правильным с точки зрения входного языка (Pascal), пока нс проверим семантические требования для всех входящих в него лексических элементов. Такими элементами здесь являются идентификаторы а, b и с. Не зная, что они собой представляют, мы не можем нс только окончательно утверждать правильность приведенного выше оператора, но и понять сто смысл. Фактически необходимо знать описание этих идентификаторов.
В том случае, если хотя бы один из них не описан, имеет место явная ошибка. Если это числовые переменные п константы, то Мы имеем дело с оператором сложения, если же это строковые переменные и констан гы — с оператором конкатенации строк. Кроме того, идентификатор а, например, пи в коем случае не может быть константой — иначе нарушена семантика оператора присваивания Также невозможно, чтобы одни из идентификаторов были числами, а другие - строками, или, скажем, идентификаторами массивов или структур такое сочетание аргументов для операции сложения недопустимо. И это только некоторая часть соглашений, которые ^Джеп проверить компилятор с точки зрения семантики входного языка (в данном примере Pascal).
Следует еще отметить, что от семантических Соглашений зависит не только правильность оператора, но и сто смысл. Действительно, операции алгебраического сложения и конкатенации строк имеют различный смысл, хотя и обозначаются в рассмотренном примере одним знаком операции — «+». Следовательно. от семантического анализатора зависит также и к<х результирующей Программы.
Если какое-либо из семантических требований входного языка не выполняется, то компилятор выдает сообщение об ошибке и процесс компиляции на этом, как правило, прекращается.
Дополнение внутреннего представления программы операторами и действиями, неявно предусмотренными семантикой входного языка, связано с преобразованием типов операндов в выражениях и при передаче параметров в процедуры и функции.
Если вернуться к рассмотренному выше элементарному оператору языка Pascal:
d := b + С.
то можно отметить, что здесь выполняются две операции: одна операция сложения (или конкатенации, в зависимости от типов операндов) и одна операция присвоения результата. Соответствующим образом должен быть порожден и код результирующей программы.
Однако не все так очевидно просто. Допустим, что где-то перед рассмотренным оператором мы имеем описание его операндов в виде:
var
а ■ real:
b integer:
С double:
из этого описания следует, что а — вещественная переменная языка Pascal, b — целочисленная переменная, с — вещественная переменная с двойной точностью. Тогда смысл рассмотренного оператора с точки зрения входной программы существенным образом меняется, поскольку в языке Pascal нельзя напрямую выполнят!, операции вад операндами различных типов. Существуют правила преобразования типов, принятые для данного языка. Кто выполняет эти преобразования?
Это может сделать разработчик программы — но тогда преобразования типов в явном виде будут присутствовать в тексте входной программы (в рассмотренном примере это не так). В другом случае это делает код, порождаемый компилятором, когда преобразования типов в явном виде в тексте программы не присутствуют, ио неявно предусмотрены семантическими соглашениями языка. Для этого в составе библиотек функций, доступных компилятору, должны быть функции преобразования типов (более подробно о библиотеках функций см. в разделе «Принципы функционирования систем программирования», глава 15). Вызовы этих функций как раз и будут встроены в текст результирующей программы для удовлетворения семантических соглашений о преобразованиях типов во входном языке, хотя в тексте программы в явном ваде они не ирису гствуют. Чтобы это произошло, яти функции должны быть встроены и во внутреннее представление программы в компиляторе. За это также отвечает сем штИЧеский анализатор.
С учетом предложенных типов данных, в рассмотренном примере будут не две, а четыре операции: преобразование целочисленной переменной b в формат вещественных чисел с двойной точностью; сложение двух вещественных чисел с двойной точностью; преобразование результата в вещественное число с одинарной точностью; присвоение результата переменной с. Количество операций возросло вдвое, причем добавились два вызова весьма нетривиальных функций преобра-
зовапня типов. Разработчик программы должен помнить об этом, если кочет до- бшшп»ся высокой эффективности результирующего кода.
Преобразование типов — это только один вариант операций, неявно добавляемых компилятором в код программы на основе сематических соглашении. Другим примером такого рода операций могут служить операции вычисления адреса, когда происходит обращение к элементам сложных структур данных. Существу- ктг и другие варианты такого рода операций (преобравовапие типов — только самый распространенный пример).
Таким образом, и здесь действия, выполняемые семантическим анализатором, существенным образом влияют на порождаемый компилятором код результирующей! программы.
Проверка элементарных смысловых норм языков программирования, напрямую не связанных с входным языком, — это сервисная функция, которую иредостав- чяют большинство современных компиляторов. Эта функция обеспечивает проверку компилятором некоторых соглашении, применимых к большинству современных языков программирования, выполнение которых связано со смыслом как всей входной программы в целом, гак и отдельных ее фрагментов.
Примерами таких соглашений являются следующие требования:
U каждая переменная пли константа должна хотя бы одни раз использоваться в программе;
каждая переменная должна быть определена до ее первого использования при любом ходе выполнения программы (периоду использованию переменной должно всегда предшествовать присвоение ей какого-либо значения);
результат функции должен быть определен при любом ходе ее выполнения;
каждый оператор в исходной программе должен иметь возможность хотя бы один раз выполниться;
опера горы условия и выбора должны предусматривать возможности хода выполнения программы ио каждой из своих ветвей;
операторы цикла должны предусматривать возможность завершения цикла.
Конечно, это только примерный перечень основных соглашений. Конкретный состав проверяемых соглашений зависит от семантики языка. Однако, в отличие <№ семантических требований языка, строго проверяемых семантическим анализатором, выполнение данных соглашении не является обязательным. Поэтому го, какие конкретно соглашения будут проверяться и как они будут обрабатываться. зависит от качества компилятора, от функций, заложенных в него разработчиками. Простейший компилятор вообще может не выполнять этот этап се- мапгпческого анализа и нс проверять ни одного такого соглашения, даже если это и возможно с точки зрения семантики входного ягилка.
Необязательность соглашений такого типа накладывает еще одну особенность на их обработку в семантическом анализаторе: их несоблюдение не может трактоваться как ошибка. Даже если компилятор полностью уверен в своей «правоте», тот факт, что какое-то из указанных соглашений не соблюдаемся, не должен приводить к прекращению компиляции входной программы. Обычно факт обнаружения несоблюдения такого рода соглашений трактуется компилятором как «предупреждение» (warning). Компилятор выдает пользователю сообщение об обнаружении несоблюдения одного из требований, не прерывая сам процесс компиляции, - ■ то есть он просто обращает внимание пользователя на го или иное место в исходной программе То, как реагировать на «предупреждение» (вносить изменения в исходный код или проигнорировать этот факт), — это уже забота и ответственность разработчика программы.
Необязательность указанных соглашений объясняется тем, о чем уже говорилось выше (см. раздел «Языки и цепочки символов. Способы задания языков», глава 9), — ни один компилятор не способен полностью попять и оценить смысл исходной программы. А поскольку смысл программы доступен только человеку (для плохо написанной программы — только ее разработчику, а в другом случае — некоторому кругу лиц), то он и должен нести ответственность за семантические соглашения.
Задача проверки семантических соглашении входного языка во многом связана с проблемой верификации программ. Эта проблема детально рассмотрена в [1, 25, 4G, 511.
Рассмотрим в качестве примера функцию, представляющую собой фрагмент входной программы на языке С:
int f_test(lnt а)
{ int b.c
b = 0:
с - 0;
if (b=l) { return a: ) c - a + b:
}*
Практически любой современный компилятор с этого языка обнаружит в данном месте входной программы массу «неточностей Например, переменная с описана, ей присваивается значение, по опа нигде не используется; значение переменной Ь, присвоенное в операторе Ь=0 тоже никак не используется; наконец, услов-
иый оператор лишен смысла, так как всегда предусматривает ход выполнения только по одной своей ветке (и значит, оператор с=а+Ь; никогда выполнен не будет). Скорее всего, компилятор выдаст еще одно предупреждение, характерное именно для языка С, — в операторе if(Ь=1) присвоение стоит в условии (это не запрещено ни синтаксисом, пи семантикой языка, но является очень распространенной смысловой ошибкой в С). В принципе смысл (а точнее, бессмысленность) этого фрагмента будет правильно воспринят и обработан компилятором.
Однако если взять аналогичный по смыслу, но синтаксически более сложный фрагмент программы, то картина будет несколько иная:
int f_test_add(1nt* a. Int* b)
{
*а - 1;
*b - 0:
return *a:•.
int f_test(int a)
( int b.c:
b - 0
if (f_test_add(&b.&c) ! = 0) { return a; } c - a + b;
}
Здесь уже компилятор вряд ли сможет выяснить порядок изменения значения переменных и выполнение условий в данном фрагменте из двух функций (обе они сами по себе независимо вполне осмысленны!). Единственное предупреждение, которое, скорее всего, получит в данном случае разработчик, — это то, что функция f test не всегда корректно возвращает результат (отсутствует оператор return перед концом функции). И то это предупреждение на самом деле нс будет соответствовать истинному положению вещей.
В целом проверка дополнительных семантических соглашении является весьма полезной функцией компиляторов, по ответственность за смысл исходной программы всегда по-прсжисму остается на сс разработчике.
Идентификация лексических единиц языков программирования
Идентификация переменных, типов, процедур, функций и других лексических единиц языков программирования — это установление однозначного соответствия между данными объектами и их именами в тексте исходной программы. Идентификация лексических единиц языка чаще всего выполняется па этапе семантического анализа.
Как правило, большинство языков программирования требуют, чтобы в исходной программе имена лексических единиц не совпадали как между собой, так и с ключевыми словами синтаксических конструкций языка*. Однако чаше всего этого бывает недостаточно, чтобы установит!, однозначное соответствие между лексическими единицами и их именами, поскольку существуют дополнительные смысловые (сем ? кы ) чс ния, накладываемые я ьн м на употребление mix имен.
Например, локальные переменные в большинстве языков программирования име юг так называемую «область видимости», которая ограничивает употребление имени переменной рамками того блока исходном программы, где эта переменная описана. Это значит, что одной стороны, такая переменная не может быть и пользована вне пределов своей области видимости. С другой стороны, их ременной может быть не уникальным, поскольку в двух различных областях видимости допускается существование двух различных переменных с одинаковым именем (причем в большинстве языков программирования, допускающих блочные структуры, области видимости переменных в них могут перекрываться). Другой пример такого рода ограничений на уникальность имен — эт< Ю факт, что в языке программирования С и разные функции или процедуры с различными аргументами могут иметь одно и то же имя.
Безусловно полный перечень гаки ограничений зависит от семантики конкретного языка программирования. Все они четко заданы в описании языка и не могут допуска и. неоднозначности в толковании, но не могут быть полностью определены па вгане лексического р б ра, а потому требуют от компиляг ра дополнительных действий на этапах синтаксического разбора и семантического па; иза. Общая ниправленпость этих действий такова, чтобы дать каждой лексической единице языка уникальное имя в пределах всей исходной программы и потом использовать это имя при синтезе результирующей программы.
Можно дать примерный перечень действий компиляторов для идентификации п । -м иных, констант, функций, процедур и других лексических единиц языка:
имена локальных переменных дополняются именами тех блоков (функций, процедур), в которых эти переменные описаны;
имена внутренних переменных и функций модулей исходной программы дополняются именем самих модулей, причем это касается только внутренних имен и нс должно происходить, с ти переменная или функция дос гунны навис модуля:
имена процедур п функций, при надлежащих объектам (классам), в обьсктно- ориентироваишях языках программирования дополняются иаименощшяем типа обьекта (класса), которому они принадлежат:
1 Сущее гну ют языки программирования (например, PL/1), которые допуыМуп, чтобы имена идентификаторов, задаваемые пользователями. совпадали с ключевыми словами языка. В таких языках возможны нСсына питсреоные синтаксические конструкции. Однако, па взгляд автора, такая возможность не предоставляет ничего хорошего, кроме лишних хлопот разработчикам компилятора. Даже если такая особенность прпсуп* входному языку, пользоваться ею напоя'цшыю не рекомендуется -- читаемость программы 6ynw сильно 1 прудг па т гп» i -. оважнып фактор
имена процедур и функций модифицируются в зависимости от типов пх формальных аргументов.
Конечно, »то далеко не полный перечень возможных действий компилятора, каждая реализация компилягора может предполагать своп набор действий 'Го, ка кие из них будут использоваться и как оны будут реализованы на практике, зависит от языка исходной программы и разработчиков компилятора.
Как правило, уникальные имена, которые компилятор присваивает лексическим единицам языка, используются только во внутреннем представлении исходной программы компилятором, и человек, создавший исходную программу, не сталкивается с ними. По они могут потребоваться пользователю в некоторых случаях — например при отладке программы, при порождении текста результирующей программы на языке ассемблера пли при использовании библиотеки, созданной версией компилятора для одного языка программирования в другом языке (или даже просто в другой версии компилятора). Тогда пользователь должен знать, по каким правилам компилятор порождает уникальные имена для лексических единиц исходной программы.
Во многих современных компиляторах (и обрабатываемых ими входных языках) предусмотрены специальные настройки и ключевые слова, которые позволяют отключить процесс порождения компилятором уникальных имен для лексических единиц языка. Эти слова учтены в специальных синтаксических конструкциях языка (как правило, эго конструкции, содержащие слова export или external). Если пользователь использует зги средства, то компилятор не применяет механизм порождения уникальных имен для указанных лексических единиц. В этом случае разработчик программы сам отвечает за уникальность имени данной лексической единицы в пределах всей исходной программы или даже в пределах всего проекта (если исполыуются несколько различных исходных модулей — более подробно см. раздел «Принципы функционирования систем программирования», глава 15), Если требование уникалыюсз и не будет выполняться, могут возникнуть синтаксические или семантические ошибки па стадии компиляции либо же другие ошибки па более поздних этапах разработки программно-
го обеспечения. Поскольку наиболее широко используемыми лексическим! единицами в различных языках программирования являются, как правило, име а процедур или функций то этот вопрос прежде всего, касается именно их
Распределение памяти.
Принципы распределения памяти
Распределение памяти — это про есс, ко орый ставит в соответств! е • еко ia- ским единицам исходной рограммы адрес размер и атрибуты области г амгп i, необходимой д.. я этой •ексической единицы. Область памяти — это б ЮК ячеек памяти, выделяемый для данных каким-то образом объединенных логи1 с i и Логика таких объединений задается семантикой исходного языка.
Распределение памяти работает с лексическими единицами языка — переменным! константами, функциями и т п. и с информацией об этих единицах, полученной на эта ах лексического и синтаксического анализа. Как правило, исходными данными для процесса распределения 1 амят в компиляторе служат таблица идетт ф катеров пос роев ая лексическим анализатором, н деклара- тив ая часть программы (так называемая «область описаний») полученная в резулы те синтаксического анализа. Нс во всех языках программирования дек г - ративная часть программы грпсуктвуел явно, ^которые я«ыкм предусматривают дополни лын.е сематические правила для описания констант и переменных; кроме того, перед распределением памяти надо выполнить идентификацию лек- ическнх единиц языка. Поэтому рас ределение памяп выполняется уже после семантического зна. 136 текста исходно'! npi граммы.
Процесс распределения намяли в современных компиляторах, как правило, ра ботаст с относительными, а не абсолютными адресами ячеек г амяти (боле под робно о разнице между абсос ютпыми и относительными адресами см в раздел «Принципы функционирования систем программирования», глава 15). Распре- деленп ах яти выполняется перед гепера ей кода результирующей программы полому ню его результаты должны быть ис пользованы в процессе генерации кода.
Во всех языках программирования существ; ет понятие так называемых «б зо вых типов да !ых» (основных пли «скалярных» типов) Размер обла ли памяти, пеобход мый для лексической едш ицы базового типа считается заранее известным Он определяется семант коп яз и<а и архи ек турой вычислительной систе мы, на которой должна выполняться созданная компилятором результирующая программа. Р мер памяти выделяемо! под лексические единицы базовых типов, п? зависит от версии компилятора, что обеспечивает- совместимость и переносимость исходных программ Этого правила строго придерживаются ведущие раз работч и к и ко м пил яторов.
Идеальным вариантом для разработчиков программ ыч бы такой компилятор, у которого размер памяти для базовых типов зависел бы только от- сехниггики языка Но чаще всего зависимость результирующей программы от архитектуры вычислительной системы полностью исключить не удается Тогда создатели ком пиляторов и языков программирования разрабатывают механизмы позволяющие с вес ти эту зависимость к минимуму
Например, в языке программирования С базовыми типами данных являются типы char, int, long int и т. и. (реально этих типов, конечно, больше, чем три), а в языке программирования Pascal — типы byte, char, word, integer и т. и. Размер базового типа int в языке С для архитектуры компьютера на базе 16-разрядных процессоров составляет 2 банта, а для 32-разрядных процессоров — 4 байта. Разработчики исходной программы на этом языке, конечно, могут узнать данную информацию (опа, как правило, известна для каждой версии компилятора), ио если ее использовать в программе напрямую, го такая программа будет жестко привязана к конкретной архитектуре компьютера. Чтобы исключить подобную зависимость, лучше использовать механизм определения размера памяти для типа данных, предоставляемый языком программирования, — в языке С, нанри- мер, это функция sizeof.
Для более сложных структур данных используются правила распределения памяти, определяемые соматикой (смыслом) этих структур Эти правила достаточно просты и в принципе одинаковы во всех языках программирования.
Вот правила распределения памяти под основные виды структур данных:
для массивов — произведение числа элементов в массиве па размер памяти для одного элемента (то же правило применимо и для строк, но во многих языках строки содержат еще и дополнительную служебную информацию фиксировавших) объема);
для структур (записей с именованными нолями) — сумма размеров памяти но всем полям структуры;
для объединений (союзов, общих областей, записей с вариантами) — резмер максимального ноля в объединении;
для реализации объектов (классов) — размер памяти для структуры с такими же именованными полями плюс намять под служебную информацию объектно-ориентированного языка (как правило фиксированного обьема).
Формулы для вычисления объема памяти можно записать следующим образом:
для массивов: = П(т.)'/„.
Мп
где и — размерность массива, т, — количество элементов г'-й размерности, — объем памяти для одного элемента;
для структур: Vtmp = £V№Uj,
i=Ln
где п — общее количество полей в структуре, У,1МЯ. — объем памяти для г-го поля структуры;
U для объединений: Vtmp = max Vno„ ,
где п — общее количество полей в объединении, — объем памяти для г-го поля объединения.
Для боя ее сложных структур данных входного языка объем памяти, отводимой под эти структуры данных, вычисляется рекурсивно. Например, если имеется массив структур, то при вычислении обьема отводимой под этот массив памяти
языка (см. раздел «Конечные автоматы», глава 10). Граф переходов этого автомата приведен на рис. 13.8. Видно, что построенный автомат является детерминированным КА, поэтому в дальнейших преобразованиях он не нуждается. Начальным состоянием автомата является состояние Н.
Рис. 13 8. Граф конечного автомата для распознавания целочисленных констант языка С
Приведем автомат к полностью определенному виду — дополним его новым состоянием Е, па которое замкнем все дуги неопределенных переходов. На графе автомата дуги, идущие в это состояние, не нагружены символами — они обозначают функцию перехода по любому символу, кроме тех символов, которыми уже помечены другие дуги, выходящие из той же вершины графа (такое соглашение принято, чтобы не загромождать обозначениями граф автомата). На рис. 13.8 это состояние и связанные с ним дуги переходов изображены пунктирными линиями.
При построении КА знаком «1» были обозначены любые символы, которыми может завершаться целочисленная константа языка С. Однако в реальной программе этот символ не встречается. При моделировании КА следует принять во внимание, какими реальными символами входного языка может завершаться целочисленная константа языка С.
В языке С в качестве границы константы могут выступать:
знаки пробелов, табуляции, перевода строки;
разделители (, ), [, ], {. }, ,, :, ;;
знаки операций +, -, *, /, 8, |, ?, ~, <, >, ", =, %
Теперь можно написать программу, моделирующую работу указанного автомата. Ниже приводится текст функции на языке Pascal, реализующей данный распо-
для вычисления обьема памяти, необходимой для одного элемента массива, будет вызвана процедура вычисления памяти структуры. Такой подход определения объема занимаемой памяти очень удобен, если декларативная часть языка представлена в виде дерева типов. Тогда для вычисления объема памяти, занимаемой типом из каждой вершины дерева, нужно вычислить обл,ем памяти для всех потомков этой вершины, а йотом применить формулу, связанную непосредственно с самой вершиной (этот механизм подобен механизму СУ-перевода, применяемому при генерации кода). Как раз такого типа древовидные конструкции строит спи токсический разбор для декларативной части языка.
Далеко по все лексические единицы языка требуют для себя выделен ня памяти. То, под какие элементы языка нужно выделять ячейки памяти, а под какие нет, определяется исключительно реализацией компилятора и архитектурой используемой вычислительной системы. Так, целочисленные константы можно разместить в статической памяти, а можно и непосредственно в тексте результирующей программы (это позволяют практически все современные вычислительные системы), то же самое относится и к константам с плавающей точкой, но их размещение в тексте программы допустимо не всегда. Кроме того, в целях экономии памяти, занимаемой результирующей программой, под различные элементы языка компилятор может выделить одни и те же ячейки памяти. Например, в одной и той же области памяти могут бы и. размещены одинаковые строковые константы или две различные локальные переменные, которые никогда не используются одновременно.
Говоря об объеме памяти, занимаемой различными лексическими единицами и структурами данных языка, следует упомянуть erne одни момент, связанный с выравниванием отводимых для различных лексических единиц границ областей памяти. Архитектура многих современных вычислительных систем предусматривает, иго обработка данных выполняется более эффективно, если адрес, по которому выбираются данные, кратен определенному числу байт (как правило, это 2, 4, 8 или 16 байт)1. Современные компиляторы учитывают особенности вычислительных систем, на которые ориентирована результирующая программа. При распределении данных они могут размещать области памяти под лексические единицы наиболее оптимальным образом. Поскольку не всегда размер памяти, отводимой под лексическую единицу, кратен указанному числу байт, то в общем объеме памяти, отводимой под результирующую программу, могут появляться неиспользуемые области.
Например, если мы имеем описание переменных па языке С:
static char cl. с2. сЗ;
то, зная, что под одну переменную типа char отводится 1 байт памяти, можем ожидать, что для описанных выше переменных потребуется всего 3 байта памяти. Однако если кратность адресов для доступа к памяти установлена 4 байта, то под эти переменные будет отведено в целом 12 байт памяти, из которых 9 не будут использоваться.
' Некоторые вычислительные системы даже вообще не могут работать с адресами, не кратными определенному числу байт.
Как правило, разработчику походной программы по нужно знать, каким образом компилятор распределяет адреса под отводимые области памяти Чаще всего компилятор сам выбирает оптимальный метод, и, изменяя границы выделенных областей, он всегда корректно осуществляет доступ к ним. Вопрос об этом может встать, если с данными программы, написанной на одном языке, работают про- г|шммы, написанные па другом языке программирования (чаще всего, на языке ассемблера), реже такие проблемы возникают при использовании двух различных компиляторов с одного и того же входного языка. Большинство компиляторов позволяют пользователю в этом случае самому указать, использовать или пет кратные адреса и какую цтапицу кратности установить (если это вообще возможно с точки зрения архитектуры целевой вычислительной системы).
Память можно разделить на локальную и глобальную намять, динамическую и статическую память.
Глобальная и локальная память
Глобальная область памяти — это область памяти, которая выделяется один раз при инициализации результирующей программы и действует все время выполнения программы. Как правило, глобальная область памяти может быть доступна из любой части исходной программы, по многие языки программирования позволяют налагать синтаксические и семантические ограничения иа доступность даже для глобальных областей памяти. При этом сами области памяти п связанные с ними лексические единицы остаются глобальными, ограничения налагаются только на возможность использования их в тексте программы па входном языке (и эти ограничения в принципе возможно обойти).
Локальная область памяти — это область памяти, которая выделяется в начале выполнения некоторого фрагмента результирующей программы (блока, функции, процедуры или оператора) и может быть освобождена по завершении выполнения данного фрагмента. Доступ к локальной области памяти всегда запрещен за пределами того фра! мента программы, в котором опа выделяется. Эго определяется как сшгтакспчесю ш и семантическими правилами языка, так и кодом ре- зультпрующей программы, Даже если удастся обойти ограничения, налагаемые входным языком, использование таких областей памяти вне их области видимости приведет к катастрофическим последствиям для результирующей программы.
Распределение памяти па локальные и глобальные области целиком определяется семантикой языка исходной программы. Только зная смысл синтаксических конструкций исходного языка, можно четко сказать, какая из них будет отнесена в глобальную область памяти, а какая — в легальную. Иногда в исходном языке для некоторых конструкций нет четкого разграничения, тогда решение об их отнесении в ту или иную область памяти принимается разработчиками компилятора и может зависеть от используемой версии компилятора. При этом разработчики исходной программы не должны полагаться па тот факт, чтосжип раз принятое решение будет неизменным во всех версиях компилятора.
Рассмотрим для примера фрагмент текста модуля программы па языке Pascal
const
GlobalJ - 1;
Global_2 : integer - 2: var
Global_I integer
function Test (Param: integer) pointer: const
Local_l - 1:
Local_2 integer - 2:
var
Local_I integer
begin
end.
Согласно семантике языка Pascal, переменная Global ! является глобальной переменной языка и размещается в глобальной области памяти, константа Global l также является глобальной, по язык не требует, чтобы компилятор обязательно размещал ее в памяти — значение константы может быть непосредственно подставлено в код результирующей программы там, где опа используется Типизированная константа Global_2 является глобальной, по в отличие or константы Global 1 семантика языка предполагает, что эта константа обязательно будет размещена в памяти, а не в коде программы Доступность идентификаторов Global I, Global_2 и Global_l из других модулей завщчгг от того, где и как они описаны. Например, для компилятора Borland Pascal переменные и константы, описанные в заголовке модулей, дос гунны из других модулей программы, а переменные и константы, описанные в теле модулей, недоступны, хотя и те и другие являются глобальными
Параметр Param функции Test, переменная Locall и константа Locall являются локальными элементами этой функции Опп не доступны вне пределов данной функции и располагаются в локальной области памяти. Типизированная коп- сгаига Local_2 представляет собой очень интересный элемент программы. С одной стороны, опа не доступна вне пределов функции Test и потому является ее локальным элементом. С другой стороны, как типизированная константа опа будет располагаться в глобальной области памяти, хотя ниоткуда извне не может быть доступна (аналогичными конструкциями языка С, например, являются локальные переменные функций, описанные как static).
Семангическис особенности языка должны учитывать создатели компилятора, когда разрабатывают модуль распределения памяти. Безусловно, это должен знат ь и разработчик исходной программы, чтобы нс допускать семантических (смы еловых) ошибок - этот тпп ошибок сложнее всею поддастся обнаружению. Так, в приведенном выше примере в качестве результата функции Test можит выступать адрес переменной Global l или типизированной константы Global_2 Результатом функции не может быть адрес констант Global_l пли Local l - эго запрещено се-
маш икой языка. Гораздо сложнее вопрос с переменной Local I или параметром функции Param. Будучи элементами локальной области памяти функции Test, они никак не могут выступать в качестве результата функции, потому что он будет использован вне самой функции, когда область ее локальной памяти может уже не существовать. Но пи синтаксис, пи семантика языка не запрещают использовать адрес этих элементов в качестве результата функции (и лалеко не всегда компилятор способен хотя бы обнаружить факт такого использования адреса локальной переменной). Эти особенности языка должен учитывать разработок программы. А вот адрес типизированной константы Local_2 в принципе может быть результатом функции Test Ведь хотя она и является локальной константой, ио размещается в глобальной области памяти (другое дело, что этим пользоваться не рекомендуется, поскольку нет гарантии, что принцип размещения локальных типизированных констант не изменится с переходом к другой версии комп и штора).
Статическая и динамическая память
Статическая область памяти — это область памяти, размер которой известен па этапе компиляции. Поскольку для статической области памяти известен ее размер, компилятор всегда может выделить эту область памяти и связать ее с соответствующим элементом программы. Поэтому для статической области памяти компилятор порождает некоторый адрес (как правило, это относительный адрес в программе — см. раздел «Принципы функционирования систем программирования», глава 15). В статическую область памяти попадает большинство переменных и констант исходного языка.
Статические области памяти обрабатываются компилятором самым простейшим образом, поскольку напрямую связаны со своим адресом.
Динамическая область памяти — это область памяти, размер которой на этапе компиляции программы нс известен. Размер динамической области памяти будет известен только в процессе выполнения результирующей программы. Поэтому для динамической области памяти компилятор не может непосредственно выделить адрес - для нее он порождает фрагмент кола, который отвечает за распределение памяти (се выделение и освобождение) Как правило, с динамическими областями памяти связаны многие операции с указателями и с экземптирами объектов (классов) в объектно-ориентированных языках программирования.
Динамические области памяги, в свою очередь, можно разделить на динамические области памят и, выделяемые пользователем, и динамические области памяти, выделяемые непосредственно компилятором.
Динамические области памяги, выделяемые пользователем, появляются в тел случаях, когда разработчик исходной программы явно использует в тексте про-
граммы функции, связанные с распределением памяти (примером таких функций являются New и Dispose в языке Pascal, malloc и free в языке С, new и delete в языке C++ и др.). Функции распределения памяти могут использовать либо напрямую средства ОС, либо средства исходного языка (которые, в конце концов, все равно основаны па средствах ОС). В этом случае за своевременное выделение и освобождение памяти отвечает сам разработчик, а нс компилятор (принципы динамического распределения памяти в ОС более подробно описаны в части 1 этого пособия). Компилятор должен только построить код вызова соответствующих функций и сохранения результата — в принципе, для него работа с динамической памятью пользователя ничем не отличается от работы с любыми другими функциями и дополнительных сложностей не выбывает
Другое дело — динамические области памяти, выделяемые компилятором. Опи появляются тогда, когда пользователь использует тины данных, операции над которыми предполагают перераспределение памяти, по присутствующее в явном виде в тексте исходной программы. Примерами таких типов данных могут служить строки в некоторых языках программирования, динамические массивы и, конечно, многие операции над экземплярами объектов (классов) в обьекгио- ориентпроваппых языках (наиболее характерный пример тип данных string в версиях Borland Delphi языка Object Pascal). В этом случае сам компилятор отвечает за порождение кода, который будет всегда обеспечивать своевременное выделение памяти под элементы программы, и за освобождение ее по мере использования. Многие компиляторы обьекгио-ориентироваипых языков программирования используют для этих целей специальный менеджер памяти, к которому обращаются во всех случаях при явном пли неявном использовании динамической памяти. Код менеджера памяти включается в текст результирующей программы или поставляется в виде отдельной библиотеки.
Как статические, так п динамические области памяти сами по себе могут быть глобальными или локальными.
Дисплей памяти процедуры (функции). Стековая организация дисплея памяти
Дисплей памяти процедуры (функции) — это область данных, доступных для обработки в этой процедуре (функции).
Как правило, дисплей памяти процедуры включает следующие составляющие:
глобальные данные (переменные и константы) всей программы;
формальные аргументы процедуры;
локальные данные (переменные и константы) данной процедуры. •
С глобальными данными никаких проблем не возникает — их адреса известны, они все устанавливаются на этапе распределения памяти. Даже если память под эти данные распределяется динамически, компилятору однозначно известно, как к этой памяти обратиться, и способ обращения одинаков для всей пропжммы. Поэтому он всегда может создать код для обращения к этим данным вне зависимости от того, откуда это обращение происходит.
Сложнее с. локальными параметрами и переменными процедуры. Опп относятся к локальной памяти процедуры и потому должны быть как-то связаны с нею. Существует несколько вариантов связывания локальных переменных и параметров с кодом соответствующей процедуры или функции. Им соответствуют варианты организации дисплея памяти процедуры, перечисленные ниже:
статическая организация дисплея памяти процедуры;
динамическая организация дисплея памяти процедуры,
стековая организация дисплея памяти процедуры.
Статическая организация дисплея памяти процедуры (функции) прсдусмагри вает, чго с каждой процедурой (функцией) исходной программы при распределении памяти компилятор связывает фиксированную область памяти, предназначенную для размещения се локальных данных (переменных и параметров). Адрес этой области памяти фиксируется па этапе распределения памяти. Каждому формальному параметру процедуры (функции) и каждой локальной переменной соответствует своя группа ячеек в згой области памяти. Отдельная группа ячеек отводится дЛя хранения адреса возврата — адреса, по которому должно быть передано управление по завершению выполнения процедуры (функции).
Тогда в каждой точке вызова процедуры компилятор порождает код, который размещает фактические параметры вызова процедуры (функции) в ячейках области памяти процедуры, соответствующих ее формальным параметрам. Перед вызовом процедуры запоминается адрес возврата, который должен указывать на фрагмент кода непосредственно за местом вызова, после чего управление передается самой процедуре. В коде процедуры обращение к локальным данным (параметрам и переменным) происходит по фиксированным адресам, которые для каждой процедуры известны после этапа распределения памяти. После завершения выполнения процедуры (функции) из фиксированной ячейки памяти извлекается код возврата и происходит передача управления по нему.
Схема статической организации памяти проиллюстрирована па рис. 14.1.
Данная схема мало чем отличается от обычного распределения памяти под статические глобальные данные, за исключением того, что области памяти процедур, которые никогда в тексте программы не вызывают друг друга, могут частично или полностью перекрываться. Эта схема проста и удобна, она не требует сложной адресации. За счет использования одной и той же области памяти для
В принципе в этом случае разработчик исходной программы может даже и не подозревать о том, что используемая им операция предполагает перераспределение памяти. Однако это не лучшим образом будет сказываться на эффективности созданной разра- ботч11ком 11 ]юграммы.
храпения локальных данных процедур н функций, которые в исходной программе никогда не вызывают друг друга, компилятор может добиться достаточно эффективного распределения памяти в результирующей программе. У этой схемы есть один серьезный недостаток — опа нс допускает рекурсивных вызовов процедур и функций.
Рис. 14.1. Схема статической организации дисплея памяти процедуры (функции)
Действительно, если в схеме, приведенной па рис 14.1, в момент выполнения кода процедуры А вновь выполнить рекурсивный вызов той же самой процедуры, то все се локальные данные будут записаны заново (в том числе и код возврата!). После возврата из вызванной процедуры данные вызвавшей процедуры будут безвозвратно потеряны. Процедура, рекурсивно вызвавшая сама себя, в такой схеме просто не сможет корректно завершиться. То же самое произойдет, сели рекурсивный вызов будет происходить нс непосредственно, а через цепочку' вызовов других процедур.
Этот серьезный недостаток ограничивает применение данной схемы организации дисплея памяти процедур и функций в современных компиляторах.
Динамическая организация дисплея памяти процедуры (функции) основана на тех же принципах, что и статическая, ио память для локальных данных процедуры выделяется в пей динамически всякий раз в момент вызова, а по завершении процедуры — освобождается. Адрес области памяти, связанной с каждой процедурой, в данном случае уже не может быть фиксированным — его надо каким- либо образом передать в процедуру (функцию). Для этой цели используется один из регистров процессора, называемый регистром адресации.
Тогда при вызове процедуры необходимо выполнить следующие действия:
выделить область памяти, необходимую для хранения Локальных данных процедуры, и запомнить ее адрес;
заполнить значения параметров процедуры и адрес возврата;
запомнить состояние регистра адресации в точке вызова;
поместить в регистр адресации адрес области памяти процедуры и передать управление процедуре.
После выполнения этих действий может выполняться код вызванной процедуры. Доступ ко всем локальным данным и параметрам в нем осуществляется только через регис гр адресации.
При возврате из процедуры необходимо выполнить следующие действия:
выбрать из области памяти процедуры адрес возврата (сразу за точкой вызова) и передать туда управление;
освободить область памяти, на которую указывает регистр адресации;
по смещению относительно точки возврата выбрать сохраненное значение и поместить его в регистр адресации.
После этого можно продолжить выполнение кода результирующей программы следующего за вызовом процедуры.
Данная схема, хотя и допускает рекурсивные вызовы, по получила широкого распространения. Это вызвано целым рядом причин. Во-первых, опа требует постоянного динамического перераспределения памяти — это достаточно сложная операция, которая замедляет выполнение вызовов. Во-вторых, такая схема предполагает доеппочио сложную схему адресации локальных данных. Наконец, oira требует храпения промежуточных данных непосредственно в исполняемом коде программы, что представляет определенную сложность.
В настоящее время эта схема не используется, поскольку уступает почти но всем показателям схеме стековой организации дисплея памяти.
Стековая организация дисплея памяти процедуры (функции) основана на том, что в момент вызова все параметры процедуры и адрес возврата помещаются в единый для всей результирующей программы стек параметров, а при завершении выполнения программы они извлекаются («выталкиваются») из стека Код процедуры адресуется к локальным данным по смещениям относительно верхушки стека.
Эта схема получила наибольшее распространение в современных компиляторах, поэтому далее опа рассмотрена наиболее подробно.
Стековая организация дисплея памяти процедуры (функции)
В этой схеме компилятор организует единый для всей программы стек параметров. Верхушка стека адресуется одним из регистров процессора — регистром стека. Для доступа к данным удобно использовать еще один регистр процессора, называемый базовым регистром. В начале выполнения результирующей программы стек пуст.
Тогда при вызове процедуры необходимо выполнить следующие действия:
поместить в стек все параметры процедуры,
запомнить в стеке адрес возврата и передать управление вызываемой процедуре;
запомнить в стеке значение базового регистра;
запомнить состояние регистра стека в базовом регистре;
в начале выполнения вызываемой процедуры разместить в стеке все необходимые ей локальные данные.
После выполнения этих действий может выполняться код вызванной процедуры. Доступ ко всем локальным данным и параметрам в нем может выполняться через базовый регистр (параметры лежат в стеке ниже места, указанного базовым регистром, а локальные переменные и константы — выше места, указанного базовым регистром, ио ниже места, указанного регистром стека).
При возврате из процедуры необходимо выполнить следующие действия:
выбрать из стека все локальные данные процедуры;
выбрать из стека значение базового регистра;
выбрать из стека адрес возврата;
передать управление по адресу возврата и выбрать из стека все параметры процедуры.
После этого можно продолжить выполнение кода результирующей программы, следующего за вызовом процедуры.
Па рис. 14.2 показано, как изменяется содержание стека параметров при выполнении трех последовательных вызовов процедур: сначала процедуры А, затем процедуры В и снова процедуры Л. При вызовах стек заполняется, а при возврате из кода процедур — освобождается в обратном порядке.
Из рис. 14.2 видно, что при рекурсивном вызове все локальные данные последовательно размещаются в стеке и при этом каждая процедура работает только со своими данными. Более того, такая схема легко обеспечивает поддержку вызовов вложенных процедур, которые допустимы, например, в языке Pascal.
Стековая организация памяти получила широкое распространение в современных компиляторах. Практически все существующие ныне языки высокого уровня предполагают именно такую схему организации памяти, ее также используют и в программах на языках ассемблера. Широкое распространение стековой организации дисплея памяти процедур и функцш тесно связано также с тем, что большинство операций, выполняемых при вызове процедуры в этой схеме, реа-
лизованы в виде соответствующих машинных команд во многих современных вычислительных системах. Это, как правило, все операции со стеком параметров, а также команды вызова процедуры (с ав томатическим сохранением адреса возврата в стеке) и возврата из вызванной процедуры (с выборкой адреса возврата из стека). Такой подход значительно упрощает и ускоряет выполнение вызовов при стековой организации дисплея памяти процедуры (функции).
Вызов 1 процедуры А
Содержимое стека:.
-«—I Регистр стека |Локальные данные процедуры А
Значение базового регистра 1.—,
^—Базовый регистр
Адрес возврата вызова 111
Фактические параметры процедуры А
Вызов 2 процедуры В
Содержимое стека:
Локальные данные процедуры В
| Регистр стека |
Значение базового регистра 2
Адрес возвра а вызова 2
Iр
Фактические параметры процедуры В
Локальные данные процедуры А
Значение базового регистра 1
Адрес возврата вызова 1
Фактические параметры процедуры А
Вызов 3 процедуры А Содержимое стека:
Локальные данные процедуры А
Я1ИС, 1 р С, 1 ска
Значение базового регистра 3
Адрес возврата вызова 3
Я I DdJUbWH pel HCip
Фактические параметры процедуры А
Локальные данные процедуры В
Значение базового регистра 2
Адрес возврата вызова 2
Фактические параметры процедуры В
Локальные данные процедуры А
Значение базового регистра 1
Адрес возврата вызова 1
Фактические параметры процедуры А
Рис. 14.2. Содержание стека параметров при выполнениитрех последовательных вызовов процедур
знаватель. Результатом функции является текущее положение считывающей головки анализатора в потоке входной информации после завершения разбора лексемы при успешном распознавании либо 0 при неуспешном распознавании (если лексема содержит ошибку).
В программе переменная IState отображает текущее состояние автомата, переменная 1 является счетчиком символов входной строки. В текст программы вписаны комментарии в те места функции, где возможно выполнить запись найденной лексемы в таблицу лексем и таблицу символов, а также выдать сообщение об обнаруженной ошибке.
function RunAuto (const slnput: string; IPos: integer): integer:
type
TAutoState = (AUTO_H. AUTO_S. AUTO_W. AUT0_U. AUTO_L. AUTO_V. AUTO J). AUTO_G. AUTOJG AUTO_O. AUTO_Z. AUTO_N. AUTO_E):
const
AutoStop - [#10. #13. #7. '('. )'. [".
V. &'. r. ■?*.'<■. ■>•, ,x'. •-■. 'Ж']
var
iState : TAutoState:
i.il . integer;
sOut : String;
begin
i := iPos:
11 - Length(slnput);
sOut :- ":
iState :- AUTO_H;
repeat
case iState of
AUTO_H:
case slnput[1] of •
' + ','-';iState:- AUTO N;
'O'iState= AUTO_Z;
'Г'9iState- AUTO_D
elseiState:= AUTO_E:
end'
AUTO_N:
case slnput[i] of
'O’ iState :- AUTO_Z
'Г /Э'; iState :- AUTO_D:
else iState := AUTO_E
end;
AUTO_Z:
case slnput[i] of
■x":iState:•AUTO_X:
'O'./?': iState := AUTO_Q;
'8'.'9' iState :- AUT0_D:
else
Стековая организация дисплея памяти процедуры позволяет организовать рекурсию вызовов (в отличие от статической схемы) и имеет все преимущества перед динамической схемой. Однако у этой схемы есть один существенный недостаток — память, отводимая под стек параметров, используется неэффективно Очевидно, что стек параметров должен иметь размер, достаточный для хранения всех параметров, локальных данных и адресов при самом глубоком вложенном вызове процедуры в результирующей программе. Как правило, ни компилятор, ни разработчик программы не могут знать точно, какая максимальная глубина вложенных вызовов процедур возможна в программе. Следовательно, не известно заранее, какая глубина стека потребуется резулы ирующей программе во время ее выполнения. Размер стека обычно выбирается разработчиком программы «с запасом», так, чтобы гарантированно обеспечить любую возможную глубину вложенных вызовов процедур и функций. С другой стороны, большую часть времени своей работ ы результирующая программа не использует значительную часть стека, значит, и часть оперативной памяти компьютера нс используется, так как стек параметров сам по себе не может быть использован для других целей. _
Стековая организация дисплея памяти процедур и функций присутствует практически во всех современных комииля горах. Тем не менее процедуры и функции, построенные с помощью одного входного языка высокого уровня, не всегда могут быть использованы в программах, написанных на другом языке. Дело в том, что стековая организация определяет основные принципы организации дисплея памяти процедуры, но не определяет механизм реализации данной схемы. В разных входных языках детали реализации этой схемы могут отличаться порядком помещения параметров процедуры в стек и извлечения из пего. Эти вопросы определяются в соглашениях о вызове и передаче параметров, принятых в различных входных языках программирования.
Параметры могут помещаться в стек в прямом порядке (в порядке их следования в описании процедуры) или в обратном порядке. И тлекаться из стека они могут либо в момент возврата из процедуры (фуик/щи), либо непосредственно после возврата. Кроме того, для повышения эффективности программ и увели-
чепия скорости вызова процедур и функций компиляторы могут предусматривать передачу части их параметров не через стек, а через свободные регистры процессора.
Обычно разработчику нет необходимости знать о том, какое соглашение о передаче параметров принято во входном языке программирования. При обработке вызовов внутри текста исходной программы компилятор сам корректно формирует код для выполнения вызовов процедур и функций. Наиболее развитые компиляторы могут варьировать код вызова в зависимости от типа и количества параметров процедуры (функции). Однако если разработчику необходимо использовать процедуры и функции, созданные на основе одного входного языка, в программе на другом входном языке, то компилятор не может знать заранее соглашение о передаче параметров, принятое для этих процедур и функций. В этом случае разработчик должен сам позаботиться о том, чтобы соглашения о передаче параметров в двух языках были согласованы, поскольку несогласованное гь в передаче параметров может привести к полной неработоспособности результирующей программы. Для этого во многих языках программирования существуют специальные ключевые слова, которые позволяют разработчику явно указать, какое соглашение о передаче параметров будет использовано для той или ивой процедуры (функции).
Память для типов данных (RTTI-информация)
Современные компиляторы с объектно-ориентированных языков программирования предусматривают что результирующая программа может обрабатывать не только переменные, константы и другие структуры данных, но и информацию о типах данных, описанных в исходной программе. Эта информация получила название RTTI — Run Time Type Information — «информация о типах во время выполнения».
Состав RTTI-информации определяется семантикой входного языка и реализацией компилятора. Как правило, для каждого типа данных в объектно-ориентированном языке программирования создается уникальный идентификатор типа данных, который используется для сопоставления типов. Для него может храниться наименование типа, а также другая служебная информация, которая используется компилятором в коде результирующей программы. Вся эта информация может быть в той или иной мере доступна пользователю. Еще одна цель хранения RTTI информации — обеспечить корректный механизм вызова виртуальных процедур и функций (так называемое «позднее связывание»), предусмотренный во всех объектно-ориентированных языках программирования [8, 34, 35, 76,71].
RTTI-информация хранится в результирующей программе в виде RTTI-таблицы RTTI-таблица представляет собой глобальную статическую структуру данных, которая создается и заполняется в момент начала выполнения результирующей программы Компилятор с объектно-ориентированного входного языка отвечает за порождение в результирующей программе кода, ответственного за заполнение RTTI-таблицы.
Каждому типу данных в RTTI-таблице соответствует своя область данных, в которой хранится вся необходимая информация об атом типе данных, его взаимосвязи с другими типами данных в обшей иерархии типов данных программы, а также все указатели на код виртуальных процедур и функций, связанных с этим типом. Всю эту информацию и указатели размещает для каждого типа данных в RTTI-таблице код, автоматически порождаемый компилятором.
В принципе всю эту информацию можно было бы разместить непосредственно в памяти, статически или динамически отводимой для каждого экземпляра объекта (класса) того или иного типа. Но поскольку данная информация для всех объектов одного и того же типа совпадает, то это привело бы к нерациональному использованию оперативной памяти. Поэтому компилятор размещает всю информацию по типу данных в одном месте, а каждому экземпляру объекта (класса) при выделении памяти для него добавляет только небольшой объем служебной информации, связывающей этот объект с соответствующим ему типом данных (как правило, эта служебная информация является указателем на нужную область данных в RTTI-таблице). При статическом распределении памяти под экземпляры объектов (классов) компилятор сам помещает в нее необходимую служебную информацию, при динамическом распределении — порождает код, который заполнит эту информацию во время выполнения программы (поскольку RTTI- таблица является статической областью данных, адрес ее известен и фиксирован).
На рис. 14.3 приведена схема, иллюстрирующая построение RTTI-таблицы и ее связь с экземплярами объектов в результирующей программе
Рис 14 3 Взаимосвязь RTTI-таблицы с экземплярами объектов в результирующей программе
Компилятор всегда сам автоматически строит код, ответственный в результирующей программе за создание и заполнение RTTI-таблицы и за ее взаимосвязь с экземплярами объектов (классов) различных типов. Разработчику не надо за-
ботиться об этом, тем более что он, как правило, не может воздействовать на служебную информацию, помещаемую компилятором в RTTI-таблиц^
Проблемы могут появиться в том случае, когда некоторая программа взаимодей- ствует с динамически загружаемой библиотекой. При «том могут возникнуть ситуации, вызванные несоответствием типов данных в программе и библиотеке, хотя и та и другая могут быть построены на одном и том же входном языке с помощью одного и того же компилятора. Дело в том, что код инициализации программы, порождаемый компилятором, ответственен за создание и заполнение своей RTTI-таблпцы, а код инициализации библиотеки, порождаемый тем же компилятором, — своей RTTI-таблпцы. Если программа и динамически загружаемая библиотека работают с одними и теми же типами данных, то эти RTTI- таблицы будут полностью совпадать, но при этом они не будут одной и той же таблицей. Это уже должен учитывать разработчик прогриммы при проверке соответствия типов данных.
Еще большие сложности могут возникнуть, если программа, построенная на основе входного объектно-ориентированного языка программирования, будет взаимодействовать с библиотекой или программой, построенной на основе другого объектно-ориентированного языка (или даже построенной на том же языке, по с помощью другого компилятора). Поскольку формат RTTI-таблиц и состав хранимой в них служебной информации не специфицированы для всех языков программирования, то они полностью зависят от используемой версии компилятора. Соответствующим образом различается и порождаемый компилятором код результирующей программы. В общем случае организовать такого рода взаимодействие между двумя фрагментами кода, порожденного двумя различными компиляторами, напрямую практически невозможно (о других возможностях такого рода взаимодействия см. в разделе «Примеры современных систем программирования», глава 15).
Генерация кода. Методы генерации кода
Общие принципы генерации кода.
Синтаксически управляемый перевод
Генерация объектного кода — это перевод компилятором внутреннего представления исходной программы в цепочку символов выходного языка. Генерация объектного кода порождает результирующую объектную программу на языке ассемблера или непосредственно на машинном языке (в машинных кодах). Внутреннее представление программы может иметь любую структуру в зависимости от реалйзации компилятора, в то врёмя как результирующая программа всегда представляет собой линейную последовательность команд. Поэтому генерация объектного кода (объектной программы) в любом случае должна выполнять действия, связанные с преобразованием сложных синтаксических структур в линейные цепочки.
Генерацию кода можно считать функцией, определенной па синтаксическом дереве и па информации, содержащейся в таблице идентификаюров. Поэтому генерация объектного кода выполняется после того, как выполнен синтаксический авали $ программы и все необходим^ действия ио подготовке к генерации кода: распределено адресное пространство под функции и переменные, проверено соответствие имен и типов переменных, констант и функций в синтаксических конструкциях исходной программы и т. д. Характер отображения входной профаммы в последовательность команд, выполняемого генерацией, зависит от входного языка, архитектуры вычислительной системы, на которую ориентирована результирующая ирофамма, а также от качества желаемого объектного кода.
В идеале компилятор должен выполнить синтаксический разбор всей входной программы, затем провести ее семантический анализ, после чего приступить к подготовке генерации и непосредственно генерации кода. Однако такая схема работы компилятора практически почти никогда не применяется Дело в том, что в общем случае ни один семантический анализатор и ни один компилятор не способны проанализировать и оценить смысл всей входной программы в целом Формальные методы анализа семантики применимы только к очень незначительной части возможных входных профамм. Поэтому у компилятора нет практической возможности порождать эквивалентную выходную программу па основе всей входной программы.
Как правило, компилятор выполняет генерацию результирующего кода поэтапно, на основе законченных синтаксических конструкций входной профаммы. Компилятор выделяет закопченную синтаксическую консфукцию из текста входной программы, порождает для нее фрагмент результирующего кода и помещает ею в текст выходной профаммы. Затем он переходит к следующей синтаксической конструкции. Так продолжается до тех пор, пока не будет разобрана вся входная ирофамма. В качестве анализируемых законченных синтаксических консфук- ций выступают операторы, блоки операторов, описания процедур и функций. Их конкретный состав зависит от входного языка и реализации компилятора.
Смысл (семантику) каждой такой синтаксической конструкции входного языка можно определить, исходя из ее типа, а тип определяется синтаксическим анализатором на основании грамматики входного языка. Примерами типов синтаксических конструкций могут служить операторы цикла, условные операторы, операторы выбора и т. д. Один и те же типы синтаксических конструкций харак терны для различных языков программирования, при этом они различаются синтаксисом (который задается грамматикой языка), но имеют схожий смысл (который определяется семантикой). В зависимости от типа синтаксической консфукции выполняется генерация кода результирующей программы, соответствующего данной синтаксической конструкции. Для семантически схожих конструкций различных входных языков программирования может порождаться типовой результирующий код.
Чтобы компилятор мог построить код результирующей прот раммы для синтаксической конструкции входного языка, часто используется метод, называемый синтаксически управляемым переводом — СУ переводом СУ-перевод — это основной метод порождения кода результирующей программы на основании ре
зультатов синтаксического разбора. Для удобства понимания сути мет ода можно считать, что результат синтаксического разбора представлен в виде дерева синтаксического разбора (либо же дерева операций), хотя в реальных компиляторах это не всегда так.
Суть принципа СУ-перевода заключается в следующем: с каждой вершиной дерева синтаксического разбора N связывается цепочка некоторого промежуточного кода C(N). Код для вершины N строится путем сцепления (конкатенации) в фиксированном порядке последовательности кода С(М) и последовательностей кодов, связанных со всеми вершинами, являющимися прямыми потомками N В свою очередь, для построения последовательностей кода прямых потомков вершины N потребуется найти последовательности кода для их потомков — потомков второго уровня вершины N — и т. д. Процесс перевода идет, таким образом, снизу вверх в строго установленном порядке, определяемом структурой дерева.
Для того чтобы построить СУ-перевод по заданному дереву синтаксического разбора, необходимо найти последовательность кода для корня дерева. Поэтому для каждой вершины дерева порождаемую цепочку кода надо выбирать таким образом, чтобы код, приписываемый корню дерева, оказался искомым кодом для всего оператора, представленного этим деревом. В общем случае необходимо иметь единообразную интерпретацию кода C(N), которая бы встречалась во всех ситуациях, где присутствует вершина N. В принципе эта задача может оказаться нетривиальной, так как требует оценки смысла (семантики) каждой вершины дерева. При применении СУ-перевода задача интерпретации кода для каждой вершины дерева решается только разработчиком компилятора.
Возможна модель компилятора, в которой синтаксический анализ входной программы и генерация кода результирующей программы объединены в одну фазу Такую модель можно представить в виде компилятора, у которого операции генерации кода совмещены с операциями выполнения синтаксического разбора Для описания компиляторов такого типа часто используется термин «СУ-ком- пиляция» (синтаксически управляемая компиляция).
Схему СУ-компиляции можно реализовать не для всякого входного языка программирования. Если принцип СУ-перевода применим ко всем входным КС- языкам, то применить СУ-компиляцию оказывается не всегда возможным. Однако известно, что схемы перевода на основе СУ-компиляции можно построить для многих из широко распространенных классов КС-языков, в частности для LR- и LL-языков [6, 15].
В процессе СУ-перевода и СУ-компиляции не только вырабатываются цепочки текста выходного языка, но и совершаются некоторые дополнительные действия, выполняемые самим компилятором. В общем случае схемы СУ-перевода могут предусматривать выполнение следующих действий.
помещение в выходной поток данных машинных кодов или команд ассемблера, представляющих собой результат работы (выход) компилятора;
выдача пользователю сообщений об обнаруженных ошибках и предупреждениях (которые должны помещаться в выходной поток, отличный от потока, используемого для команд результирующей программы);
порождение и выполнение команд, указывающих, что некоторые действия должны быть произведены самим компилятором (например, операции, выполняемые над данными, размещенными в таблице идентификаторов).
Ниже рассмотрены основные технические вопросы, позволяющие реализовать схемы СУ перевода и СУ-компиляции. Но прежде чем рассматривать их, необходимо разобраться со способами внутреннего представления программы в компиляторе. От того, как исходная программа представляется внутри компилятора, во многом зависят методы, используемые для обработки команд этой программы.
Способы внутреннего представления программ
Возможны различные формы внутреннего представления синтаксических конструкций исходной программы в компиляторе. На этапе синтаксического разбора часто используется форма, именуемая деревом вывода (методы его построения рассматривались выше). Но формы представления, используемые на этапах синтаксического анализа, оказываются неудобными в работе при генерации и оптимизации объектного кода. Поэтому перед оптимизацией и непосредственно перед генерацией объектного кода внутреннее представление программы может преобразовываться в одну из соответствующих форм записи.
Все внутренние представления программы обычно содержат в себе две принципиально различные вещи — операторы и операнды. Различия между формами внутреннего представления заключаются лишь в том, как операторы и операнды соединяются между собой. Также операторы и операнды должны отличаться друг от друга, если они встречаются в любом порядке. За различение операндов и операторов, как уже было сказано выше, отвечает разработчик компилятора, который руководствуется семантикой входного языка.
Известны следующие формы внутреннего представления программ':
связочные списочные структуры, представляющие синтаксические деревья;
многоадресный код с явно именуемым результатом (тетрады);
многоадресный код с неявно именуемым результатом (триады);
обратная (постфиксная) польская запись операций;
ассемблерный код или машинные команды.
В каждом конкретном компиляторе может использоваться одна из этих форм, выбранная разработчиками. Но чаще всего компилятор не ограничивается ис-
Существуют три формы записи выражений — префиксная, инфиксная и постфиксная. При префиксной записи операция записывается перед своими операндами, при инфиксной — между операндами, а при постфиксной — после операндов Общепринятая запись арифметических выражений является примером инфиксной записи. Запись математических функций и функции в языках программирования является префиксной (другие примеры префиксной записи — команды ассемблера, триады и тетрады, в том виде, как они рассмотрены далее). Постфиксная запись в повседневной жизни встречается редко. С нею сталкиваются разве что пользователи стековых калькуляторов и программисты на языке Forth.
пользованием только одной формы для внутреннего представления программы. На различных фазах компиляции могут использоваться различные формы, которые по мере выполнения проходов компилятора преобразуются одна в другую. Нс все из перечисленных форм широко используются в современных компиляторах, и об этом будет сказано по мерс их рассмотрения.
Некоторые компиляторы, незначительно оптимизирующие результирующий код, генерируют объектный код по мере разбора исходной программы. В этом случае применяется схема СУ-компиляции, ко1да фазы синтаксического разбора, семантического анализа, подготовки и генерации объектного кода совмещены в одном проходе компилятора. Тогда внутреннее представление программы существует только условно в виде последовательности шагов алгоритма разбора. В любом случае компилятор всегда будет работать с представлением программы в форме машинных команд — иначе он не сможет построить результирующую программу. Далее все перечисленные формы представления рассматриваются более подробно.
Синтаксические деревья
Синтаксические деревья уже были рассмотрены выше. Эго структура, представляющая собой результат работы синтаксического ашиигзатора. Опа отражает синтаксис конструкций входного языка и явно содержит в себе полную взаимосвязь операции. Очевидно также, что синтаксические деревья — это машиппо- пезависимая форма внутреннего представления программы.
Недостаток синтаксических деревьев заключается в том, что они подставляют собой сложные связные структуры, а потому ин могут быть тривиальным образом преобразованы в линейную последовательность команд результирующей программы. Тем не менее они удобны при работе с внутренним представлением программы па тех этапах, когда не г необходимости непосредственно обращаться к командам результирующей программы.
Синтаксические деревья могут быть преобразованы в другие формы внутреннего представления программы, представляющие собой линейные списки, с учетом семантики входного языка. Алгоритмы такого рода преобразований рассмотрены далее. Эти преобразования выполняются на основе принципов СУ-компиляции.
Многоадресный код с явно именуемым результатом (тетрады)
Тетрады представляют собой запись операций в форме из четырех составляющих: операция, два операнда и результат операции. Например, тетрады могут выглядеть так: <операция>(<операнд1>,<операнд2>.<результат>).
Тетрады представляют собой линейную последовательность команд. При вычислении выражения, записанного в форме тетрад, они вычисляются одна за другой последовательно. Каждая тетрада в последовательности вычисляется так: операция, заданная тетрадой, выполняется над операндами и результат се выполнения помещается в переменную, заданную результатом тетрады. Если какой-то из операндов (или оба операнда) в тетраде отсутствует (например, если тетрада представляет собой унарную операцию), то он может быть опущен или заменен пустым операндом (в зависимости от принятой формы записи и ее реализации)
Результат вычисления тетрады никогда опущен быть не может, иначе тетрада полностью теряет смысл. Порядок вычисления тетрад может быть изменен, но только если допустить наличие тетрад, целенаправленно изменяющих этот порядок (например, тетрады, вызывающие переход на несколько шагов вперед или назад при каком-то условии).
Тетрады представляют собой линейную последовательность, а потому для них несложно написать тривиальный алгоритм, который будет преобразовывать последовательность тетрад в последовательность команд результирующей программы (либо последовательность команд ассемблере). В этом их преимущество перед синтаксическими деревьями. А в отличие от команд ассемблера тетрады не зависят от архитектуры вычислительной системы, на которую ориептироваиа результирующая программа. Поэтому они представляют собой машинно-независимую форму внутреннего представления программы.
Тетрады требуют больше памяти для своего представления, чем триады, они также нс отражают явно взаимосвязь операций между собой. Кроме того, есть сложности с преобразованием тетрад в машинный код, так как они плохо отображаются в команды ассемблера и машинные коды, поскольку в наборах команд большинст ва современных компьютеров редко встречаются операции с тремя операндами.
Например, выражение A:-B*C+D-B*10, записанное в виде тетрад, будет иметь вид:
* ( В. С. Т1 )
+ ( Tl.D, Т2 )
* ( В, 10,ТЗ )
- ( Т2.ТЗ.Т4 )
- ( Т4.0. А )
Здесь все операции обозначены соответствующими знаками (при этом присвоение также является операцией). Идентификаторы Т1. ... Т4 обозначают временные переменные, используемые для храпения результатов вычисления тетрад. Следует обратить внимание, что в последней тетраде (присвоение), которая требует только одного операнда, в качестве второго операнда выступает незначащий операнд «0».
Многоадресный код с неявно именуемым результатом (триады)
Триады представляют собой запись операций в форме из трех составляющих: операция и два операнда. Например, триады могут иметь вид: <операция>(<операнд1>, <операнд2>). Особенностью триад является то, что один или оба операнда могут быть ссылками на другую триаду в том случае, если в качество операнда данной Триады выступает результат выполнения другой триады. Поэтому триады при записи последовательно нумеруют для удобства указания ссылок одних триад на другие (в реализации компилятора в качестве ссылок можно использовать не номера триад, а непосредственно ссылки в виде указателей — тогда при изменении нумерации и порядка следования триад менять ссылки не требуется).
Триады представляют собой линейную последовательность команд. При вычислении выражения, записанного в форме триад, они вычисляются одна за другой последовательно. Каждая триада в последовательности вычисляется так операция, заданная триадой, выполняется над операндами, а если в качестве одного из операндов (или обоих операндов) выступает ссылка на другую триаду, то берегся результат вычисления той триады. Результат вычисления триады нужно сохранять во временной памяти, так как он может быть затребован последующими триадами. Если какой-то из операндов в триаде отсутствует (например, если триада представляет собой унарную операцию), то он может быть опущен или заменен пустым операндом (в зависимости от принятой формы записи и ее реа литации). Порядок вычисления триад, как и для тетрад, может быть изменен, ио только если допустить наличие триад, целенаправленно изменяющих этот порядок (например, триады, вызывающие переход на несколько шагов вперед или назад при каком-то условии).
Триады представляют собой линейную последовательность, а потому для них несложно написать тривиальный алгоритм который будет преобразовывать последовательность триад в последовательность команд результирующей программы (либо последовательность команд ассемблера). В этом их преимущество перед синтаксическими деревьями. Однако здесь требуется также и алгоритм, отвечающий за распределение памяти, необходимой для храпения промежуточных результатов вычисления, так как временные переменные дня этой цели не используются. В этом отличие триад or тетрад.
Так же как и тетрады. триады не зависят от архитШстуры вычислительной системы, на которую ориентирована результирующая программа. Поэтому они представляют собой машинно-независимую форму внутреннего представления программы.
Трйады требуют меньше памяти для своего представления, чем тетрады, они также явно отражают взаимосвязь операций между собой, что делает их применение удобным. Необходимость иметь алгоритм, отвечающий за распределение памяти для хранения промежуточных результатов, не является недостатком, так как удобно распределять результаты не только ио доступным ячейкам временной памяти, но и ио имеющимся регистрам процессора. Это дает определенные преимуществе, Триады ближе к двухадресным машинным командам, чем тетрады, а именно эти команды более всего распространены в наборах команд большинства современных компьютеров.
Например, выражение A:=B*C+D-B*10, записанное в виде триад, будет иметь вид:
* ( В, С )
+ Г1. D )
* ( В, 10 )
- Г2, А3 )
:= ( А. А4 )
Здесь операции обозначены г ответе дующим знаком (при этом присвоение также является операцией), а знак А означает ссылку операнда одной триады па результат другой.
if slnput[1] in AutoStop then iState :- AUT0_S else iState .- AUT0_E: end:
AUT0_X:
case slnput[i] of
'a’./f: iState - AUT0_G
else IState :- AUT0_E: end:
AUT0_Q:
case slnput[1] of
'0'..'7'.iState-AUT0_Q.
'8V9':iState:-AUT0_D:
'u':iState:-AUT0_U;
'1';iState-AUT0_L,
'h':iState:-AUTO V
el se
if slnput[i] in AutoStop then iState - AUT0_S
else iState - AUT0_E: end:
AUTO_D:
case sInput i] of
'O'. 9' iState-AUT0_D
"u":iState:-AUTO U:
T:iState-AUT0_L
"h':iState:=AUTO V:
else
if slnput[i] in AutoStop then iState := AUTO_S else State = AUTO_E
end:
AUTO G:
case slnput[i] of •O'./g'.
’a' f: iState-AUTO_G;
ViState-AUTO_U
’1iState-AUTO_L
'h':iState:-AUTO_V
else
if slnput[i] in AutoStop then iState :- AUTO_S else iState - AUTO_E
end:
AUT0_U:
case slnput[i] of
Обратная польская запись операций
Обратная (постфиксная) польская запись — очень удобная для вычисления выражений форма записи операций и операндов. Эта форма предусматривает, что знаки операций записываются после операндов.
Более подробно обратная польская запись рассмотрена далее.
Ассемблерный код и машинные команды
Машинные команды удобны тем, что при их использовании внутреннее представление программы полностью соответствует объектному коду и сложные преобразования не требуются. Команды ассемблера представляют собой лишь форму записи машинных команд (см. раздел «Трансляторы, компиляторы и интерпретаторы — общая схема работы», глава 13), а потому в качестве формы внутреннего представления программы практически ничем не отличаются от них.
Однако использование команд ассемблера или машинных команд для внутреннего представления программы требует дополни тельных структур для отображения взаимосвязи операций. Очевидно, что в этом случае внутреннее представление программы получается зависимым от архитектуры вычислительной системы, на которую ориентирован результирующий код Значит, при ориентации компилятора па другой результирующий код потребуется перестраивать как само внутреннее представление программы, так и методы его обработки (при использовании триад или тетрад этого не требуется).
Тем не менее машинные команды это язык, па котором должна быть записана результирующая программа. Поэтому компилятор так или иначе должен работать с ними Кроме того, только обрабатывая машинные команды (или их представление в форме команд ассемблера), можно добиться наиболее эффективной результирующей программы (см. далее в разделе «Оптимизация кода. Основные методы оптимизации»). Отсюда следует, что любой компилятор работает с представлением результирующей программы в форме машинных команд, однако их обработка происходит, как правило, на завершающих этанах фазы генерации кода.
Обратная польская запись операций
Обрашая польская запись — это постфиксная запись операций. Она была предложена польским математиком Я. Лукашевичем, откуда и происходит ее название [6, т. 2, 23, 82]'.
В этой записи знаки операций записываются непосредственно за операндами По сравнению с обычной (инфиксной) записью операций в польской записи операнды следуют в том же порядке, а знаки операций — строго в порядке их вы-
1 Ошибочно думать, что польская запись выражений исиользушея для записи арифметических выражений в Польше — это не так. В Польше, как и во многих других правах мира, пользуются привычной всем инфиксной формой записи, когда знаки математических операций ставятся между операндами
полнепия. Тот факт, что в этой форме записи все операции выполняются в том порядке, в котором они записаны, делает ее чрезвычайно удобной для вычисления выражений на компьютере. Польская запись не требует учитывать приоритет операций, в ней не употребляются скобки, и в этом ее основное преимущество.
Она чрезвычайно эффективна в тех случаях, когда для вычислений используется стек. Ниже будет рассмотрен алгоритм вычисления выражений в форме обратной польской записи с использованием стека.
Главный недостаток обратной польской записи также проистекает из метода вычисления выражений в ней: поскольку используется стек, то для рабо ты с ним всегда доступна только верхушка стека, а это делает крайне затруднительной оптимизацию выражений в форме обратной польской записи. Практически выражения в форме обратной польской записи почти не поддаются оптимизации.
Но там, где оптимизация вычисления выражений не требуется или не имеет большого значения, обратная польская запись оказывается очень удобным методом внутреннего представления программы.
Обратная польская запись была предложена первоначально для записи арифме тических выражений. Однако этим се применение не ограничивается. В компиляторе можно порождать код в форме обратной польской записи для вычисления практически любых выражений. Для этого достаточно ввести знаки, предусматривающие вычисление соответствующих операций В том числе, возможно ввести операции условного и безусловною перехода, предполагающие изменение последовательности хода вычислений и перемещение вперед пли штад па некоторое количество шагов в зависимости от результата на верхушке стека [6, т. 2, 74, 82] Такой подход позволяет очень широко применять форму обратной польской записи.
Преимущества и недостатки обратной польской записи определяют и сферу ее применения. Так, она очень широко используется для вычисления выражений в ин герпретаторах и командных процессорах, где оптимизация вычислений либо отсутствует вовсе, либо не имеет существенного значения
Вычисление выражений с помощью обратной польской записи
Вычисление выражении в обратной польской записи идет элементарно просто с помощью стека. Для этого выражение просматривается в порядке слева наира во, и встречающиеся в нем элементы обрабатываются по следующим правилам:
. Если ветрешется операнд, то он помещается в стек (попадает на верхушку стека).
Если встречается знак унарной операции (операции, требующей одного операнда), то операнд выбирается с верхушки стека, операция выполняется и результа! помещается в стек (попадает на верхушку стека).
Если встречается знак бинарной операции (операции, требующей двух операндов), то два операнда выбираются с верхушки стека, операция выполняется и результат помещается в стек (попадает на верхушку стека).
Вычисление выражения закапчивается, когда достигается конец записи выражения. Результат вычисления при этом всегда находится па верхушке стека.
Очевидно, что данный алгоритм можно легко расширить и для ботее сложных операций, требующих три и более операндов.
На рис. 14.4 рассмотрены примеры вычисления выражений в обратной польской записи.
Вычисление выражения 6+(7+10) * 4=74
Вычисление выражения 6+7+10 * 4 = 53
Рис. 14.4. Вычисление выражений в обратной польской записи с использованием стека
Схема СУ-компиляции для перевода выражений в обратную польскую запись
Существует множество алгоритмов, которые позволяют преобразовывать выражения из обычной (инфиксной) формы записи в обратную польскую запись Далее рассмотрен алгоритм, построенный на основе схемы СУ-компиляции для языка арифметических выражений с операциями +, -, * и /. В качестве основы алгоритма выбрана грамматика арифметических выражений, которая уже многократно рассматривалась ранее в качестве примера.
Эта грамматика приведена далее.
G({+.-./.*.a.b). (S.T.E). Р. S): Р:
S -» S+T | S-Т | Т
Т -» Т*Е I Т/Е I Е
Е -» (S) | а | b ■
Схему СУ-компиляции будем строить с таким расчетом, что имеется выходная цепочка символов R и известно текущее положение указателя в этой цепочке р. Распознаватель, выполняя свертку или подбор альтернативы ио правилу грамматики, может записывать символы в выходную цепочку и менять текущее положение указателя в ней.
Построенная таким образом схема СУ-компиляции для преобразования арифметических выражений в форму обратной польской записи оказывается исключительно простой 16, т. 2]. Она приведена ниже. В пей с каждым правилом грамматики связаны некоторые действия, которые записаны через ; (точку с запятой) сразу за правой частью каждого правила. Если никаких действий выполнять не нужно, в записи следует пустая цепочка (X).
S -» S+T: R(p) - "1". р-р+1
5 -» S-T: R(p) - '-". р-р+1
S -» Т X
Т -» Т*Е; R(p) - "*". р-р+1
Т -» Т/Е; R(p) - "/" р-р+1
Т -» Е: X
Е -» (S): X
Е -» a; R(p) - "а". р=р+1
Е -> b: R(p) - "b". р-р+1
Эту схему СУ-компиляции можно использовать для любого распознавателя без возвратов, допускающего разбор входных цепочек на основе правил данной грамматики (например для распознавателя па основе грамматики операторного предшествования, рассмотренного в разделе «Восходящие распознаватели КС- языков без возвратов», глава 12). Тогда, в соответствии с принципом СУ-перевода, каждый раз выполняя свертку на основе некоторого правила, распознаватель будет выполнять также и действия связанные с этим правилом. В результате его работы будет построена цепочка R, содержащая представление исходного выражения в форме обратной польской записи (строго говоря, в данном случае автомат будет представлять собой уже не только распознаватель, по и преобразователь цепочек [6, 42]).
Представленная схема СУ-компиляции, с одной стороны, иллюстрирует, насколько просто выполнить преобразование выражения в форму обратной польской записи, а с другой стороны, демонстрирует, как па практике работает сам принцип СУ-компиляции
Схемы СУ-перевода
Выше был описан принцип СУ-перевода, позволяющий получить линейную последовательность команд результирующей программы или внутреннего пред-
ставления программы в компиляторе на основе результатов синтаксического разбора. Теперь построим вариант алгоритма генерации кода, который получает на входе дерево операций и создает по нему фрагмент объектного кода для линейного участка результирующей программы. Рассмотрим примеры схем СУ-пе- рсвода для арифметических операций. Эти схемы достаточно просты, и на их основе можно проиллюстрировать, как выполняется СУ-перевод в компиляторе при генерации кода.
В качестве формы представления результатов синтаксического разбора возьмем дерево операций (метод построения дерева операций рассмотрен выше). Для построения внутреннего представления объектного кода (в дальнейшем — просто кода) по дереву операций может использоваться простейшая рекурсивная процедура обхода дерева. Можно использовать и другие методы обхода дерева — важно, чтобы соблюдался принцип, что нижележащие операции в дереве всегда выполняются перед вышележащими операциями (порядок выполнения операций одного уровня не важен, он не влияет на результат и зависит от порядка обхода вершин дерева).
Процедура генерации кода по дереву операций прежде всего должна определить тип узла дерева. Он соответствует типу операции, символом которой помечен узел дерева. После определения типа узла процедура строит код для узла дерева в соответствии с типом операции. Если все узлы следующего уровня для текущего узла есть листья дерева, то в код включаются операнды, соответствующие этим листьям, и получившийся код становится результатом выполнения процедуры. Иначе процедура должна рекурсивно вызвать сама себя для генерации кода нижележащих узлов дерева и результат выполнения включить в свой порожденный код. Фактически процедура генерации кода должна для каждого узла дерева выполнить конкатенацию цепочек команд, связанных с текущим узлом и с нижележащими узлами. Конкатенация цепочек выполняется таким образом, чтобы операции, связанные с нижележащими узлами, выполнялись до выполнения операции, связанной с текущим узлом.
Поэтому для построения внутреннего представления объектного кода по дереву вывода в первую очередь необходимо разработагь формы представления объектного кода для четырех случаев, соответствующих видам текущего узла дерева вывода:
оба нижележащих узла дерева — листья, помеченные операндами;
только левый нижележащий узел является листом дерева;
только правый нижележащий узел является листом дерева;
оба нижележащих узла не являются листьями дерева.
Рассмотрим построение двух видов внутреннего представления по дереву вывода:
построение списка триад по дереву вывода,
построение ассемблерного кода по дереву вывода.
Далее рассматриваются две функции, реализующие схемы СУ-перевода для каждого их этих случаев.
Пример схемы СУ-перевода дерева операций на язык ассемблера
В качестве языка ассемблера возьмем язык ассемблера процессоров типа Intel 80x86. При этом будем считать, что операнды могут быть помещены в 16-разряд- ные регистры процессора и в коде результирующей объектной программы могу г использоваться регистры АХ (аккумулятор) и DX (регистр данных), а также стек для хранения промежуточных результатов.
Функцию, реализующую перевод узла дерева в последовательность команд ас семблера назовем Code. Входными данными функции должна быть информация об узле дерева операции. В ее реализации на каком-либо языке программирования эти данные можно представить в виде указателя па соответствующий узел дерева операции. Выходными данными является последовательность команд языка ассемблера. Будем считать, что она передастся в виде строки, возвращаемой в качестве результата функции.
Тогда четырем формам текущего узла дерева для каждой арифметической операции будут соответствовать фрагменты кода на языке ассемблера, приведенные в табл. 14.1.
Каждой допустимой арифметической операции будет соответствовать своя команда па языке ассемблера. Если взять в качестве примера операции сложения (+), вычитания (-), умножения (*) и деления (/), ю им будут соответствовать команды add, sub, mul и div. Причем в ассемблере Intel 80x86 от типа операции зависит не только тип, но и синтаксис команды (операции mul и div в качестве первого операнда всегда предполагают регистр процессора АХ, который не нужно указывать в команде). Соответствующая команда должна записываться вместо act при порождении кода в зависимости от типа узла дерева.
Таблица 14.1. Преобразование узлов дерева вывода в код на языке ассемблера для арифметических операций
Вид узла дерева
Результирую щий код
Примечание
act
mov ах, operl act ах, орсг2
act — команда соответствующей операции
oporl, орсг2 — операнды (листья дерева)
орет 1
орвг2
act
Code (Узел 2) mov dx, ах mov ах, operl act. ах, dx
Узел 2 - нижележащий узел (не л пег!) дерева Code (Узел 2) — код, порождаемый процедурой для нижележащего узла
орет 1
Узел 2
Вид узла дереваВид узла дереваРезультирую Примечание щий код
Вид узла дерева
Вид узла дерева
Code (Узел 2) act ах, орег2Code (Узел 2) act ах, орег2Code (Узел 2) push ахCode (Узел 3) mov dx, ахpop ах act ах, dxCode (Узел 2) push ахCode (Узел 3) mov dx, ахpop ах act ах, dxCode (Узел 2) — код, порождаемый процедурой для нижележащею узла
Code (Узел 2) act ах, орег2
Code (Узел 2) act ах, орег2
Code (Узел 2) push ах
Code (Узел 3) mov dx, ах
pop ах act ах, dx
Code (Узел 2) push ах
Code (Узел 3) mov dx, ах
pop ах act ах, dx
Code (Узел 2) — код, порождаемый процедурой для нижележащего узла Code (Узел 3) — код. порождаемый процедурой для нижележащего узла push и pop — команды сохранения результатов в стеке и извлечения результатов из стека
Код, порождаемый для операции присвоения результата, будет отличаться от кода, порождаемого для арифметических операций. Кроме того, семантика языка требует, чтобы в левой части операции присвоения всегда был операнд, поэтому для нее возможны только два типа узлов дерева, влекущие порождение кода (два других типа узлов должны приводить к сообщению об ошибке, которая в компиляторе обнаруживается синтаксическим анализатором) Фрагменты кода для этой опе|жции приведены ниже в табл. 1ф2.
Вид узла дереваВид узла дереваРезультирую щии кодРезультирую щии кодПримечаниеПримечаниеТаблица 14.2. Преобразование узлов дерева вывода в код на языке ассемблера для операции присвоения
Вид узла дерева
Вид узла дерева
Результирую щии код
Результирую щии код
Примечание
Примечание
mov ах, орег2 mov oporl, ахmov ах, орег2 mov oporl, ахCode (Узел 2) mov opcrl, ахCode (Узел 2) mov opcrl, ах
mov ах, орег2 mov oporl, ах
mov ах, орег2 mov oporl, ах
Code (Узел 2) mov opcrl, ах
Code (Узел 2) mov opcrl, ах
opcrl, opcr2 — операнды (листья дерева)
Узел 2 — нижележащий узел (не лист!) дерева Code (Узел 2) — код, порождаемый процедурой для нижележащего узла
Теперь последовательность порождаемого кода определена для всех возможных типов узлов дерева.
Рассмотрим в качестве примера выражение A:=B*C+D-B*10 Соответствующее ему дерево вывода приведено па рис. 14 5.
Рис 14.5. Дерево операций для арифметического выражения «A:=B*C+D-B* 10»
Построим последовательность команд языка ассемблера, соответствующую дереву операций на рис. 14.5. Согласно принципу СУ-перевода, построение начинается от корня дерева. Для удобства иллюстрации рекурсивного построения последовательности команд все узлы дерева помечены от 1)1 до U5. Рассмотрим последовательность построения цепочки команд языка ассемблера по шагам рекурсии. Эта последовательность приведена ниже.
Шт 1.
Шаг 2.
ШтЗ
Code(U2) mov А. ахCode(U3) push ах Code(U5) mov dx. ax pop ax sub ax. dx mov A. axCode(U4) add ax. 0 push ax Code(U5) mov dx. ax pop ax sub ax. dx mov A.axmov ax. В mul C add ax. DCode(U2) mov А. ахCode(U3) push ах Code(U5) mov dx. ax pop ax sub ax. dx mov A. axCode(U4) add ax. 0 push ax Code(U5) mov dx. ax pop ax sub ax. dx mov A.axmov ax. В mul C add ax. DШаг. 4.
Code(U2) mov А. ах
Code(U3) push ах Code(U5) mov dx. ax pop ax sub ax. dx mov A. ax
Code(U4) add ax. 0 push ax Code(U5) mov dx. ax pop ax sub ax. dx mov A.ax
mov ax. В mul C add ax. D
Code(U2) mov А. ах
Code(U3) push ах Code(U5) mov dx. ax pop ax sub ax. dx mov A. ax
Code(U4) add ax. 0 push ax Code(U5) mov dx. ax pop ax sub ax. dx mov A.ax
mov ax. В mul C add ax. D
Шаг 5.
push ах Code(U5) mo dx. ах pop ах sub ах. dx mov А. ах
mov
ax.
в
mul
C
add
ax.
0
push ax
mov
ax.
в
mul
10
mov
dx.
ax
pop
ax
sub
ax.
dx
mov
A. .
ax
Полученный объектный код на языке ассемблера, очевидно, может быть оптимизирован. Вопросы оптимизации результирующего кода рассмотрены далее в разделе «Оптимизация кода. Основные методы оптимизации».
В рассмотренном примере при порождении кода преднамеренно не были приняты во внимание многие вопросы, возникающие при построении реальных компиляторов. Это было сделано для упрощения примера. Например, фрагменты кода, соответствующие различным узлам дерева, принимают во внимание тип операции, но никак не учитывают тип операндов. А в языке ассемблера операндам различных типов соответствуют существенно различающиеся команды, могут использоваться различные регистры (например, если сравнить операции с целыми числами и числами с плавающей точкой). Некоторые типы операндов (например, строки) не могут быть обработаны одной командой языка, а требуют целой последовательности команд. Такую последовательность разумнее оформить в виде функции библиотеки языка, тогда выполнение операции будет представлять собой вызов этой функции.
Кроме того, ориентация на определенный язык ассемблера, очевидно, сводит на нет универсальность метода. Каждый тип операций жестко связывается с последовательностью определенных команд, ориентированных на некоторую архитектуру вычислительной системы.
Все эти требования ведут к тому, что в реальном компиляторе при генерации кода надо принимать во внимание очень многие особенности, зависящие как от семантики входного языка, так и от архитектуры целевой вычислительной системы, на которой будет выполняться результирующая программа. В результате увеличится число различных типов узлов дерева, каждому из которых будет соответствовать своя последовательность команд. Это может осложнить алгоритм генерации кода, что особенно нежелательно в том случае, если порождаемы!! код не является окончательным и требует дальнейшей обработки.
Указанных проблем можно избежать, если порождать код не в виде цепочки команд ассемблера, а в виде последовательности команд машинно-независимого метода внутреннего представления программы — например, тетрад или триад.
Пример схемы СУ-перевода дерева операций в последовательность триад
Как и в случае с порождением команд ассемблера, функцию, реализующую перевод узла дерева в последовательность триад, назовем Code Входные и выходные данные у этой функции будут те же, что и при работе с командами ассемблера. Триады являются машинно-независимой формой внутреннего представления в компиляторе результирующей объектной программы, а потому нс требуют оговорки дополнительных условий при генерации кода. Триады взаимосвязаны между собой, поэтому для установки корректной взаимосвязи функция генерации кода должна учитывать также текущий номер очередной триады (1).
Будем рассматривать тс же вариант ы узлов дерева, что и для команд ассемблера. Тогда четырем формам текущего узла дерева будут соответствовать последовательности триад объектного кода, приведенные в табл. 14.3.
Таблица 14.3. Преобразование типовых узлов дерева вывода в последовательность триадiТаблица 14.3. Преобразование типовых узлов дерева вывода в последовательность триадiВид узла дереваВид узла дереваРезультирую щий кодi) act (opcrl,<>рсг2)Результирую щий кодi) act (opcrl,<>рсг2)ПримечаниеПримечаниеact — тип цтиалыoperl, oper2 — операнды (листья дерена вывода)act — тип цтиалыoperl, oper2 — операнды (листья дерена вывода)
Таблица 14.3. Преобразование типовых узлов дерева вывода в последовательность триад
i
Таблица 14.3. Преобразование типовых узлов дерева вывода в последовательность триад
i
Вид узла дерева
Вид узла дерева
Результирую щий код
i) act (opcrl,
<>рсг2)
Результирую щий код
i) act (opcrl,
<>рсг2)
Примечание
Примечание
act — тип цтиалы
operl, oper2 — операнды (листья дерена вывода)
act — тип цтиалы
operl, oper2 — операнды (листья дерена вывода)
i) Code (Узел 2, i) i+j) act (opcrl, Ai+j-l)i) Code (Узел 2, i) i+j) act (opcrl, Ai+j-l)Узел 2Узел 2i) Code (Узел 2 i)i+j) act (Ai+j-l, oper2)i) Code (Узел 2 i)i+j) act (Ai+j-l, oper2)
i) Code (Узел 2, i) i+j) act (opcrl, Ai+j-l)
i) Code (Узел 2, i) i+j) act (opcrl, Ai+j-l)
Узел 2
Узел 2
i) Code (Узел 2 i)
i+j) act (Ai+j-l, oper2)
i) Code (Узел 2 i)
i+j) act (Ai+j-l, oper2)
Узел 2 — нижележащий узел дерева вывода Code (Узел 2, i) — последовательность триад, порождаемая для Узла 2, начиная с т риалы с номером i, j — количество триад, порождаемых для Ума 2
Увел 2 — нижележащий узел дерена вывода Code (Узел 2, i) - i юслсдоватсл ь I юсть триад, порождаемая для Узла 2, начиная с триады с номером i, j — количество триад, порождаемых для Узла 2
'I'.'h1 State :- AUTO_W;
else
if slnput[i] in AutoStep then
1State :- AUTD_S
else 1State ;- AUTO_E: end;
AUTO_L:
case slnput[1] of
'u'-1State - AUTO_W:
else
if slnput[i] in AutoStop then 1State :- AUT0_S else iState :- AUTO E;
end.
AUT0_V:
case s!nput[i] of
V iState - AUTO W; else
if slnput[i] 1n AutoStop then
iState - AUT0_S else iState - AUTO_E end:
AUTOW:
if slnput[i] in AutoStop then
iState - AUTO_S else iState - AUTO_E. end (case);
if not (iState in [AUTO_E.AUTO_S]) then begin
sOut - sOut + slnput[i];
1 :- i ♦ 1 end:
until ((iState - AUTO_E) or (iState - AUT0_S) or (i > il));
if (iState - AUTO_S) or (i > 1L) then begin
{ Сюда надо вставить вызов функций записи пексемь sOut ) RunAuto - 1:
end
else begin { Сюда надо вставить вызов функции формирования сообщения об ошибке ) RunAuto - 0:
end;
end- { RunAuto )
Конечно, рассмотренная программа — это всего лишь пример для моделирования такого рода автомата. Она построена так, чтобы можно было четко отеле-
Вид узла дерева
Результирую щий код
Примечание
Узел 2
act
УзелЗ
i) Code (Узел 2, i) i+j) Code (Узел 3, i+j) i+j+k) act Ci+j-1. Ai+j+k-l)
Узел 2, Узел 3 — нижележащие узлы дерева вывода
Code (Узел 2, i) — 1 юсИедонатояыюс гь триад, порождаемая для Узла 2, начиная с триады с номером i, j — количество триад, порождаемых для Узла 2 Code (Узел 3, i+j) — носТкуюваталыюсть триад, порождаемая для Узла 3, начиная с триады с номером i+j, к — количество триад, порождаемых для Узла 3
Для триал не требуется учитывать операцию присваивания как отдельный тип узла дерева. Она преобразуется в триаду обычного вида.
Рассмотрим в качестве примера то же самое выражение A:-B*C+D-B*10. Соответст вующее ему дерево вывода уже было приведено на рис. 14.5.
Построим последовательность триад, соответствующую дереву операции иа рис. 14.5. Согласно принципу СУ-перевода, построение начинается от корня дерева.
Шаг 1.
1)
Code (U2. 1)
1)
:- (А. *1-1)
Шаг 2.
1)
Code (U3. 1)
J)
Code (U5, j)
1-1)
- (*J-1. *1-2)
1)
:- (A. *1-1)
Шаг 3.
1)
Code (U4. 1)
к)
+ (*k-l. D)
3)
Code.(U5. j)
1-1)
- (*j-l. *1-2)
1)
:= (A. *1-1)
Шаг. 4.
1)
* (B. C)
2)
+ Г1. 0)
3)
Code (U5. 3)
1-1)
- Cj-1. *1 2)
1)
:= (A. *1-1)
Шаг 5.
* (В. С)+ (П. 0)* (В. 10)- ("2. А3):- (А. *4)* (В. С)+ (П. 0)* (В. 10)- ("2. А3):- (А. *4)1)
* (В. С)
+ (П. 0)
* (В. 10)
- ("2. А3)
:- (А. *4)
* (В. С)
+ (П. 0)
* (В. 10)
- ("2. А3)
:- (А. *4)
2)
3)
4)
5)
Следует обратить внимание, что в данном алгоритме последовательные номера триад (а следовательно, и ссылки па них) устанавливаются не сразу. Это нс имеет значения при рекурсивной организации процедуры, но если используется другой способ обхода дерева операций, при реализации генерации кода лучше увязывать триады между собой именно по ссылке (указателю), а не по порядковому номеру.
Использование Триад позволяет сократить количество рассматриваемых вариантов узлов дерева операций. В этом случае не требуется принимать во внимание типы операндов и ориентироваться на архитектуру целевой вычислительной системы. Все эти аспекты необходимо будет принимать во внимание уже на этапе преобразования триад в последовательность команд ассемблера или непосредственно в последовательность машинных команд результирующей программы.
Как и последовательность команд ассемблера, последовательность триад можно оптимизировать. Для триад разработаны универсальные (машинно-независимые) алгоритмы оптимизации кода. Уже после их выполнения (оптимизации внутреннего представления) триады могут быть преобразованы в команды на языке ассемблера.
Оптимизация кода.
Основные методы оптимизации
Общие принципы оптимизации кода
Как уже говорилось выше, в подавляющем большинстве случаев генерация кода выполняется компилятором не для всей входной программы в целом, а последовательно для отдельных се конструкций. Полученные для каждой синтаксической конструкции входной программы фрагменты результирующего кода также последовательно объединяются в общий текст результирующей программы. При этом связи между различными фрагментами результирующего кода в полной мере не учитываются.
В свою очередь, для построения результирующего кода различных синтаксических конструкций входного языка используется метод СУ-персвода. Он также объединяет цепочки построенного кода по структуре дерева без учета их взаимосвязей (что видно из примеров, приведенных выше).
Построенный таким образом код результирующей программы может содержать лишние команды и данные. Это снижает эффективность выполнения результирующей программы. В принципе компилятор может завершить на этом гене-
рацию кода, поскольку результирующая программа построена и опа является эквивалентной по смыслу (семантике) программе на входном языке. Однако эффективность результирующей программы важна для ее рлзработчпка, поэтому большинство современных компиляторов выполняют еще один этан компиляции — оптимизацию результирующей программы (пли просто «оптимизацию»), чтобы повысить ее эффективность насколько это возможно.
Важно отметить два момента: .во-первых выделение огйпмивации в отдельный этап генерации кода — это вынужденный iiiai Компилятор вынужден производить оптимизацию построенного кода, поскольку он нс может выполнить семантический анализ всей входной программы в целом, оцепить ее смысл и, исходя из него, построить результирующую программу. Оптимизация нужна, поскольку результирующая программа строится не вся сразу, а поэтапно. Во-вторых, оптимизация — это необязательный этап компиляции. Компилятор может вообще не выполнять оптимизацию, и при этом результирующая программа будет правильной, а сам компилятор будет полностью выполнять свои функции. Однако прак тически все компиляторы так или иначе выполняют оптимизацию, поскольку их разработчики стремятся завоевать хорошие позиции на рынке средств разработки программного обеспечения. Оптимизация, которая существенно влияет на эф фективность результирующей программы, является здесь немаловажным фактором.
Теперь дадим определение понятию «оптимизация».
Оптимизация программы — это обработка связанная с нереупорядочиваписм и изменением операций в компилируемой программе с целью олучения более эф- фекгивпой результирующей объектной программы. Оптимизация выполняется па этанах подготовки к генерации и непосредственно при генерации объектного кода.
В качестве показателей эффективности результирующей программы можно ис пользовать два критерия: объем памяти, необходимый для выполнения результи рующей программы (для храпения всех ее данных и кода) и скорость выполнения (быстродействие) программы. Далеко не всегда удается выполнить оптимизацию так, чтобы удовтегворить обоим этим критериям. Зачастую сокращение необходимого протрамме объема данных ведет к уменьшению ее быстродействия, и на оборот. Поэтому для оптимизации обычно выбирается либо один из упомянутых критериев, либо некий комплексный критерий, основанный па них Выбор кри терпя оптимизации обычно выполняет непосредственно пользователь в настрой ках компилятора.
Но, лаже выбрав критерий оптимизации, в общем случае практически невозможно построить код результирующей программы, который бы являлся самым коротким или самым быстрым ко сом, соответствующим входной программе. Дело в том что нет алгоритмического способа нахождения с смой короткой или самой быстрой результирующей программы, экв валентной заданной исходной программе. Эта задача в принципе неразрешима Существуют алгоритмы, которые можно ускорять сколь угодно много раз для большого числа возможных входных данных и при этом для ругих наборов входных данных они окажутся неон тимальными |6, т. 2]. К тому же компилятор обладает весьма ограниченными средствами анализа семантики всей входной программы в целом Все, что можно
сделать на этапе оптимизации, — это выполнить над заданной программой последовательность преобразований в надежде сделать ее более эффективной.
Чтобы оцепить эффективность результирующей программы полученной с помощью того или иного компилятора, часто прибегают к сравнению ее с эквивалентной программой (программой, реализующей тот же алгоритм), полученной из исходной программы, написанной на языке ассемблера Лучшие оптимизирующие компиляторы могут получать результирующие объектные программы из сложных исходных программ, написанных па языках высокого уровня, почти не уступающие по качеству программам на языке ассемблера. Обычно соотношение эффективноегн программ, построенных с помощью компиляторов с языков высокого уровня, к эффективности программ, построенных с помощью ассемблера, составляет 1,1-1,3. То есть объектная программа, построенная с помощью компилятора с языка высокого уровня, обычно содержит па 10-30 % больше команд, чем эквивалентная ей объектная программа, построенная с помощью ассемблера, а также выполняется па 10-30 % медленнее.
Эю очень неплохие результаты, достигнутые компиляторами с языков высокого уровня, если сравнить трудозатраты па разработку программ па языке ассемблера и языке высокого уровня. Далеко нс каждую программу можно реализовать па языке ассемблера в приемлемые сроки (а значит, и выполнить напрямую приведенное выше сравнение можно только для узкого круга программ).
Оптимизацию можно выполнять на любой стадии генерации кода, начиная от завершения синтаксического разбора и вплоть до последнего этапа, когда порождается код результирующей программы. Если компилятор использует несколько различных форм внутреннего представления программы, то каждая из них может быть подвергнута оптимизации, причем различные формы внутреннего представления ориентированы на различные методы оптимизации [6, т. 2, 74, 82]. Таким образом, оптимизация в компиляторе может выполняться несколько раз на этапе сеиерации кода.
Принципиально различаются два основных вида оптимизирующих преобразований:
преобразования исходной программы (в форме ее внутреннего представления в компиляторе), не зависящие от результирующего объектного языка;
О преобразования результирующей объектной программ^.
Первый вид преобразований нс зависит от архитектуры целевой вычисли тельной системы, па которой будет выполняться результирующая программа. Обычно он основан па выполнении хорошо известных и обоснованных математических и логических преобразований, производимых над впуфсниим представлением программы (некоторые из них будут рассмотрены ниже).
Второй вид преобразований может зависеть не только err свойств объектного языка (что очевидно), но и от архитектуры вычислительной системы, на которой
будет выполняться результирующая программа. Так, например, при оптимизации может учитываться объем кэш-памяти и методы организации конвейерных операций центрального процессора. В большинстве случаев эти преобразования сильно зависят от реализации компилятора и являются «ноу-хау» производителей компилятора. Именно этот тип оптимизирующих преобразований позволяет существенно повысить эффективность результирующего кода.
Используемые методы оптимизации ни при каких условиях не должны приводить к изменению «смысла» исходной программы (то есть к таким ситуациям, когда результат выполнения программы изменяется после ее оптимизации). Для преобразований первого вида проблем обычно нс возникает. Преобразования второго вида могут вызывать сложности, поскольку не все методы оп тимизации, используемые создателями компиляторов, могут быть теоретически обоснованы и доказаны для всех возможных видов исходных программ. Именно эти преобразования могут повлиять на «смысл» исходной программы. Поэтому большинство компиляторов предусматривает возможность отключать те или иные из возможных методов оптимизации.
Нередко оптимизация ведет к тому, что смысл программы оказывается нс совсем таким, каким его ожидал увидеть разработчик программы, по не но причине наличия ошибки в оптимизирующей части компилятора, а потому, что пользователь не принимал во внимание некоторые аспекты программы, связанные с оптимизацией. Например, компилятор может исключить из программы вызов некоторой функции с заранее известным результатом, но если эта функция имела «побочный эффект» — изменяла некоторые значения в глобальной памяти, — смысл программы может измениться. Чаще всего это говорит о плохом стиле программирования исходной программы. Такие ошибки трудноуловимы, для их нахождения следует обратить внимание на предупреждения разработчику программы, выдаваемые семантическим анализом, или отключить оптимизацию. Применение оптимизации также нецелесообразно в процессе отладки исходной программы.
У современных компиляторов существуют возможности выбора не только общего критерия оптимизации, по и отдельных методов, которые будут использоваться при выполнении оптимизации.
Методы преобразования программы зависят or типов синтаксических конструкций исходного языка. Теоретически разработаны методы оптимизации для многих типовых конструкций языков программирования.
Оптимизация может выполняться для следующих типовых синтаксических конструкций:
линейных участков программы;
логических выражении;
циклов;
вызовов процедур и функций;
других конструкций входного языка.
Во всех случаях могут использоваться как машинно-зависимые, так и машинно- независимые методы оптимизации.
Далее будут рассмотрены только некоторые машинно-независимые методы оптимизации, касающиеся в первую очередь линейных участков программы. Перечень их здесь далеко не полный. С остальными машинно-независимыми методами можно более подробно ознакомиться в [6, т. 2J. Что касается машинно-зависимых методов, то они, как правило, редко упоминаются в литературе. Некоторые из них рассматриваются в технических описаниях компиляторов. Здесь эта методы будут рассмотрены только в самом общем виде.
Оптимизация линейных участков программы
Принципы оптимизации линейных участков
Линейный участок программы — это выполняемая по порядку последовательность операций, имеющая один вход и один выход. Чаще всего линейный участок содержит последовательность вычислений, состоящих из арифметических операций и операторов присвоения значений переменным.
Любая программа предусматривает выполнение вычислений и присваивания значений, поэтому линейные участки встречаются в любой программе. В реальных программах они составляют существенную часть программного кода. Поэтому для линейных участков разработан широкий спектр методов оптимизации кода. Кроме того, характерной особенностью любого линейного участка является последовательный порядок выполнения операций, входящих в его состав. Ни одна операция в составе линейного участка программы не может быть пропущена, пи одна операция не может быть выполнена большее число раз, чем соседние с нею операции (иначе этот фрагмент программы просто не будет линейным участком). Это существенно упрощает задачу оптимизации линейных участков программ. Поскольку все операции линейного участка выполняются последовательно, их можно пронумеровать в порядке их выполнения.
Для операций, составляющих линейный участок программы, могут применяться следующие виды оптимизирующих преобразований:
удаление бесполезных присваиваний;
исключение избыточных вычислений (лишних операций);
свертка операций объектного кода;
перестановка операций;
□ арифметические преобразования.
Удаление бесполезных присваиваний заключается в том, что если в составе линейного участка программы имеется операция присвоения значения некоторой произвольной переменной А с номером 1, а также операция присвоения значения гой же переменной А с номером j, i<j и ни в одной операции между i и j не используется значение переменной А, то операция присвоения значения с номером i является бесполезной. Фактически бесполезная операция присваивания значения дает переменной значение, которое нигде не используется. Такая операция может быть исключена без ущерба для смысла программы.
В общем случае, бесполезными могут оказаться не только операции присваивания, по и любые другие операции линейного участка, результат выполнения которых нигде не используется.
Например, во фрагменте программы
А :- В * С:
D :- В + С:
А :- 0 * С.
операция присвоения А-~В*С: является бесполезной и может быть удалена. Вместе с удалением операции присвоения здесь может быть удалена и операция умножения, которая в результате также окажется бесполезной.
Обнаружение бесполезных операций присваивания далеко нс всыда столь очевидно, как было показано в примере выше. Проблемы могут возникнуть, если между двумя операциями присваивания в линейном участке выполняются действия над указателями (адресами памяти) или вызовы функции, имеющих так называемый «побочный эффект».
Например, в следующем фрагменте программы
Р :- @А
А - В * С;
D - РА + С:
А :- D * С:
операция присвоения А:-В*С: уже нс является бесполезной, хотя это и не столь очевидно. В этом случае неверно следовать простому принципу о том, что если переменная, которой присвоено значение в операции с номером 1, не встречается ни в одной операции между 1 и J, то операция присвоения с номером 1 является бесполезной.
Не всегда удается установить, используется или нет присвоенное переменной значение, только на основании факта упоминания переменной в операциях. Тогда устранение лишних присваиваний становится достаточно сложной задачей, требующей учета операций с адресами памяти и ссылками.
Исключение избыточных вычислений (лишних операций) заключается в нахождении и удалении из объектного кода операций, которые повторно обрабатывают одни и те же операнды. Операция линейного участка с порядковым номером 1 считается лишней, если существует идентичная ей операция с порядковым номером j, j<i и никакой операнд, обрабатываемый этой операцией не изменялся никакой операцией, имеющей порядковый помер между 1 и j.
Свертка объектного кода — это выполнение во время компиляции тех операций исходной программы, для которых значения операндов уже известны Тривиальным примером свертки является вычисление выражений, все операнды которых являются константами. Более сложные варианты алгоритма свертки принимают во внимание также и переменные, и функции, значения для которых уже известны.
Не всегда компилятору удается выполнить свертку, даже если выражение допускает ее выполнение. Например, выражение А:=2*В*С*3: может быть преобразовано к виду А:~6*В*С:, но при порядке вычислений А.=2*(В*(С*3)); это не столь оче-
видно Дчя более эффективной) выполнения свертки объектного кода возможно совместить ее выполнение с другим методом — перестановкой операции.
Хорошим стилем программирования является объединение вместе операций, про изводимых над константами, чтобы обло мить компилятору выполнение свертки. Например, если имеется константа Р1=3.14, представляющая соответствующую математическую постоянную, то операцию Ь=$1п(2ла); лучше записать в виде B:-s1n(2*PI*A); или даже B:=s1n((2*PI)*A);, чем в виде B:=sin(2*A*PI);.
Перестановка операций заключается в изменении порядка следования операций, которое может повысить эффективное гь программы, по не будет влиять на конечный результат вычислений.
Например, операции умножения в выражении А:=2*В*3*С; можно переставить без изменения конечного результата и выполнить в порядке А:=(2*3)*(В*С),. То гда представляется возможным выполнить свертку и сократить количество операций.
Другое выражение A =(B+C)+(D+E); может потребовать как минимум одной ячейки памяти (или регистра процессора) для хранения промежуточного результата. Но при вычислении его в порядке A:=B+(C+(D+E)): можно обойтись одним регистром, в то время как результат будет тем же.
Особое значение перестановка операций приобретает в тех случаях, когда порядок их выполнения влияет на эффективность программы в зависимости от архитектурных особенностей целевой вычислительной системы (таких, как использование регистров процессора, конвейеров обработки данных и т. п.) Эти особенности рассмотрены далее в подразделе, посвященном машинно-зависимым методам оптимизации.
Арифметические преобразования представляют собой выполнение изменения характера и порядка следования операций на основании известных алтебраических и логических гождеств.
Например, выражение A:-B*C+B*D; может быть заменено на A:~B*(C+D) Конечный результат при этом не изменится, ио объектный код будет содержать на одну операцию умножения меньше.
К арифметическим преобразованиям можно отнести и такие действия, как замена возведения в степень па умножение; а целочисленного умножения па константы, кратные 2, — на выполнение операций сдвига. В обоих случаях удается повысить быстродействие программы заменой сложных операций на более простые.
Далее подробно рассмотрены два метода: свертка объектного кода и исключение липших операций.
Свертка объектного кода
Свертка объектного кода — это выполнение во время компиляции тех операций исходной программы, для которых значения операндов уже известны. Нет необходимости многократно выполнять эти операции в результирующей програм мс — вполне достаточно один раз выполнить их при компиляции программы.
Простейший вариант свертки — выполнение в компиляторе операций, операндами которых являются константы. Несколько более сложен процесс определения тех операций, значения которых могут быть известны в результате выполнения других операций. Для этой цели при оптимизации линейных участков программы используется специальный алгоритм свертки объектного кода.
Алгоритм свертки для линейного участка программы работает со специальной таблицей Т, которая содержит пары (<переменная>,<константа>) для всех переменных, значения которых уже известны. Кроме того, алгоритм свертки помечает те операции во внутреннем представлении программы, для которых в результате свертки уже не требуется генерация кода. Так как при выполнении алгоритма свертки учитывается взаимосвязь операций, то удобной формой представления для него являются триады, так как в других формах представления операций (таких, как тетрады или команды ассемблера) требуются дополнительные структуры, чтобы отразить связь результатов одних операций с операндами других.
Рассмотрим выполнение алгоритма свертки объектного кода для триад. Для пометки операций, не требующих порождения обтюктного кода, будем использовать триады специального вида С(К.О).
Алгоритм свертки триад последовательно просматривает триады линейного участка и для каждой триады делает следующее:
Если операнд есть переменная, которая содержится в таблице Т то операнд заменяется па соответствующее значение константы.
Если операнд есть ссылка на особую триаду типа С(К,0), то операнд заменяется па значение константы К.
Если все операнды триады являются константами, то триада может быть свернута. Тогда данная триада выполняется и вместо нее помещается особая триада вида С(К,0), где К — константа, являющаяся результатом выполнения свернутой триады. (При гепе^щии кода для особой триады объектный код не порождается, а потому опа в дальнейшем может быть просто исключена.)
Если триада является присваиванием типа А:=В, тогда:
О если В — константа, то А со значением константы заносится в таблицу Т (если там уже было старое значение для A. то это старое значение исключа ется);
О если В — не константа, то А вообще исключается из таблицы Т, если оно там есть.
Рассмотрим пример выполнения алгоритма.
Пусть фрагмент исходной программы (записанной на языке типа Pascal) имеет вид:
I :- 1 * 1:
I - 3.
J -6*1+1
Ее внутреннее представление в форме триад будет иметь вид:
+ (1.1)
:= (I, "1)
:- (I. 3)
* (6. I)
+ (А4. I)
:- (J. А5)
Процесс выполнения алгоритма свертки показан в табл. 14.4.
Таблица 14.4. Пример работы алгоритма свертки
Триада
Шаг 1
Шаг 2
Шаг 3
Шаг 4
Шаг 5
Шаг 6
1
С (2, 0)
С (2, 0)
С (2, 0)
С (2, 0)
С (2, 0)
С (2, 0)
2
:- (I, А1)
:- (1. 2)
:- О. 2)
:- (1, 2)
:- (I, 2)
:- О. 2)
3
:* (I, 3)
:= (1, 3)
:- (I. 3)
:- (I- 3)
:- (1. 3)
:- (1, 3)
4
* (6,1)
* (6. Т)
* (6. I)
С (18, 0)
С (18, 0)
С (18, 0)
5
+ (А4,I)
+ (А4.I)
+ (А4.1)
+ (А4,1)
С (21, 0)
С (21, 0)
6
:- U. А5)
:- 0, А5)
:- а. А5)
:- U. А5)
U. А5)
:-(1.21)
Т
(.)
(1.2)
(1.3)
(1. 3)
(1. 3)
(I. 3)
(I, 21)
Если исключить особые триады типа С(К,0) (которые ле порождают объектного кода), то в результате выполнения свертки получим следующую последовательность триад:
:= (I. 2)
:= (I. 3)
:- (J. 21)
Алгоритм свертки объектного кода позволяет исключить из линейного участка пртраммы операции, для которых па этапе компиляции уже известен результат. За счет этого сокращается время выполнения, а также обьем кода результирующей программы.
Свертка объектного кода в принципе может выполняться не только для линейных участков программы. Когда операндами являются константы, логика выполнения программы значения не имеет — свертка можег быть выполнена в любом случае. Если же необходимо учитывать известные значения переменных, то нужно принимать во внимание и логику выполнения результирующей программы.
лить соответствие между этой программой и построенным на рис. 13.8 автоматом. Конкретный распознаватель будет существенно зависеть от того, как организовано его взаимодействие с остальными частями компилятора. Это влияет на возвращаемое функцией значение, а также на то, как она должна действовать при обнаружении ошибки и при завершении распознавания лексемы.
В данном примере предусмотрено, что основная часть лексического анализатора сначала считывает в память весь текст исходной программы, а потом начинает его последовательный просмотр. В зависимости от текущего символа вызывается тот или иной сканер. По результатам работы каждого сканера разбор либо продолжается с позиции, которую вернула функция сканера, либо прерывается, если функция возвращает 0.
Приведенный в примере сканер можно дополнить сканерами для констант с плавающей точкой языка С, а также идентификаторов и ключевых слов языка. В последнем случае распознающий КЛ будет явно недетерминированным, поскольку в языке С возможны, например, идентификатор if fl и ключевое слово if, имеющие принципиально разное значение. Такой КЛ потребует дополнительных преобразований.
В целом техника построения сканеров основывается па моделировании работы детерминированных и недетерминированных КА с дополнением функций распознавателя вызовами функций обработки ошибок, а также заполнения таблиц символов и таблиц идентификаторов. Такая техника не требует сложной математической обработки и принципиально важных преобразований входных грамматик. Для разработчиков сканера важно только решить, где кончаются функции сканера и начинаются функции синтаксического разбора. После этого процесс построения сканера легко поддается автоматизации.
Автоматизация построения лексических анализаторов (программа LEX)
Лексические распознаватели (сканеры) — это не только важная часть компиляторов. Лексический анализ применяется во многих других областях, связанных с обработкой текстовой информации на компьютере. Прежде всего, лексического анализа требуют все возможные командные процессоры и утилиты, требующие ввода командных строк п параметров пользователем. Кроме того, хотя бы самый примитивный лексический анализ вводимого текста применяют практически все современные текстовые редакторы и текстовые процессоры Практически любой более-менее серьезный разработчик программного обеспечения райо или поздно сталкивается с необходимостью решать задачу лексического анализа (разбора). С одной стороны, задачи лексического анализа имеют широкое распространение, а с другой — допускают четко формализованное решение на основе техники моделирования работы КЛ. Все это вместе предопределило возможность автоматизации решения данной задачи. И программы, ориентированные па ее решение,
Поэтому для нелинейных участков программы (ветвлений и циклов) алгоритм будет более сложным, чем последовательный просмотр линейного списка триад.
Исключение лишних операций
Алгоритм исключения лишних операций просматривает операции в порядке их следования. Так же как и алгоритму свертки, алгоритму исключения лишних операций проще всего работать с триадами, потому что они полност ью от ражают взаимосвязь операций.
Рассмотрим алгоритм исключения лишних операций для триад.
Чтобы следить за внутренней зависимостью переменных и триад, алгоритм присваивает им некоторые значения, называемые числами зависимости, но следующим правилам:
изначально для каждой переменной ее число зависимости равно 0, так как в начале работы программы значение переменной не зависит ни от какой триады;
после обработки 1-й триады, в которой переменной А присваивается некоторое значение, число зависимости A (dep(A)) получает значение 1, так как значение А теперь зависит от данной 1-й триады;
при обработке 1-й триады ее число зависимости (dep(i)) принимается равным значению 1*(наксинальное из чисел зависимости операндов).
Таким образом, при использовании чисел зависимости триад и поименных можно утверждать, что если i-я триада идентична j-й триаде (j<1), го i-я триада считается лишней в том и только в том случае, когда dep(1)-dep( j).
Алгоритм исключения лишних операций использует в своей работе триады особого вида SAMEC j 0) Если такая триада встречается в позиции с номером 1, то это означает, что в исходной последовательности триад некоторая триада 1 идентична триаде j.
Алгоритм исключения лишних операций последовательно просматривает триады линейного участка. Он состоит из следующих шагов, выполняемых для каждой триады:
Если какой-то операнд триады ссылается на особую триаду вида SAME(j.O), то он заменяется на ссылку на триаду с номером j (*j)
Вычисляется число зависимости текущей триады с номером 1, исходя из чисел зависимости ее операндов.
Если в просмотренной части списка триад существует идентичная j-я i риада, причем j<1 и dep(1)=dep(j), то текущая триада 1 заменяется на триаду особого вида SAME(j.O).
Если текущая триада есть присвоение, то вычисляется число зависимости соответствующей переменной
Рассмотрим работу алгоритма на примере:
D - D + С*В.
А :- 0 + С*В:
С - D + С*В:
Этому фрагменту программы будет соответствовать следующая последовательность триад:
* (С. В)
+ (D. Ч)
:= (D. Ч)
* (С. В)
+ (D, Ч)
:~ (А, *5)
* (С. В)
+ (0. Ч)
:= (С. -8)
Видно, чго в данном примере некоторые операции вычисляются дважды над одними и теми же операндами, а значит, они являются лишними и могут быть ис ключепы. Работа алгоритма исключения липших операций отражена в табл. 14.5.
Таблица 14.5. Пример работы алгоритма исключения лишних операций
Обрабатываемая триада ।
Числа зависимости переменных
Числа зависимости триад dep(i)
Триады, полученные после выполнения алгоритма
А
в
с
D
1) • (С, В)
0
0
0
0
1
1) • (С, В)
2) + (U, Ч)
0
0
0
0
2
2) + (D, Ч)
3) :- (D, л2)
0
0
0
3
3
3) :- (D, Ч)
4) * (С, В)
0
0
0
3
1
4) SAME (1, 0)
5) + (D, Ч)
0
0
0
3
4
5) + (В,Ч)
6) :- (Л, Л5)
6
0
0
3
5
6) :- (А, А5)
7) * (С, В)
6
0
0
3
1
7) SAME (1,0)
8) + (D, л7)
6
0
0
3
4
8) SAME (5, 0)
9) :- (С, А8)
6
0
9
3
5
9):- (С, А5)
Теперь, если исключить триады особого вида SAME(j.O), то в результате выполнения алгоритма получим следующую последовательность триад:
* (С. В)
+ (D. *1)
:= (D, Ч)
+ (D, Ч)
:= (А. Ч)
:- (С. Ч)
Обратите внимание, что в итоговой последовательности изменилась нумерация триад и номера в ссылках одних триад па другие. Если в компиляторе в качестве ссылок использовать не номера триад, а непосредственно указатели на них, то изменения ссылок в таком варианте нс потребуется.
Алгоритм исключения липших операций позволяет избежать повторного выполнения одних и тех же операций над одними п геми же операндами. В результате оптимизации по этому алгоритму сокращается и время выполнения, и объем кода результирующей про1раммы.
Другие методы оптимизации программ
Оптимизация вычисления логических выражений
Особенность оптимизации логических выражений заключается в том, что не всегда необходимо полностью выполнять вычисление всего выражения для того, чтобы знать его результат. Иногда по результату первой операции пли даже по значению одного операнда можно заранее определить результат вычисления всего выражения.
Операция называется предопределенной для некоюрого шаченпя онеренда, если ее результат зависит только от этого операнда и остается неизменным (ипнари- аптным) относительно значений других операндов.
Операция логического сложения (or) является предопределенной для логического значения «истина» (true), а операция логического умножения — предопределена для логического значения «ложь» (false).
Эти факты могут быть использованы для оптимизации логических выражений. Действительно, получив значение «истина» в последовательности логических сложений или значение «ложь» в последователыюсти логических умножений, ист никакой необходимости далее производить вычисления — результат уже определен п известен.
Например, выражение A or В or С or D не имеет смысла вычислять, если известно, что значение переменной А есть fine («истина»).
Компиляторы строят объектный код вычисления логических выражений таким образом, что вычис ление выражения прекращается сразу же, как Только сто значение становится предопределенным Это позволяет ускорить вычисления при выполнении результирующей программы. В сочетании с преобразованиями логических выраженш па основе тождеств булевой алгебры и перестановкой операций эффективность данного метода может быть несколько увеличена.
Не всегда такие преобразования инвариантны к смыслу программы. Например, при вычислении выражения A or F(B) or G(C) функции F и G не будут вызваны и выполнены, если значением переменной А является true. Эю не важно, если результатом этих функций является только возвращаемое ими значение, но если они обладают «побочным эффектом» (о котором было сказано выше в замечаниях), то семантика программы может измениться.
В большинстве случаев современные компиляторы позволяют исключить такого рода оптимизацию, и тогда все логические выражения будут вычисляться до конца, без учета предопределенности результата. Однако лучше избегать такого
рода «побочных эффектов», которые, как правило, говорят о плохом стиле программирования.
Хорошим стилем счи тается также принимать во внимание эту особенность вычисления логических выражений Тогда операнды в логических выражениях следует стремиться располагать таким образом, чтобы в первую очередь вычислялись те из них, которые чаще определяют все значение выражения. Кроме того, значения функций лучше вычислять в конце, а не в начале логического выражения, чтобы избежать лишних обращении к ним. Так, рассмотренное выше выражение лучше записывать и вычислять в порядке A or F(B) or G(C), чем в порядке F(B) or G(C) or A.
He только логические операции могут иметь предопределенный результат Некоторые математические операции и функции также обладают этим свойством Например, умножение на 0 нс имеет смысла выполнять.
Другой пример такого рода преобразовании — это исключение вычислений для инвариантных операндов.
Операция называется инвариантной относительно некоторого значения операнда, если ее результат не зависит от этого значения операнда и определяет ся другими операндами.
Например, логическое сложение (or) инвариантно относительно значения «ложь» (False), логическое умножение (and) — относительно значения «истина»; алгебраическое сложение инвариантно относительно 0, а алгебраическое умножение — относительно 1.
Выполнение такого рода операций можно исключить из текста программы, если известен инвариантный для них операнд. Этот метод оптимизации имеет смысл в сочетании с методом свертки операций
Оптимизация передачи параметров в процедуры и функции
Выше рассмотрен метод передачи параметров в процедуры и функции через стек Этот метод применим к большинству языков программирования и используется практически во всех современных компиляторах для различных архитектур целевых вычислительных систем (на которых выполняется результирующая программа).
Данный метод прост в реализации и имеет хорошую аппаратную поддержку во многих архитектурах. Однако он является пеэффекпшпым в том случае если процедура или функция выполняет несложные вычисления над небольшим количеством параметров. Тогда всякий раз при вызове процедуры или функции компилятор будет создавать объектный код для размещения ее фактических параметров в стеке, а при выходе из нее — код для освобождения ячеек, занятых параметрами. При выполнении результирующей программы этот код будет задействован. Если процедура или функция выполняет небольшие по объему вычисления, код для размещения параметров в стеке и доступа к ним может составлять существенную часть ко всему коду, созданному для этой процедуры или функции. А если эта функция вызывается многократно, то этот код будет заметно снижать эффективность ее выполнения.
ные переменные процедуры он желал бы разместить в регистрах процессора. Тогда компилятор стремится распределить свободные регистры в первую очередь именно для этих параметров и переменных.
Метод подстановки кода функции в вызывающий обьекгный код (так называемая ini ne-подстаиовка) основан на том, что объектный код функции непосредственно включается в вызывающий объектный код всякий раз в месте вызова функции.
Для разработчика исходной программы такая функция (называемая inline-функцией) ничем не отличается от любой другой функции, но для вызова се в результирующей программе используется принципиально другой механизм. По сути, вызов функции в результирующем объектном коде вовсе не выполняется — просто все вычисления, производимые функцией, выполняются непосредственно в самом вызывающем коде в месте се вызова.
Очевидно, что в общем случае такой метод оптимизации ведет не только к уве личению скорости выполнения программы, по и к увеличению объема объектного кота. Скорость увеличивается за счет отказа от всех операций, связанных с вызовом функции, — это не только сама команда вызова, по и все действия, связанные с передачей параметров. Вычисления при этом идут непосредственно с фактическими параметрами функции. Объем кода растет, так как приходится всякий раз включать код функции в место ее вызова. Тем не менее если функция очень проста и включает в себя лишь несколько машинных команд, то можно даже добиться сокращения кода результирующей Программы) гак как при включении кода самой функции в место ее вызова оттуда исключаются операции, связанные с передачей ей параметров
Как правило, этот метод применим к очень простым функциям или процедурам, иначе обьем результирующего кода может существенно возрасти. Кроме того, он применим только к функциям, вызываемым непосредственно по адресу, без применения косвенной адресации через таблицы RTTI (см. раздел «Семантический анализ и подготовка к генерации кода» этой главы). Некоторые компиляторы допускают применять его только к функциям, предполагающим последовательные вычисления и не содержащим циклов
Ряд языков программирования (например, C++) позволяют разработчику явно указать, для каких функций он желает использовать inline-подстановку. В C++, например, для этой цели служит ключевое слово входного языка Inline
Оптимизация циклов
Циклом в программе называется любая последовательность участков программы. которая может выполняться повторно.
Циклы присущи подавляющему большинству программ Во многих программах есть циклы, выполняемые многократно. Большинство языков программирования имеют синтаксические конструкции, специально ориентированные на организацию циклов. Очевидно, такие циклы легко обнаруживаются на этапе синтаксического разбора.
Однако понятие цикла с точки зрения объектной программы, определенно!' выше, является более общим. Оно включает в себя ие только циклы, явно описанные
в синтаксисе языка. Циклы могут быть организованы в исходной программе с помощью любых конструкций, допускающих передачу управления (прежде всего, с помощью условных операторов и операторов безусловного перехода). Для результирующей объектной программы не имеет значения, с помощью каких конструкций организован цикл в исходной программе
Чтобы обнаружить все циклы в исходной программе, используются методы, основанные на построении графа управления программы [6, т 2)
Циклы обычно содержат в себе один или несколько линейных участков, где производятся вычисления. Поз тому методы оптимизации линейных участков позволяют повысить также и эффективность выполнения циклов, причем они оказываются тем более эффективными, чем больше кратность выполнения цикла. Но есть методы оптимизации программ, специально ориентированные на оптимизацию циючов.
Для оптимизации пик юв используются следующие методы-
вынесение инвариантных вычислений из циклов;
замена операций с индуктивными переменными;
слияние и развертывание циклов.
Вынесение инвариантных вычислений из циклов заключается в вынесении за пределы циклов тех операции, операнды которых нс изменяются в процессе выполнения цикла. Очевидно, что такие операции могут быть выполнены один раз до начала цикла, а полученные результаты потом могут использоваться в теле цикла.
Например, цикл*
for i:-l to 10 do
beg п
AEU :- В * C * A[i],
end:
может быть заменен на последоват ельность операций
0 - В * С:
for 1 :-1 to 10 do
beg n
A[1] :- 0 * A[i]:
end:
если значения В и С не изменяются нигде в теле цикла. При этом операция умножения В*С будет выполнена только один раз, в то время как в первом варианте она выполнялась 10 раз над одними и теми же результатами.
Конечно, во внутреннем представлении компилятора циклы нс могут быть зап шаны в таком виде, по для наглядности здесь мы их будем представ :ять в синтаксической записи входного языка (в данном случае — языка Pascal; как и во всех примерах далее). На суть выполняемых операций оптимизации это никак не влияет. 1
Неизменность операндов в теле цикла может оказаться нс столь очевидной. Отсутствие присвоения значений переменным не может служить достоверным признаком их неизменности. В общем случае компилятору придется принимать во внимание все операции, которые так или иначе могут повлиять на инвариантность переменных. Такими операциями являются операции над указателями, вызовы функций (даже если сами переменные не передаются в функцию в качестве параметров, она может изменять их значения через «побочные эф<]>екты&). Поскольку невозможно отследить все изменения указателей (адресов) и все «побочные эффекты» на этапе компиляции, компилятор вынужден отказаться от вынесения из цикла инвариантных переменных всякий раз, когда в теле цикла появляются «подозрительные» операции, которые могуч поставить под сомнение инвариантность той или иной переменной.
Замена операций с индуктивными переменными заключается в изменении сложных операций с индуктивными переменными в теле цикла на более простые операции. Как правило, выполняется замена умножения на сложение.
Переменная называется индуктивной в цикле, если ее значения в процессе выполнения цикла образуют арифметическую прогрессию. Таких переменных в цикле может быть несколько, тогда в теле цикла их иногда можно заменить па одну единственную переменную, а реальные значения для каждой переменной будут вычисляться с помощью соответствующих коэффициентов соотношения (за пределами цикла значения всем переменным должны быть присвоены, если, конечно, они используются).
Простейшей индуктивной переменной является переменная-счетчик цикла с перечислением значений (цикл типа for, который встречается в синтаксисе многих современных языков программирования). Более сложные случаи присутствия индуктивных переменных в цикле требуют специального анализа тела цикла. Не всегда выявление таких переменных является тривиальной задачей.
После того как индуктивные переменные выявлены, необходимо проанализировать те операции в теле цикла, где они используются. Часть таких операций может быть упрощена. Как правило, речь идет о замене умножения на сложение [6, т. 2, 82]
Например, цикл
S .- 10:
for i:-l to N do A[i] .- i*S:
может быть заменен на последовательность операций
S - 10:
Т - S: 1 - 1:
while 1 <- 10 do
begin
A[i] - T. T :- T + 10; 1 -1 + 1:
end:
Здесь использован синтаксис языка Pascal, а Т — это некоторая новая временная переменная (использовать для той же цели уже существующую переменную S не вполне корректно, так как ее значение может быть использовано и после завершения этого цикла). В итоге удалось отказаться от выполнения N операций ум-
ножения, заменив их на N операций сложения (которые обычно выполняются быстрее). Индуктивной переменной в первом варианте цикла являлась 1, а во втором варианте — i и Т.
В Другом примере
S - 10;
for 1.-1 to N do R - R + F(S): S - S + 10.
две индуктивных переменных — i и S. Если заменить их на одну, то выяснится, что переменная i вовсе не имеет смысла, тогда этот цикл можно заменить па последовательность операций
S :- 10; М - 10 ► N*10:
while S <- М do begin R - R + F(S): S - S + 10; end.
Здесь удалось исключить N операций сложения для переменной 1 за счет добавления новой временной переменной М (как и в предыдущем примере, использован синтаксис языка Pascal).
В современных реальных компиляторах такие преобразования используют я достаточно редко, поскольку они требуют достаточно сложного анализа программы, в то время как достигаемый выигрыш невелик — разница в скорости выполнения сложения и умножения, равно как и многих других операций, в современных вычислительных системах не сюль существенна. Кроме того, существуют варианты циклов, для которых эффективность указанных методов преобразования явтяется спорной [G, т. 2].
Слияние л развертывание циклов предусматривает два различных варианта пре образований: слияния двух вложенных циклов в один и замена цикла на линейную последовательность операций.
Слияние двух циклов можно проиллюстрировать на примере циклов
for 1.-1 to N do
for j=l to M do A[ j] :- 0;
Здесь происходит инициализация двумерного массива. Но в объектном коде двумерный массив — это всего лишь область памяти размером М*М, поэтому (с точки зрения объектного кода, по не входного языка!) эту операцию можно представить так:
К .- N*M
for i -1 to К do А[ ] .- 0.
Развертывание циклов можно выполнить для циклов, кратность выполнения которых известна уже па этапе компиляции. Тогда цикл, кратностью N, можно заменить на линейную последовательность N операций, содержащих тело цикла.
Например, цикл
for 1 -1 to 3 do А[ ] :- 1;
можно заменить операциями
А[1] - 1,
А[2] - 2:
А[3] - 3;'
Незначительный выигрыш в скорости достигается за счет исключения всех операций с индуктивной переменной, однако объем программы может существенно возрасти
Машинно-зависимые методы оптимизации
Машинно-зависимые методы оптимизации ориентированы на конкретную архитектуру целевой вычислительной системы, на которой будет выполняться результирующая программа. Как правило, каждый компилятор ориентирован па одну определенную архитектуру целевой вычислительной системы. Иногда возможно в настройках компилятора явно указать одну из допустимых целевых архитектур. В любом случае результирующая программа всегда порождается для четко заданной целевой архитектуры.
Архитектура вычислительной системы есть представление аппаратной и программной составляющих частей системы и взаимосвязи между ними с точки зрения системы как единого целого. Понятие «архитектура» включает в себя особенности и аппаратных, и программных средств целевой вычислительной системы. При выполнении машинно-зависимой оптимизации компилятор может принимать во внимание тс или иные ее составляющие. То, какие конкретно особенности архитектуры будут приняты во внимание, зависит от реализации компилятора и определяется его разработчиками.
Количество существующих архитектур вычислительных систем к настоящему времени очень велико. Поэтому не представляется возможным рассмотреть все ориентированные на них методы оптимизации даже в форме краткого обзора. Интересующиеся этим вопросом могут обратиться к специализированной литературе [6, т. 2, 40, 74, 82]. Далее будут рассмотрены только основные два аспекта машинно-зависимой оптимизации: распределение регистров процессора и порождение кода для параллельных вычислений.
Распределение регистров процессора
Процессоры, па базе которых строятся современные вычислительные системы, имеют, как правило, несколько программно-доступных регистров. Часть из них может быть предназначена для выполнения каких-либо определенных целей (например, регистр - указатель стека или регистр — счетчик команд), другие могут быть использованы практически произвольным образом при выполнении различных операций (так называемые «регистры общего назначения»). Использование регистров общего назначения для хранения значений операндов и результатов вычислений позволяет добиться увеличения быстродействия программы, так как действия над регистрами процессора всегда выполняются быстрее, чем над ячейками памяти. Кроме того, в ряде процессоров нс все операции могут быть выполнены над ячейками памяти, а потому часто требуется предварительная загрузка операнда в регистр. Результат выполнения операции чаще всего тоже оказывается в регистре, и если необходимо, его надо выгрузить (записать) в ячейку памяти.
Программно доступных регистров процессора всегда ограниченное количество. Поэтому встает вопрос об их распределении при выполнении вычислений. Этим
ис замедлили появиться. Причем поскольку математический аппарат КА известен уже длительное время, да и с задачами лексического анализа программисты столкнулись довольно давно, то и программам, их решающим, насчитывается не один десяток лет.
Для решения задач построения лексических анализаторов существуют различ ные программы. Наиболее известной среди них является программа LEX.
LEX — программа для генерации сканеров (лексических анализаторов) Входной язык содержит описания лексем в терминах регулярных выражений. Результатом работы LEX является программа на некотором языке программирования, которая читает входной файл' (обычно это стандартный ввод) п выделяет из пего последовательности символов (лексемы), соответствующие заданным регулярным выражениям.
История программы LEX тесно связана с историей операционных систем типа UNIX. Эта npoi-рамма появилась в составе утилит ОС UNIX и в настоящее время входит в поставку практически каждой ОС этого типа. Однако сейчас сущи ствуют версии программы LEX практически для любой ОС, в том числе для широко распространенной MS-DOS.
Поскольку LEX появилась в среде ОС UNIX, то первоначально языком программирования, на котором строились порождаемые ею лексические анализаторы, был язык С. Но со временем появились версии LEX, порождающие сканеры и на основе других языков программирования (например, известны версии для языка Pascal)
Принцип работы LEX достаточно прост: на вход ей подастся текстовый файл, содержащий описания нужных лексем в терминах регулярных выражений, а на выходе получается файл с текстом исходной программы сканера на заданном языке программирования (обычно — на С). Текст исходной программы сканера может быть дополнен вызовами любых функций из любых библиотек, поддерживаемых данным языком и системой программирования. Таким образом, LEX позволяет значительно упростить разработку лексических анализа торов, практи- чески сводя эту работу к описанию требуемых лексем в терминах регулярных выражений.
Более подробную информацию о работе с программой LEX можно получить в [5, И 31,39,67,68].
Синтаксические анализаторы.
Синтаксически управляемый перевод
Основные принципы работы синтаксического анализатора
Синтаксический анализатор (синтаксический разбор) — эго часы, компилятора, которая отвечает за выявление основных синтаксических конструкций входного языка. В задачу синтаксического анализа входит: найти и выделить основные
занимается алгоритм распределения регистров, который присутствует практически в каждом современном компиляторе в части генерации кода результирующей программы.
При распределении регистров под хранение промежуточных и окончательных результатов вычислений может возникнуть ситуация, когда значение той или иной переменной (связанной с нею ячейки памяти) необходимо загрузить в регистр для дальнейших вычислений, а все имеющиеся доступные регистры уже заняты. Тогда компилятору перед созданием кода по загрузке переменной в регистр необходимо сгенерировать код для выгрузки одного из значений из регистра в ячейку памяти (чтобы освободить регистр). Причем выгружаемое значение затем, возможно, придется снова загружать в регистр. Встает вопрос о выборе того регистра, значение которого нужно выгрузить в память.
При этом возникает необходимость выработки стратегии замещения регистров процессора. Она чем-то сродни стратегиям замещения процессов и страниц и памяти компьютера, которые были рассмотрены в части 1 данного пособия. Однако отличие заключается в том, что, в отличие от диспетчера памяти в ОС, компилятор может проанализировать код и выясни ть, какое из выгружаемых значений ему понадобится для дальнейших вычислении и когда (следует помнить, что компилятор сам вычислений не производит — он только порождает код для них, иное дело — интерпретатор). Как правило, стратегии замещения регистров процессора предусматривают, что выгружается тот регистр, значение которою будет использовано в последующих операциях позже всех (хотя не всегда эта стрпте- гия является оптимальной).
Кроме общего распределения регистров могут использоваться алгоритмы распределения регистров специального характера. Например, во многих процесс» рах есть регистр-аккумулятор, который ориентирован на выполнение различных арифметических операций (операции с ним выполняются либо быстрее, либо имеют более короткую запись). Поэтому в него стремятся всякий раз загрузить чаще всего используемые операнды; он же используется, как правило, при передаче результатов функций и отдельных операторов. Могут быть также регистры, ориентированные на хранение счетчиков циклов, базовых указателей и т. п. Тогда компилятор должен стремиться распределить их именно для тех целей, на выполнение которых они ориентированы.
Оптимизация кода для процессоров, допускающих распараллеливание вычислений
Многие современные процессоры допускают возможность параллельного выполнения нескольких операций. Как правило, речь идет об арифметических операциях.
Тогда компилятор может порождать объектный код таким образом, чтобы в нем содержалось максимально возможное количество соседних операций, все операнды которых не зависят друг от друга. Решение такой задачи в глобальном объеме для всей программы в целом не представляется возможным, ио для конкретного оператора оно, как правило, заключается в порядке выполнения операций. В этом случае нахождение оптимального варианта сводится к выполнению перестановки операций (если опа возможна, конечно). Причем оптимальный ва-
риант зависит как от характера операции, так и от количества имеющихся в процессоре конвейеров для выполнения параллельных вычислений.
Например, операцию A+B+C+D+E+F на процессоре с одним потоком обработки данных лучше выполнять в порядке ((((A+B)+C)+D)+E)+F. Тогда потребуется меньше ячеек для хранения промежуточных результатов, а скорость выполнения от порядка операций в данном случае не зависит.
Та же операция на процессоре с двумя потоками обработки данных в целях увеличения скорости выполнения может быть обработана в порядке ((А+В)+С)+ +((D+E)+F). Тогда по крайней мерс операции А+В и D+E, а также сложение с их результатами могут быть обработаны в параллельном режиме. Конкретный порядок команд, а также распределение регистров для хранения промежуточных результатов будут зависеть от типа процессора.
На процессоре с тремя потоками обработки данных ту же операцию можно уже разбить на части в виде (A+B)+(C+D)+(E+F). Теперь уже три операции А+В, C+D и E+F могут быть выполнены параллельно Правда, их результаты уже должны быть обработаны последовательно, ио тут уже следует принять во внимание соседние операции для нахождения наиболее оптимального варианта.
Контрольные вопросы и задачи
Вопросы
Справедливы ли следующие утверждения:
О любой язык программирования является контекстно-свободным языком;
О синтаксис любого языка программирования может быть описан с помощью контекстно-свободной грамматики;
О любой язык программирования является контекстно-зависимым языком;
О семантика любого языка программирования может быть описана с помощью контекстно-свободной грамматики?
Какие задачи в компиляторе решает семантический анализ? Можно ли построить компилятор без семантического анализатора? Если да, то какие условия должны при этом выполняться?
Цепочка символов, принадлежащая любому языку программирования, может быть распознана с помощью распознавателя для контекстно-зависимых языков. При этом не будет требоваться дополнительный семантический анализ цепочки. Почему такой подход не применяется в компиляторах на практике?
На языке С описаны переменные:
int i.j.k /* целом и ленные переменные */
doub е x.y.z: /* вещественные переменные *7
объясните, есть ли различия в объектном коде, порождаемом компиляторов для перечисленных ниже пар операторов, и если есть, го в чем они заключаются:
О 1 = х/у; i = (mt) (х/у);
О i = х/у; 1 = ((int)x)/((int)y):
О i - j/k; i - ((double)j)/(double)k);
О z = x/y: z=((int)x)/((int)y):
О z = j/k: z = (double)(j/k):
О z = j/k; z = ((double)j)/( (double)k):
Объясните, какие функции по преобразованию типов должен будет включить в объектный код компилятор в каждом из перечисленных случаев. В каких случаях выполняется неявное преобразование типов?
Функция (test, описанная в исходной программе на языке C++ как «void ftest(int 1)», помещается в библиотеку. В объектном коде библиотеки она получит имя, присвоенное компилятором. Это имя может иметь вид, подобный «ftestPiPv» Если разработчик пользуется одним и тем же компилятором, то имя функции будет присваиваться одно и то же. Почему тем не менее не стоит вызывать функцию из библиотеки по этому имени? Что должен иредпри- • пять разработчик, чтобы функция в библиотеке имела имя «ftest»?
Почему распределение памяти не может быть выполнено до выполнения семантического анализа? Как работает процесс распределения памяти с таблицей идентификаторов?
Может ли компилятор для языков С и Pascal использовать статическую схему организации дисплея памяти процедуры (функции)? А динамическую схему?
Син таксис языка Pascal предусматривает возможность вложения процедур и функций. Как это должно отразиться на организации дисплея памяти, если учесть, что, согласно семантике языка, вложенные процедуры и функции могут работать с локальными данными объемлющих процедур н функции? Требуются ли какие-либо изменения в стековой организации дисплея памяти в таком случае?
От чего зависит состав информации, хранящейся в таблице RTTI? Регламентируется ли состав информации в этой таблице синтаксисом или семантикой исходного языка программирования? Зависит ли информация в таблице RTTI от архитектуры целевой вычислительной системы?
Почему в большинстве компиляторов помимо генерации результирующего объектного кода выполняется еще и оптимизация его? Можно ли построить компилятор, исключив фазу оптимизации кода?
От чего зависит эффективность объектного кода, построенного компилятором? Влияют ли на эффективность результирующего кода синтаксис и семантика исходного языка программирования?
В чем заключается основной принцип СУ-псрсвода? Является ли СУ-пере- вод наиболее эффективным методом порождения результирующего кода?
Сравните между собой основные способы внутреннего представления программ. На каких этапах компиляции лучше всего использовать каждый из этих способов? Какие (или какой) из способов внутреннего представления программы обязательно должен уметь обрабатывать компилятор?
Почему имеются трудности с оптимизацией выражении, представленных в форме обратной польской записи?
От чего зависит код, порождаемый процедурой СУ-перевода для каждого узла дерева операций? Должна ли процедура СУ-перевода обращаться к таблице символов? Должна ли процедура СУ-перевода учитывать тип операндов? Как зависят действия этой процедуры от типа порождаемого ею внутреннего представления программы?
Какой из двух основных методов оптимизации: машинно-зависимый или машинно-независимый может порождать более эффективный результирующий код? Как сочетаются эти два метода оптимизации в компиляторе?
Почему линейные участки легче поддаются оптимизации, чем все другие части внутреннего представления программы и обьектного кода?
Почему можно утверждать, что исключение из исходного языка операций с указателями и адресами значительно усиливает возможности компилятора по оптимизации кода?
Массив А содержит 10 элементов (индексы от 0 до 9). Будет ли условный оператор языка Pascal «if (i<10) and (A[i]=0) then „» правильным с точки зрения порождаемого компилятором объектного кода? Будет ли правильным с этой же точки зрения оператор «if (i<=0) or (i>=10) or (A[i]=0) then _»? Что можно сказать об объектном коде, порождаемом компилятором для каждого из этих операторов, если компилятор не будет выполнять оптимизацию логических выражен и й?
Какое значение будет иметь переменная i после выполнения следующего оператора языка С:
it (*+1>10 || 1+*>20 && 1++<50 || ы-кО) 1--;
если переменная i имеет перед его выполнением следующие значения:
О 9
О И
О 21
О 0
О -2?
Как изменится значение переменной при выполнении этого оператора, если предположить, что при порождении объектного кода компилятор не выполняет оптимизацию логических выражений?
Какие варианты распараллеливания выполнения операций может использовать компилятор при порождении обьектного кода, необходимого для вычисления выражения «А*В*С + A*D/E - В/Е - F»?
Задачи
Функции опйсаны на язык<? Pascal следующим образом: procedure А(1 : integer; j: integer): var t: float:
begin
end:
procedure В.
var a.b.c: integer.
begin
end:
Функция А вызывает функцию В, которая затем рекурсивно вызывает (функцию Л. Отобразите содержимое стека, после выполнения одного такого вызова, двух таких вызовов. Объясните, как будет происходить возврат в вызывающую программу, если после двух вызовов рекурсия заканчивается.
На языке Pascal описаны следующие типы данных:
type
TestArray - array[1..4] of integer:
TestRecord - record
A TestArray:
В ; char.
C : integer
end;
Testunion - record
J : byte:
case I integer of
(A : TestRecord);
(B ; TestArray).
(C : integer);
end:
а также переменные:
var
cl.c2 ; char.
.i2 : integer;
al . TestArray
rl.r2.r3 : TestRecord;
ul TestUnion;
Какой размер памяти потребуется для этих переменных, если считать, что размер памяти, необходимой для типов данных «char» и «byte», составляет 1 байт, а размер памяти для типа данных «integer» — 2 байта?
Как изменится размер необходимой памяти, если предположить, что архитек тура целевой вычислительной системы предполагает выравнивание данных по границе 2 байта? 4 байта?
Вычислите следующие выражения, записанные в форме обратной польской записи:
О 1 2 * 3 5 4 * + +
012354**++
012+3*5+4
О 12+3*54+-
напишите для каждого из них выражение в форме инфиксной (обычной) записи.
Дана грамматика G({(.).\&.~.a). {S.T.E}. Р, S)
Р:
S -> S^T I т
Т -» Т&Е I Е
Е -> -Е | (S) | а
Постройте схему СУ-компиляции выражений входных цепочек языка, заданного этой грамматикой, во внутреннее представ чение в форме обратной польской записи.
Для грамматики, приведенной выше в задаче № 4, постройте схему СУ-перевода во внутреннее представление программы в форме триад. Выполните перевод в форму триад цепочки символов «ала&-а&(ал—а)».
Для грамматики, приведенной выше в задаче № 4, постройте схему СУ-перевода во внутреннее представление программы в форме команд ассемблера (например, Intel 80x86). Выполните перевод в ассемблерный код цепочки символов «аЛа&-а&(аЛ—а)».
Дана последовательность операции:
А - 2;
В - А * 3,
С :- 4*3 + 1.
А - D;
D - В + С:
С :- А:
В :- А + 4*С + 3*(D+12):
О преобразуйте ее в форму записи на основе триад
О выполните свертку операций
Дана последовательность операций:
А :- В*С + D*E:
В :- В*С + D*E:
С - В*С + D*E:
D - В*С - (D*E + 2).
Е •- В*С - (0*Е + 3);
А - А*Е + В*Е + 1.
В .- А*Е + В*Е + 1.
О преобразуйте ее в форму записи на основе триад;
О выполните исключение лишних операций.
Постройте алгоритм распределения регистров для хранения промежуточных результатов на основе внутреннего представления программы в форме триад.
Постройте алгоритм перевода внутреннего представления программы в форме триад в последовательность команд ассемблера.
ГЛАВА 15 Современные системы программирования
Понятие и структура системы программирования
История возникновения систем программирования
Всякий компилятор является составной частью системного программного обеспечения. Основное назначение компиляторов — служить для разработки новых прикладных и системных программ с помощью языков высокого уровня.
Любая программа, как системная, так и прикладная, проходит этапы жизненного цикла, начиная от проектирования и вплоть до внедрения и сопровождения [13, 25, 43[ А компиляторы — это средства, служащие для создания программного обеспечения на этапах кодирования, тестирования и отладки.
Однако сам по себе компилятор не решает полностью всех задач, связанных с разработкой повой программы. Средств только лишь компилятора недостаточно для того, чтобы обеспечить прохождение программой указанных этапов жизненного цикла. Поэтому компиляторы — это программное обеспечение, которое функционирует в тесном взаимодействии с другими техническими средствами, применяемыми на данных этапах.
Основные технические средства, используемые в комплексе с компиляторами, включают в себя следующие программные модули:
текстовые редакторы, служащие доя создания текстов исходных программ;
компоновщики, позволяющие объединять несколько объектных модулей, порождаемых компилятором, в единое целое;
библиотеки прикладных программ, содержащие в себе наиболее часто используемые функции п подпрограммы в виде готовых объектных модулей;
загрузчики, обеспечивающие подготовку готовой программы к выполнению;
отладчики, выполняющие программу в заданном режиме с целью поиска, обнаружения и локализации ошибок.
Первоначально компиляторы представляли собой обособленные программные модули, решающие исключительно задачу перевода исходного текста программы на входном языке в язык машинных кодов. Компиляторы разрабатывались вне связи с другими техническими средствами, с которыми им приходилось взаимодействовать. В задачу разработчика программы входило обеспечить взаимосвязь всех используемых технических средств:
подать входные данные в виде текста исходной программы па вход компиля- гора;
получить от компилятора результаты его работы в виде набора объектных файлов;
подать весь набор полученных объектных файлов вместе с необходимыми библиотеками подпрограмм на вход компоновщику;
получить от компоновщика единый файл программы (исполняемый файл) и подготовить его к выполнению с помощью загрузчика;
поставить программу на выполнение, при необходимости использовать отладчик для проверки правильности выполнения программы.
Все эти действия выполнялись с помощью последовательности команд, инициировавших запуск соответствующих программных модулей с передачей им всех необходимых параметров. Параметры передавались каждому модулю в командной строке и представляли собой набор имен файлов и настроек, реализованных в виде специальных «ключей». Пользователи могли выполнять эти команды последовательно вручную, а с развитием средств командных процессоров ОС они стали объединять их в командные файлы.
Со временем разработчики компиляторов постарались облегчить труд пользователей, предоставив им все необходимое множество программных модулей в составе одной поставки компилятора. Теперь компиляторы поставлялись уже вкупе со всеми необходимыми сопровождающими техническими средствами. Кроме того, были унифицированы форматы объектных файлов и файлов библиотек подпрограмм. Теперь разработчики, имея компилятор от одного производителя, могли в принципе пользоваться библиотеками и объектными файлами, полученными от другого производи геля компиляторов.
Для написания командных файлов компиляции был предложен специальный командный язык — язык Makefile [31, 39, 67, 68, 90]. Он позволял в достаточно гибкой и удобной форме описать весь процесс создания программы от порождения исходных текстов до подготовки ее к выполнению. Это было удобное, но достаточно сложное техническое средство, требующее от разработчика высокой степени подготовки и профессиональных знаний, поскольку сам командный язык М ikcfile был по сложности сравним с простым языком программирования. Язык Makefile стал стаи щртиы.м средством, единым для компиляторов всех разработчиков.
Такая структура средств разработки существовала достаточно долгое время, а в некоторых случаях она используется и по сей день (особенно при создании системных программ). Ее широкое распространение было связано с тем, что сама по себе вся эта структура средств разработки была очень удобной при пакетном выполнении программ на компьютере, что способствовало ее повсеместному применению в эпоху «mainframe».
Следующим шагом в развитии средств разработки стало появление так называемой «интегрированной среды разработки». Интегрированная среда объединила в себе возможности текстовых редакторов исходных текстов программ и командный язык компиляции. Пользователь (разработчик исходно!! программы) теперь не должен был выполнять всю последовательность действий от порождения исходного кода до его выполнения, от него также не требовалось описывать этот процесс с помощью системы команд в Makefile. Теперь ему было достаточно только указать в удобной интерфейсной форме состав необходимых для создания программы исходных модулей и библиотек. Ключи, необходимые компилятору и другим техническим средствам, также задавались в виде интерфейсных форм настройки.
После этого интегрированная среда разработки сама автоматически готовила всю необходимую последовательность команд Makefile, выполняла ых, получала результат и сообщала о возникших ошибках при их наличии. Причем сам текст исходных модулей пользователь мог изменить здесь же, не прерывая работ)' с ин тегрировапной средой, чтобы потом при необходимости просто повторит ь весь процесс компиляции.
Создание интегрированных сред разработки стало возможным благодаря бурному развитию персональных компьютеров и появлению развитых средств интерфейса пользователя (сначала текстовых, а потом и графических). Их появление на рынке определило дальнейшие развитие такого рода технических средств. Пожалуй, первой удачной средой такого рода можно признать интегрированную среду программирования Turbo Pascal на основе языка Pascal производства фирмы Borland [34, 71, 72, 98] Ее широкая популярность определила тот факт, что со временем все разработчики компиляторов обратились к созданию интегрированных средств разработки для своих продуктов.
Развитие интегрированных сред несколько снизило требования к профессиональным навыкам разработчиков исходных программ. Теперь в простейшем случае от разработчика требовалось только знание исходного языка (его синтаксиса и семантики). При создании прикладной программы ее разработчик мог в про стейшем случае даже не разбираться в архитектуре целевой вычислительной системы.
Дальнейшее развитие средств разработки также тесно связано с повсеместным распространением развитых средств графического интерфейса пользователя. Такой интерфейс стал неотъемлемой составной частью многих современных ОС и так называемых графических оболочек [35, 71]. Со временем он стал стандартом «де-факто» практически во всех современных прикладных программах.
Это не могло не сказаться на требованиях, предъявляемым к средствам разработки программного обеспечения. В их состав были сначала включены соответ-
ствующис библиотеки, обеспечивающие поддержку развитого графического интерфейса пользователя и взаимодействие с функциями API (application program interface, прикладной программный интерфейс) операционных систем. Л затем для работы с ними потребовались дополнительные средства, обеспечивающие разработку внешнего вида интерфейсных модулей. Такая работа была уже более характерна для дизайнера, чем для программиста.
Для описания графических элементов программ потребовались соответствующие языки. На их основе сложилось понятие «ресурсов» (resources) прикладных программ.
Ресурсами прикладной программы будем называть множество данных, обеспечивающих внешний вид интерфейса пользователя этой программы и не связанных напрямую с логикой выполнения программы. Характерными примерами ресурсов являются: тексты сообщений, выдаваемых программой; цветовая гамма элементов интерфейса; надписи на таких элементах, как кнопки и заголовки окоп и т. п.
Для формирования структуры ресурсов, в свою очередь, потребовались редакторы ресурсов, а затем и компиляторы ресурсов, обрабатывающие результат их работы. Ресурсы, полученные с выхода компиляторов ресурсов, стали обрабатываться компоновщиками и загрузчиками.
Весь этот комплекс программно-технических средств в настоящие время составляет новое понятие, которое здесь названо «системой программирования».
Структура современной системы программирования
Системой программирования будем называть весь комплекс программных средств, предназначенных для кодирования, тестирования и отладки программного обеспечения. Нередко системы программирования взаимосвязаны и с другими техническими средствами, служащими целям создания программного обеспечения па более ранних этапах жизненного цикла (от формулировки требований и анализа до проектирования). Однако рассмотрение таких систем выходит за рамки данного учебного пособия.
Системы программирования в современном мире доминируют на рынке средств разработки. Практически все фирмы-разработчики компиляторов поставляют свои продукты в составе соответствующей системы программирования в комплексе всех прочих технических средств. Отдельные компиляторы являются редкостью и, как правило, служат только узко специализированным целям.
синтаксические конструкции в тексте входной программы, установить тип и проверить правильность каждой синтаксической конструкции и, наконец, представить синтаксические конструкции в виде, удобном для дальнейшей генерации текста результирующей программы.
В основе синтаксического анализатора лежит распознаватель текста входной программы на основе грамматики входного языка. Как правило, синтаксические конструкции языков программирования могут быть описаны с помощью КС- грамматик (см. главу «Контекстно-свободные языки»), реже встречаются языки, которые могут быть описаны с помощью регулярных грамматик (см. главу «Регулярные языки») Чаще всего регулярные грамматики применимы к языкам ассемблера, а языки высокого уровня построены на основе синтаксиса КС-языков.
Распознаватель дает ответ на вопрос о том, принадлежит пли нет цепочка входных символов заданному языку. Однако, как и в случае лексического анализа, задача синтаксического разбора не ограничивается только проверкой принадлежности цепочки заданному языку. Необходимо выполнить все перечисленные выше задачи, которые должен решить синтаксический анализатор. В гаком варианте анализатор уже не является разновидностью МП-автомата — его функции можно трактовать шире. Синтаксический анализатор должен иметь некий выходной язык, с помощью которого он передает следующим фазам компиляции всю информацию о найденных и разобранных синтаксических структурах. В таком случае он уже является преобразователем с магазинной памятью — МП-пре- образователем [6, 23, 26, 40, 42].
Синтаксический разбор — это основная часть компилятора на этапе анализа. Без выполнения синтаксического разбора работа компилятора бессмысленна, в то время как лексический разбор в принципе является необязательной фазой. Все задачи по проверке синтаксиса входного языка могут быть решены на этапе синтаксического разбора. Сканер только позволяет избавить сложный по структуре синтаксический анализатор от решения примитивных задач по выявлению и запоминанию лексем входной программы.
Выходом лексического анализатора является таблица лексем (или цепочка лексем). Э та таблица образует вход сии гаксичсского анализатора, который исследует только один компонент каждой лексемы — се тип. Остальная информация о лексемах используется на более поздних фазах компиляции при семантическом анализе, подготовке к генерации и генерации кода результирующей программы. Синтаксический анализ (или разбор) — это процесс, в котором исследуется таблица лексем и устанавливается, удовлетворяет ли она структурным условиям, явно сформулированным в определении синтаксиса языка.
На рис 15.1 приведена общая структура современной системы программировi- ния. На ней выделены все основные составляющие современной системы программирования и их взаимосвязь Отдельные составляющие разбиты ио труппам в соответствии с этанами развития средств разработки Эти группы отраж ют псе этапы развития от отдельных программных к мпонептов до цельной системы программирования
Рис. 15.1. Общая структура и этапы развития систем программирования
Из рис. 15.1 видно, что современная система программирования ■ это достаточно сложный комплекс различных программно-технических средств Все они служат цели создания прикладного и системного программною обеспечения
Тенденция такова, что все развитие систем программирования идет в направлении неуклонного повышения их дружественности и сервисных возможностей Это связано с тем, что на рынке в первую очередь лидируют те системы Дрогр ш- мирования, которые позволяют существенно снизить трудозатраты, погребные для создания программного обеспечения па Стаггах жизненною цикла, связанных с кодированием, тестированием и отладкой программ Пота «агель снижения трудозатрат в настоящее время читает; я более существенным, чем показатели, определяющие эффективность результирующей программы, построенной с помощью системы программирования.
В качестве основных тенденции в развитии современных систем пршраммиро- вания следует указать внедрение в них средств разработки на основе так называемых «языков четвертого поколения» — 4GL (four generation anguages), — а также поддержка систем «быстрой разр аботки программного обеспечения» — RAD (rapid application development).
Языки четвертого поколения — 4GL — представляют собой широкий набор средств, ориентированных на проектирование и разработку программного обес ечення Они строятся на ос ове оперирования не синтаксическими структурами я$ыка и описаниями элементов интерфейса, а представляющими их графическими образами. На таком уровне проектировать и разрабатывать прикладное программное. обеспечение может пользователь, не являющийся квалифицированным программистом, зато имеющий представление о предметной области#} па работу в
которой ориентирована прикладная программа. Языки четвертого поколения являются следующим (четвертым по счету') этапом в развитии систем программирования.
Описание программы, построенное на основе языков 4GL, транслируется затем в исходный текст и файл описания ресурсов интерфейса, представляющие собой обычный текст па соответствующем входном языке высокого уровня. С этим текстом уже может работать профессиональный программист-разработчик — он может корректировать и дополнять его необходимыми функциями. Дальнейший ход создания программного обеспечения идет уже традиционным путем, как это показано на рис. 15.1.
Такой подход позволяет разделить работу' проектировщика, ответственного за общую концепцию всего проекта создаваемой системы, дизайнера, отвечающего за внешний вид интерфейса пользователя, и профессионального программиста, отвечающего непосредственно за создание исходного кода создаваемого программного обеспечения.
В целом языки четвертого поколения решают уже более широкий класс задач, чем традиционные системы программирования. Они составляют часть средств автоматизированного проектирования и разработки программного обеспечения, поддерживающих; все этапы жизненного цикла — CASE-систем. Их рассмотрение выходит за рамки данного учебного пособия.
Более подробную информацию о языках 4GL, технологии RAD и CASE-снсте- мах в целом можно получить в [35, 43].
Принципы функционирования систем программирования
Функции текстовых редакторов в системах программирования
Текстовый редактор в системе программирования — это программа, позволяющая создавать, изменять и обрабатывать исходные тексты программ на языках высокого уровня.
В принципе текстовые редакторы появились вне какой-либо связи со средствами разработки. Они решали задачи создания, редактирования. обработки и храпения на внешнем носителе любых текстов, которые не обязательно должны были быть исходными текстами программ на языках высокого уровня. Эти функции многие текстовые редакторы выполняют и по сей день
Возникновение интегрированных сред разработки на определенном этапе развития средств разработки программного обеспечения позволило непосредственно включить текстовые редакторы в состав этих средств. Первоначально такой подход привел к тому, что пользователь (разработчик исходной программы) работал только в среде текстового редактора, не отрываясь от нее для выполнения компиляции, компоновки, загрузки и запуска программы на выполнение. Для этою
потребовалось создать средства, позволяющие отображать ход всего процесса разработки программы в среде текстового редактора, — такие, например, как метод отображения ошибок в исходной программе, обнаруженных на этапе компиляции, с позиционированием на место в тексте исходной программы, содержащее ошибку.
Можно сказать, что с появлением интегрированных сред разработки ушло в прошлое то время, koi да разработчики исходных текстов вынуждены были первоначально готовить тексты про грамм на бумаге с последующим вводом их в компьютер. Процессы написания текстов и собственно создание программного обеспечения стали единым целым.
Интегрированные среды разработки оказались очень удобным средством. Они стали завоевывать рынок средств разработки npoi раммпого обеспечения. А с их развитием расширялись и возможности, предоставляемые разработчику в среде текстового редактора. Со временем появились средства пошаговой отладки программ непосредственно по их исходному тексту, объединившие в себе возможности отладчика и редактора исходного текста. Другим примером может служить очень удобное средство, позволяющее графически выделить в исходном тексте программы все лексемы исходного языка по их типам — оно сочетает в себе возможности редактора исходных текстов и лексического анализатора компилятора.
В итоге в современных системах программирования текстовый редактор стал важной составной частью, которая не только позволяет пользователю подготавливать исходные тексты программ, но и выполняет все интерфейсные и сервисные функции предоставляемые пользователю системой программирования. И хотя современные разрабогчики по-прежнему мо<ут использовать произвольные средства для подготовки исходных текстов программ, как правило, они все же предпочитают пользоваться именно тем текстовым редактором, который включен в состав данной системы программирования.
Компилятор как составная часть системы программирования
Компиляторы являются, безусловно, основными модулями в составе любой системы программирования. Поэтому не случайно, что они стали одним из главных предметов рассмотрения в данном учебном пособии. Без компилятора никакая система программирования не имеет смысла, а все остальные ее составляющие на самом деле служат лишь целям обеспечения работы компилятора и выполнения им своих функций.
От первых этапов развития систем программирования вплоть до появления интегрированных сред разработки пользователи (разработчики исходных программ) всегда так или иначе имели дело с компилятором Они непосредственно взаимодействовали с ним как с отдельным про раммным модулем.
Сейчас, работая с системой программирования, пользователь, как правило, имеет дело только с ее интерфейсной частью, которую обычно представляет текстовый редактор с расширенными функциями. Запуск модуля компилятора и вся
его работа происходят автоматически и скрытно от пользователя — разработчик видит только конечные результаты выполнения компилятора. Хотя многие современные системы программирования сохранили прежнюю возможность непосредственного взаимодействия разработчика с компилятором (это и Makefile, и так называемый «интерфейс командной строки»), но пользуется этими средствами только узкий круг профессионалов. Большинство пользователей систем программирования сейчас редко непосредственно сталкиваются с компиляторами.
На самом деле, кроме самого основного компилятора, выполняющего перевод исходного текста па входном языке в язык машинных команд, большинство систем пртраммпрованпя могут содержать в своем составе целый ряд других ком ппляторов и трансляторов. Так, большинство систем программирования содержат в своем составе и компилятор с языка ассемблера, и компилятор (транслятор) с входного языка описания ресурсов. Все они редко непосредственно взаимодействуют с пользователем.
'Гем не менее, работая с любой системой программирования, следует помнить, что основным модулем ее всегда является компилятор. Именно технические характеристики компилятора, прежде всего, влияют на эффективность результирующих программ, порождаемых системой программирования.
Компоновщик. Назначение и функции компоновщика
Компоновщик (или редактор связей) предназначен для связывания между собой объектных файлов, порождаемых компилятором, а также файлов библиотек, входящих в состав системы программирования*}
Объектный файл (или набор объектных файлов) не может быть исполнен до тех нор, пока все модули и секции не будут в нем увязаны между еобУй Это и делает редактор связей (компоновщик). Результатом его работы является единый файл (часто называемый «исполняемым файлом»), который содержит весь текст результирующей программы на языке машинных кодов. Компоновщик может порождать сообщение об ошибке, если при попытке собрать объектные файлы в единое целое он не смей обнаружить какой-либо необходимой! составляющей Функция компоновщика достаточно проста. Он начинает свою работу с того, что выбирает из первою объектного модуля программную секцию и присваивает ей начальный адрес Пре i раммные секции остальных объектных модулей получают адреса отпоен сельпо начального адреса в порядке следования. При этом может выполнятся также функция выравнивания начальных адресов программных е акций Одновременно с объединением текстов программных секций объединяются секции данных, табищы идентификаторов и внешних имен. Разрешаются межеекцнонные ссылки.
1 Автору встречались в технической литературе примеры, когда компоновщик называли загрузчиком. С точки зрения автора это иршщинналык) неиравитыю Функции компоновщика и за! рузчика существенно раз шчаются В современных сне юмах нрограммпро иания загрузчик, как правило, огсутстуег, его функции выполняет ОС.
Процедура разрешения ссылок сводится к вычислению значений адресных констант процедур, функций и переменных с учетом перемещений секций относительно начала собираемого программного модуля. Если при этом обнаруживаются ссылки к внешним переменным, отсутствующим в списке объектных модулей, редактор связей организует их поиск в библиотеках, доступных в системе программирования. Если же и в библиотеке необходимую составляющую найти нс удается, формируется сообщение об ошибке.
Обычно компоновщик формирует простейший программный модуль создаваемый как единое целое. Однако в более сложных случаях компопонщ ик может создавать и другие модули: программные модули с оверлейной структурой, объектные модули библиотек и модули динамически подключаемых бнб пютск (работа с оверлейными и динамически подключаемыми модулями в ОС описана в первой части данного пособия).
Загрузчики и отладчики. Функции загрузчика
Большинство объектных модулей в современных системах программирования строятся па основе так называемых относительных адресов Компилятор порождающий объектные файлы, а затем и компоновщик, объединяющий их в единое целое, не могут знать точно, в какой реальной области памяти компьютера будет располагаться программа в момент ее выполнения. Поэтому они работают нс с реальными адресами ячеек ОЗУ, а с некоторыми относительными адресами. Та кис адреса отсчитываются от некоторой условной точки, принятой за начало области памяти, занимаемой результирующей программой (обычно это точка начала первого модуля программы).
Конечно, им одна программа не может быть исполнена в этих относительных адресах. Поэтому требуется модуль который бы выполнял преобразование относительных адресов в реальные (абсолютные) адреса непосредственно в момент запуска программы на выполнение. Этот процп с называется трансляцией адресов и выполняет его специальный модуль, называемый загрузчиком
Однако загрузчик не всегда является составной частью системы программирования, поскольку выполняемые нм функции очень зависят от архитектуры целевой вычислительной системы, в которой выполняется результируки 1ая программа, созданная системой программирования. На первых этапах развития ОС загрузчики существовали в виде отдельных модулей, которые выполняли трансляцию адресов и готовили программу к выполнению — создавали так называемый «образ задачи». Такая схема была характерна для многих ОС (например для ОСРВ па ЭВМ типа СМ-1, ОС RSX/11 или RAFOS на ЭВМ типа СМ-4 и т. п. (20, 57]). Образ задачи можно было сохранить на внешнем носителе или же создавать его вновь всякий раз при подготовке программы к вына нению
С развитием архитектуры вычислительных средств компьютера появилась возможность выполнять трансляцию адресов непосредственно в момент запуска программы на выполнение. Для этого потребовалось в состав исполняемого файла включить соответствующую таблицу, содержащую перечень ссылок на адреса, которые необходимо подвергнуть трансляции. В момент запуска исполняемого
файла ОС обрабатывала эту таблицу и преобразовьитла относительные адреса в абсолютные. Такая схема, например, характерна для ОС типа MS-DOS, которые широко распространены в среде персональных компьютеров. В этой схеме модуль загрузчика как таковой отсутствует (фактически он входит в состав ОС), а система программирования от ветственна только за подготовку таблицы трансляции адресов — эту функцию выполняет компоновщик.
В современных ОС существуют сложные методы преобразования адресов, которые работают непосредственно уже во время выполнения программы. Эти методы основаны на возможностях, аппаратно заложенных в архитектуру вычислительных комплексов. Методы трансляции адресов могут быть основаны на сегментной, страничной и сегментно-страничной организации памяти (все эти методы рассмотрены в первой части данного пособия). Тогда для выполнения трансляции адресов в момент запуска программы должны быть подготовлены соответствующие системные таблицы. Эти функции целиком ложатся на модули ОС, поэтому они нс выполняются в системах программирования.
Еще одним модулем системы программирования функции которого тесно связаны с выполнением программы, является отладчик.
Отладчик — это программный модуль, который позволяет выполнить основные задачи связанные с мониторингом процесса выполнения результирующ й при кладион программы Этот процесс называется отладкой и включает в себя следующие основные возможности:
последовательное пошаговое выполнение результирующей программы на основе шагов по машинным командам или по операторам входного языка;
выполнение результирующей программы до достижения ею одной из заданных точек останова (адресов останова)
выполнение результирующей программы до наступления некоторых заданных условий, связанных с данными и адресами, обрабатываемыми этой программой;
просмотр содержимого областей памяти, запятых командами или данными результирующей программы.
Первоначально отладчики представляли собой отдельные программные модули которые могли обрабатывать результирующую программу в терминах языка машинных команд Их возможности в основном сводились к моделированию выполнения результирующих программ в архитектуре соответствующей вычислительной системы Выполнение могло идти непрерывно либо по шагам.
Дальнейшее развитие отладчиков связано со следующими принципиальными моментами
появлением интегрированных сред разработки
появление возможностей аппаратной поддержки средств отладки во многих вычислительных системах.
Первый из этих шагов дал возможность разработчикам программ работать не в гермнпах машинных команд, а в терминах исходного языка программирования, что значительно сократило трудозатраты па отладку программного обес-
печения. При этом отладчики перестали быть отдельными модулями и стали интегрированной част ью систем программирования, поскольку они должны были теперь поддерживать работу с таблицами идентификаторов (см. раздел «Таблицы идентификаторов. Организация таблиц идентификаторов», глава 15) и выполнять задачу, обратную идентификации лексических единиц языка (см. раздел «Семантический анализ и подготовка к генерации кода», глава 14). Это связано с гем, что в такой среде отладка программы идет в терминах имен, данных пользователем, а не в терминах внутренних имен, присвоенных компилятором. Соответствующие изменения потребовались также в функциях компиляторов и компоновщиков, поскольку опи должны были включать таблицу имен в состав объектных и исполняемых файлов для се обработки отладчиком.
Второй шаг позволил значительно расширить возможности средств отладки. Теперь для них не требовалось моделировать работу и архитектуру соответствующей вычисли тельной системы. Выполнение результирующей программы в режиме отладки стало возможным в гой же среде, что и в обычном режиме. В задачу отладчика входили только функции перевода вычислительной системы в соответствующий режим перед запуском результирующей программы па отладку. Во многом эти функции являются приоритетными, поскольку зачастую требуют установки системных таблиц и флагов процессора вычислительной системы (мы рассмотрели большую часть этих вопросов в первой части данного учебного пособия).
Отладчики в современных системах программирования представляют собой модули с развитым интерфейсом пользователя, работающие непосредственно с текстом и модулями исходной программы. Многие их функции интегрированы с функциями текстовых редакторов исходных текстов, входящих в состав систем программирования.
Библиотеки подпрограмм как составная часть систем программирования
Библиотеки подпрограмм составляют существенную часть систем программирования. Наряду с дружественностью пользовательского ин терфейса состав доступных библиотек подпрограмм во многом определяет возможности системы программирования и ее позиции на рынке средств разработки программного обеспечения.
Библиотеки подпрограмм входили в состав средств разработки, начиная с самых ранних этапов их развития. Даже когда компиляторы еще представляли собой отдельные программные модули, они уже были связаны с соответствующими библиотеками, поскольку компиляция так или иначе предусматривает связь программ со стандартными функциями исходного языка. Эти функции обязательно должны входить в состав библиотек.
С точки зрения системы программирования, библиотеки подпрограмм состоят из двух основных компонентов. Это собственно файл (или множество файлов) библиотеки, содержащий объектный код, и набор файлов описаний функций, подпрограмм, констант и переменных, составляющих библиотеку. Описания оформ-
ляются на соответствующем входном языке (например, для языка С или С++ это будет набор заголовочных файлов) Иногда эти файлы могут быть совмещены.
Объектный код библиотеки подключается компоновщиком к результирующей программе при создании исполняемого модуля. По структуре он обычно мало чем отличается о г обычных объектных файлов, порождаемых компилятором. Чаще всего система программирования храпит объектный код входящих в ее состав библиотек в некотором упакованном виде.
Набор файлов описания библиотеки служит для информирования компилятора о составе входящих в библиотеку функций. Обрабатывая эти файлы, компилятор получает всю необходимую информацию о составе библиотеки с точки зрения входного языка программирования. Эти файлы предназначены только для того, чтобы избавить разработчика от необходимости постоянного описания библиотечных функций, подпрограмм, констант и переменных.
В состав системы программирования может входить большое количество разнообразных библиотек. Среди них всегда можно выделить основную библиотеку, содержащую обязательные функции входного языка программирования. Эта биб- литека всегда используется компилятором, поскольку без нее разработка программ па данном входном языке невозможна. Разработчик даже не должен указывать необходимость использования этой библиотеки (некоторые разработчики программ п нс догадываются, что пользуются подобной библиотекой). Все остальные библиотеки необязательны и подключаются к результирующей программе только но прямому указанию разработчика.
В процессе развития систем программирования состав библиотек постоянно расширялся. В них включалось все большее и большее число функций. Это было вызвано тем, что система программирования, предоставляющая пользователю более широкий выбор библиотечных функций, получала лучшие позиции па рынке средств разработки программного обеспечения. Сами по себе библиотеки подпрограмм и функций, созданных для того или иного языка, также становились товаром па рынке средств разработки. Этот естественный процесс продолжается до сих пор, и чем длиннее история существования того или иного языка программирования, тем шире диапазон существующих для пего библиотек.
Системы программирования для некоторых языков продолжают существовать во многом благодаря тому, что для них создан мощный аппарат библиотечных функций Так язык FORTRAN существует благодаря широчайшему набору математических и физических функций, а язык COBOL — функций из финансовой сферы.
В ходе развития систем программирования принципы создания и использования библиотечных функций претерпели мало изменений. Принципиально новые возможности предоставили только современные ОС, которые позволили подключать к результирующим программам не статические, а динамические библиотеки.
Динамические библиотеки в отличие от традиционных (статических) библиотек подключаются к программе не в момент ее компоновки, а непосредственно в ходе выполнения, как только программа затребовала ту или иную функцию,
находящуюся в библиотеки. Преимущества таких библиотек очевидны — они нс требуют включать в программу объектный код часто используемых функции, чем существенно сокращают объем кода. Различные программы, выполняемые в ОС, могут пользоваться кодом одной и той же библиотеки, содержащейся в данной ОС.
Формат файлов динамических библиотек может быть различным — как правило, он строго определяется требованиями соответствующей ОС. Как и статические библиотеки, динамические библиотеки предусматривают описание входящих в них функций в виде текста на соответствующем входном языке, чтобы дать информацию о них компилятору в ходе обработки исходного текста программы и избавить разработчика от создания таких описаний.
Конечно, динамические библиотеки требуют наличия в ОС специального механизма, позволяющего подключать часть объектного кода непосредственно по ходу выполнения программы. Однако для современных ОС, выполняющихся в вычислительных системах, которые поддерживают широкий набор методов адресации, это не является проблемой (см. часть 1 данного пособия).
Проблемы, главным образом, связаны с тем, что различные прикладные программы связываются с объектным кодом, непосредственно не входящим в их состав. Таким образом, логика (алгоритм) работы всех этих программы становится зависимой от кода библиотек. Изменение библиотеки, которое может происходить независимо от разработчиков прикладных программ, может непроизвольно повлиять и па функционирование данных программ. Такое развитие событий порой оказывается неожиданным для пользователя программ
Поэтому использование динамических библиотек накладывает определенные обязательства как на разработчика программы, так и на создателя самой библиотеки. Разработчик прикладной программы должен использовать библиотеку только так, как это определено ее создателем, а создатель библиотеки в случае се модификации должен организовывать изменения таким образом, чтобы они не сказывались на логике работы программ, ориентированных на предыдущие версии его библиотеки. Подавляющее большинство разработчиков (как программ, так п библиотек) стремится придерживаться этих правил, по нс всегда им это удается*.
Широкий набор динамических библиотек поддерживается всеми современными ОС. Как правило, они содержат системные функции ОС и общедоступные функции программного интерфейса (API). Кроме того, многие независимые разработчики предоставляют для различных систем программирования свои динамические библиотеки как отдельные товары на рынке средств разработки прикладных программ.
1 В современных ОС появились средства, позволяющие поддерживать не только файлы, но и версии динамических библиотек. Они во многом используют методы, применяемые в системах управления версиями исходных файлов. Тогда приложение, обращаясь к библиотеке, может указать не только имя, но и требуемую версию вызываемой функции библиотеки.
варианта данному лексическому анализатору па входе требуется задавать также и тип ожидаемой лексемы. Поэтому при непрямой работе лексического анализатора требуется его параллельное взаимодействие с синтаксическим распознавателем.
Выполнение действий, связанных с лексемами
Выполнение действий в процессе распознавания лексем представляет для сканера гораздо меньшую проблему. Фактически КА, который лежит в основе распознавателя лексем, должен иметь не только входной язык, ио и выходной. Он должен не только уметь распознать правильную лексему на входе, ио и породить связанную с ней последовательность символов на выходе. В такой конфигурации КА преобразуется в конечный преобразователь [5, 6, т. 1, 42].
Для сканера действия по обнаружению лексемы могут трактоваться несколько шире, чем только порождение цепочки символов выходного языка. Сканер должен уметь выполнять такие действия, как запись найденной лексемы в таблицу лексем, поиск ее в таблице символов и запись новой лексемы в таблицу символов. Набор действии определяется реализацией компилятора. Обычно эти действия выполняются сразу же по обнаружению конца распознаваемой лексемы.
В КА сканера эти действия можно отобразить сравнительно просто — достаточно иметь возможность с каждым переходом на графе автомата (или в функции переходов автомата) связать выполнение некоторой произвольной функции f(q,a), где q — текущее состояние автомата, а — текущий входной символ. Функция может выполнять произвольные действия, доступные сканеру, в том числе работать с хранилищами данных, имеющимися в компиляторе (функция может быть и пустой — не выполнять никаких действий). Такую функцию, если она есть, обычно записывают на графе переходов КА под дугами, соединяющими состояния КА.
Построение лексических анализаторов
Теперь можно рассмотреть практическую реализацию сканеров. В принципе компилятор может иметь в своем составе не один, а несколько лексических анализаторов, каждый из которых предназначен для выборки и проверки определенного типа лексем.
Таким образом, обобщенный алгоритм работы простейшего лексического анализатора в компиляторе можно описать следующим образом:
из входного потока выбирается очередной символ, в зависимости от которого запускается тот или иной сканер (символ может быть также проигнорирован либо признан ошибочным);
запущенный сканер просматривает входной поток символов программы на исходном языке: выделяя символы, входящие в требуемук? лексему, до обнаружения очередного символа, который может ограничивать лексему, либо до обнаружения ошибочного символа;
при успешном распознавании информация о выделенной лексеме заносится в таблицу лексем и таблицу идентификаторов, алгоритм возвращается к перво-
1 TR — task register.
1 Так, со стороны компании Microsoft проектом руководил известный системный программист Gordon Letwin.
2 Bit тар — битовая карта.
1 По сути дела, том (volume) — это не что иное, как разлет или логический диск.
2 ACL (access control list) — список прав доступа к данному файлу пли объекту.
2 Background session — «фоновая сессия», то есть задача, запущенная пользователем, но в настоящий момент не находящаяся непосредственно в работе. Говорят, что эта задача выполняется на фоне текущих активных вычислений.
POSIX (Portable operating system for computing environments). Синоним IEEE Std 1003.1. C 1990 года является международным стандартом на машинно-независимый (переносимый) интерфейс компьютерной среды Этот стандарт разработан американским институтом IEEE в 1988 году. Он представляет собой набор функций взятых из операционных систем AT&T UNIX System V и Berkeley Standard Distribution UNIX. Основное внимание в требованиях этого стандарта уделяется взаимодействию прикладных программ с операционной системой. Написание прикладных программ в данном стандарте позволяет создавать программы, легко переносимые из одной операционной среды в другую.