Одноуровневая память
|
И каждый уже десять лет учит роли,
О которых лет десять как стоит забыть
Б.Гребенщиков |
Эффективное управление рабочими наборами пользовательских программ и,
с другой стороны, эффективное кэширование запросов к дискам позволяют
если и не скрыть полностью, то в значительной мере сгладить различие в
производительности оперативной и внешней памяти компьютера. Поэтому сразу
же после возникновения первых машин с виртуальной памятью начались попытки
спрятать все остальные различия между этими двумя типами памяти от программиста,
реализовав так называемую одноуровневую память.
Интерес к этой идее сохраняется до сих пор. Например, на сайте [dz.yandex.ru]
в конце 2000 года была серия публикаций и довольно бурная дискуссия о
достоинствах и недостатках "персистентных объектов" — объектов
в терминах объектно-ориентированного программирования, которые переживают
перезагрузки и выключения питания системы. Для хранения таких объектов
может использоваться как флэш-память, так и другие формы энергонезависимой
памяти, те же жесткие диски. Владелец сайта и инициатор дискуссии, Дмитрий
Завалишин, отстаивал тезис о том, что такие объекты представляют собой
сокровенную мечту и своего рода Священный Грааль всего программирования
и развития вычислительной техники.
Одноуровневая память в Multix
Пионером в реализации одноуровневой памяти была ОС Multix фирмы Honeywell.
Эта система была разработана в конце 60-х годов и оказала огромное влияние
на развитие вычислительной техники как прямо, так и посредством своего
потомка Unix. Несколько машин с этой ОС эксплуатировались и были доступны
через Internet (во всяком случае, отвечали на запрос ping) еще в 1997
году.
Multix предоставляла средства отображения файлов на адреса оперативной
памяти наравне с более традиционными средствами ввода-вывода. Ранние версии
Unix предназначались в том числе и для работы на машинах без диспетчера
памяти или с виртуальной памятью на основе базовых регистров, где возможен
лишь традиционный ввод-вывод. Однако современные системы этого семейства
отчасти вернулись к истокам и также предоставляют этот способ доступа
к файлам.
Впрочем, как отмечалось выше, предоставляемый современными Unix-системами
mmap скорее представляет собой документированный внутренний интерфейс
загрузчика, чем полноценное средство организации одноуровневого доступа:
работа с отображенным файлом не полностью прозрачна для пользователя.
Например, изменения содержимого отображенного ОЗУ и наоборот, изменения,
внесенные в файл с момента отображения, необходимо синхронизовать друг
с другом вручную, используя системный вызов msynch. Средства, предоставляемые
для этой цели Windows NT/2000/XP, более прозрачны и просты в использовании,
но тоже применяются относительно редко.
Для того чтобы понять, возможна ли полностью одноуровневая память, и
если да, то в какой мере, давайте сначала установим различия между ОЗУ
и наиболее распространенным типом внешней памяти, жестким магнитным Диском.
Во-первых, объем оперативной памяти в современных компьютерах измеряется
десятками и сотнями мегабайт, а у систем коллективного пользования Достигает
нескольких гигабайт. Характерная емкость жесткого диска начинается с нескольких
гигабайт (диски меньшего объема просто не производятся) и заканчивается
сотнями гигабайт. Если мы хотим адресовать все это единым образом, мы
должны расширить адресное пространство. 32 бит явно недостаточно, 64 бит
на ближайшие годы хватит, но рост емкости дисковых массивов идет по экспоненте.
Впрочем, расширение адресного пространства само по себе не представляет
большой проблемы — мало 64 бит, сделаем 128, тем более что речь идет не
об адресной шине процессора, используемой только для адресации ОЗУ а о
виртуальном адресе. Современные технологии без особых проблем позволяют
упаковать арифметико-логическое устройство и регистры такой разрядности
в один кристалл, сделать корпус с надлежащим числом ног, печатную плату,
в которую можно впаять такой корпус и автоматическую линию, которая будет
распаивать корпуса по платам. Да, это будет не Spectrum, вручную не спаяешь
— но и материнскую плату современного PC-совместимого компьютера вручную
невозможно развести и спаять. Ну и что?
Во-вторых, оперативная память теряет свое содержимое при выключении питания,
а жесткий диск — сохраняет, поэтому нередко используется еще одна характеристика
дисковой памяти как противопоставление оперативной — постоянная память.
Можно вспомнить и о промежуточных решениях, например о флэш-памяти и ее
аналогах, которые адресуются почти как ОЗУ, а данные хранят почти как
жесткий диск. Наличие промежуточных решений наводит на мысль, что особых
проблем "с этой стороны ждать не приходится. На самом деле, проблема
здесь есть, но обсудим мы ее чуть позже.
В-третьих, и это связано сразу с обеими вышеназванными причинами, человек
снисходит до ручного наведения порядка на диске (удаления мусора и пр.)
гораздо чаще, чем до выполнения той же операции в ОЗУ. Поэтому жесткие
диски обычно снабжаются еще одной схемой адресации, ориентированной на
использование человеком: когда вместо адреса, представляемого целым числом,
используется символическое имя.
Трансляция имени в адрес (сектор, поверхность и дорожку жесткого диска)
осуществляет файловая система. Вопросы организации файловых систем, каталогов
и управления структурами свободной и занятой дисковой памяти обсуждаются
в главе 10.
Единственная из используемых в настоящее время архитектур, предоставляющая
"честную" одноуровневую память, AS/400, имеет два представления
указателя, неразрешенное — с именем в качестве селектора сегмента, и разрешенное
— с бинарным представлением этого селектора. Можно себе представить и
другие механизмы трансляции имен в адреса, например получение указателя
посредством исполнения системного вызова
void *resolve(char * object_name, int flags)
или чего-нибудь в этом роде. Особых технических проблем это не представляет,
вопрос в том, надо ли это.
Изложение одного из доводов в пользу того, что это надо далеко не всегда,
мы предлагаем начать издалека, а именно с весьма банального тезиса, что
писать программы без ошибок человечество до сих пор не научилось и вряд
ли научится в обозримом будущем. Исполнение программы, содержащей ошибки,
может порождать не только обращения по неверным указателям и выход за
границы массивов (наиболее разрушительные типы ошибок, от которых сегментные
и страничные диспетчеры памяти предоставляют определенную защиту), но
и более тонкие проблемы — фрагментацию и/или утечку свободной памяти и
различные рассогласования (в СУБД применяют более точный термин — нарушения
целостности данных).
Накопление этих ошибок рано или поздно приводит к тому, что программа
теряет способность функционировать. Потеря этой способности может быть
обнаружена и пользователем ("что-то прога глючит", "зависла"),
и системой (исчерпание квоты памяти или других ресурсов, превышение лимитов
роста своп-пространства или доступ по недопустимому адресу), и даже самой
программой — если существуют формальные критерии целостности данных, в
различных местах кода могут встречаться проверки этих критериев.
Учебники по программированию, например [Дейкстра 1978], настоятельно рекомендуют
вырабатывать такие критерии и вставлять соответствующие проверки везде,
где это целесообразно. Понятно, что забывать о здравом смысле и вставлять
их после каждого оператора, или даже лишь перед каждой операцией, для
исполнения которой требуется целостность, далеко не всегда оправдано с
точки зрения производительности, так что при реальном программировании
надо искать баланс.
Иногда в ходе таких проверок даже удается восстановить целостность (примеры
алгоритмов проверки и восстановления структур файловой системы приводятся
в главе 12), но очевидно, что далеко
не всегда это возможно. В этом случае остается лишь проинформировать пользователя,
что у нас "Assertion failed" (предположение нарушено) и по возможности
мирно завершиться.
Сохранять при этом данные в постоянную память опасно: если мы не можем
восстановиться, мы часто не можем и знать, насколько далеко зашло нарушение
целостности, поэтому сохранение чего бы то ни было в таком состоянии чревато
полной или частичной (что тоже неприятно) потерей информации. В частности,
именно из этих соображений ОС общего назначения, обнаружив ошибку в ядре,
сразу рисуют регистры на консоли (рискуя при этом целостностью файловых
систем и пользовательских данных), а не предлагают пользователю предпринять
какие-либо меры пс сохранению данных.
Смысл останова задачи или всей системы с последующим ее перезапуском состоит
в том, чтобы заново проинициализировать структуры данных, используемые
при работе программного обеспечения. Это действие можно описать ка!
"контролируемое забывание" всего плохого, что накопилось в памяти
за время работы программы, и начало с более или менее чистого листа.
Сервис автоматического перезапуска в различных формах предоставляется
многими приложениями, ОС и даже аппаратными архитектурами.
Например, практически обязательным элементом современных микроконтроллеров
является watchdog timer (сторожевой таймер,
дословно — "сторожевая собака"), часто работающий от собственного
осциллятора, а не от общего тактового генератора машины. Программа микроконтроллера
должна периодически сбрасывать сторожевой таймер, иначе, досчитав до конца,
он делает вывод, что программа "зависла" и инициирует системный
сброс.
Именно сторожевой таймер несколько раз перезагружал бортовой компьютер
посадочного модуля "Аполлона-И" и, по-видимому, спас этим жизни
астронавтов и лунную программу США [NASA 182505].
Аналогичные схемы часто применяются во встраиваемых приложениях, особенно
ориентированных на длительную автономную работу. Действительно, встраиваемое
приложение может оказаться в весьма неприятном для человека месте, например,
вблизи от активной зоны реактора или ускорителя (понятно, что для таких
применений необходимо специальное исполнение микросхем, например на сапфировой
подложке). В этих случаях иногда выводят кнопку системного сброса в "чистые"
области многометровым кабелем. Но, скажем, для управляющего компьютера
космического аппарата такое решение просто нереализуемо. Бывают и ситуации,
когда выводить кнопку системного сброса наружу нежелательно по более банальным,
эргономическим и т. д., соображениям, например из-за опасности случайного
нажатия.
О чем-то аналогичном такому сервису часто мечтают администраторы серверов.
В Новосибирском FIDO однажды вполне серьезно обсуждалась такая схема автоматизированного
перезапуска: сервер каждые пять минут перепрограммирует источник бесперебойного
питания на то, чтобы он через десять минут от текущего момента выключился
и снова включился. В FIDO встречаются описания и более экстравагантных
решений, например "деглюкатор" (устройство, включаемое в шину
ISA и по запросу программы выполняющее сброс внешнего модема) или рычажный
механизм, посредством которого компьютер, выдвинув поднос CD-ROM, может
сам себе нажать на кнопку сброса (полезен в ситуациях, когда система еще
условно работоспособна, но с высокой вероятностью может "зависнуть"
при попытке нормальной перезагрузки).
Понятно, что все вышеперечисленные решения не могут заменить собой отладку
и исправление ошибок в прикладных и системных программах И являются скорее
последней линией обороны (если даже не действиями, предпринимаемыми от
отчаяния) в борьбе с системными сбоями. Но пока человечество не научится
писать программы без ошибок, возможность "привести в чувство"
обезумевшую программу путем ее перезапуска остается жизненно необходимой.
Понятно также, что если программа полностью сохраняет свое состояние в
постоянной памяти, ее перезапуск нам ничем не поможет: программа честно
восстановит весь тот мусор, который накопился в ее сегментах и файлах
данных за время предыдущей сессии, и честно воспроизведет снова тот сбой,
из-за которого и потребовался рестарт.
В этом смысле крайне желательно держать под контролем перенос данных из
постоянной памяти в оперативную, а особенно в обратном направлении избегая
его полной "прозрачности". Каждая дополнительная точка сохранения
состояния системы повышает риск воспроизведения сбоя или даже возникновения
новых проблем, порожденных в файлах сохранения состояния во время чрезмерно
"жесткого" перезапуска.
Реестр Win32
В свете этого, например, системный реестр Win32, не имеющий адекватных
средств восстановления и самоконтроля, представляет собой если и не сознательную
диверсию, то, во всяком случае, недостаточно продуманное техническое решение
— из-за него многие проблемы, для исправления которых в более продуманных
с этой точки зрения системах достаточно перезагрузки или очистки конфигурационного
файла, в Win32 приходится решать переустановкой ОС.
Да, Windows NT предоставляет некоторые средства резервного копирования
реестра — "восстановительную" дискету и "последнюю хорошую
(last known good) конфигурацию", но эти средства неудобны для повседневного
использования, а "последняя хорошая конфигурация" и просто неадекватна:
тот факт, что с данным содержимым реестра мы дошли до окошка с именем
и паролем, это, конечно, определенное достижение и повод этот реестр сохранить
— но ни в коем случае не повод затирать предыдущую (теперь уже предпоследнюю)
"хорошую" конфигурацию!
Если говорить именно о настройках ОС, радикальнее всего эта проблема решена
в современных версиях FreeBSD (свободно распространяемой системе семейства
Unix), в которой все файлы настройки ОС и системных сервисов включены
в систему контроля версий, обеспечивающую полный или частичный откат на
неограниченное число модификаций назад. Собственно, это может быть реализовано
в любой ОС, которая хранит свою конфигурацию в текстовом формате, стандартными
средствами контроля версий, используемыми для разработки программного
обеспечения, — CVS и др.
В свете вышеприведенных рассуждений, полезно разделять оперативные и
хранимые объекты не только по способу адресации, но и по представлению
данных. Эти представления должны удовлетворять различным и не всегда совместимым
требованиям: при выборе внутреннего, оперативного
представления данных основные критерии — это скорость, удобство
доступа и умопостижпмость кода, который будет с этими данными работать,
а для „нешнего, хранимого представления — прежде всего легкость проверки
и, если это возможно, восстановление целостности данных. При разработке
внешнего формата данных желательно также принять во внимание соображения
межмашинной совместимости — возможные различия в порядке байтов и даже
битов в целочисленных значениях, особенности представления чисел с плавающей
точкой, различия кодировки текста и так далее.
Если хранимое и оперативное представления объектов различны, одноуровневая
память для нас скорее вредна, чем бесполезна: прежде чем программа сможет
работать с объектом, она должна преобразовать его во внутреннее представление
(в объектно-ориентированных языках это может делать один из конструкторов
объекта). Эту процедуру обычно оказывается целесообразно совместить со
считыванием внешнего представления объекта из файла. "Прозрачная"
же для пользовательской программы запись данных во внешний формат бывает
и просто опасна — нередко для обеспечения целостности данных оказывается
необходим контроль над порядком записи тех или иных полей и структур.
Объединение оперативной и долговременной памяти, таким образом, оказывается
применимо лишь в тех ситуациях, когда нам, во-первых, удалось разработать
модель данных, одновременно удовлетворяющую требованиям, предъявляемым
и к оперативному, и к хранимому представлениям, и во-вторых, когда нас
не беспокоит опасность нарушения нашей модели данных из-за неполного их
сохранения в момент системного сбоя (или когда мы имеем какие-то средства
предотвращения этой опасности).
Безусловно, средства для отображения файлов в память лучше иметь, чем
не иметь. К тому же их можно использовать и для других целей, кроме собственно
организации одноуровневого доступа к данным — для загрузки программ, выделения
памяти или эмуляции сегментов данных, разделяемых между задачами и даже
между машинами (при доступе к файлу по сети).
Важно еще подчеркнуть, что разделение представлений данных на внешние
и внутренние не обязано полностью соответствовать способу их хранения
— в ОЗУ или на диске. Кроме хранения оперативных данных в своп-пространстве
и разного рода "виртуальных дисков" можно привести и более радикальный
пример: таблицы, индексы и прочие файлы данных сервера реляционной СУБД
представляют собой, скорее, оперативное представление данных, РОЛЬ же
хранимого представления в данном случае играют форматы, Используемые для
экспорта и резервного копирования содержимого таблиц.
Благодаря этому примеру становится понятнее, почему единственная из коммерчески
применяемых в настоящее время систем с одноуровневой адресацией — AS/400
— ориентирована на использование в качестве сервера СУБД. В литературе,
особенно в рекламной, даже встречается ее описание "аппаратного сервера
баз данных".
Примечание
Вообще, описание специализированных компьютеров как "аппаратное что-то
там"— нередко встречающийся, остроумный и довольно эффективный маркетинговый
прием. Понятно, что чем более короткий и однозначный ответ дает технический
специалист на вопросы "принимающих решения", тем легче ему будет
обосновать конкретный выбор. Поэтому наравне с грамотным и исчерпывающим
описанием технических достоинств, хорошее рекламное описание должно в
явном или неявном виде содержать и варианты ответов на многие распространенные
вопросы со стороны нетехнического персонала.
Так, если начальник спрашивает администратора: "Вот, купим мы этот
компьютер — моя секретарша сможет на нем в Lines играть?", тот может
ему ответить: "А это не компьютер, это аппаратный... " маршрутизатор
(Cisco), сервер СУБД (AS/400), веб-сервер (попытки продавать такие серверы
на основе Linux делались, но большого успеха не имели), нужное подставить.
|