Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Перевод статьи Рэймонда Чена, более 25 лет участвовавшего в разработке ОС Windows, автора блога The Old New Thing, начатого им в 2003 году.
С выходом Windows 95 появилось большое количество программных продуктов, предназначенных специально для этой ОС. Много внимания в какой-то момент привлекла одна из таких программ – SoftRAM 95. На коробке значилось, что программа может «удвоить вашу память».
Оказалось, что не может.
Я встречал несколько статей, описывавших, что эта программа делала (а главное – не делала), но почти нигде авторы не углублялись в код и не описывали, как именно она работала. Поэтому я решил написать такую статью.
В конце 1995 года меня попросили разобраться с этим продуктом – он приводил к падению Windows 95, из-за него в поддержку поступало множество звонков, не говоря уже о том, что он делал ОС антирекламу. Была вероятность того, что программа каким-то образом взаимодействовала с неизвестной ошибкой в Windows 95, и нам нужно было найти её и пропатчить.
В итоге мне пришлось дизассемблировать их собственный драйвер подкачки страниц. В набор средств разработки драйверов (DDK) Windows 3.0 и 3.1 входил исходный код драйвера подкачки, и их драйвер был, очевидно, основан на коде, поставлявшемся в одном из старых DDK.
Они размещали кусок невыгружаемой памяти [non-paged memory] при старте системы и использовали его в качестве пула сжатия. При выгрузке участков памяти драйвер сжимал память и добавлял её к пулу. Каждый блок буфера сжатия начинался с байта, обозначавшего алгоритм сжатия, за которым следовали сжатые данные.
Когда система запрашивала определённую страницу памяти, драйвер проверял, нет ли её сжатой копии в пуле. Если она там была, он разжимал её и возвращал. В ином случае он считывал память с диска, только довольно странным методом.
Для ясности я нарисовал картинки. Будем ранжировать страницы системы по вероятности того, что к ним обратятся. Левее будут горячие страницы, к которым будут обращаться вероятнее всего. Правее – холодные, к которым обратятся с меньшей вероятностью. Система без изменений выглядит так:
В данном примере воображаемой системы в памяти имеется 16 самых горячих страниц, а остальные хранятся на диске. Размер секции «в памяти» равен объёму установленного ОЗУ. Размер секции «на диске» может расти до максимального размера файла подкачки.
При размещении новой страницы она вставляется слева как наиболее горячая. Поскольку размер секции «в памяти» ограничен, самые холодные из горячих страниц выталкиваются на диск. В среднем почти все страницы в системе холодные, поэтому масштаб приведённой выше диаграммы не соблюдён. Размер секции «на диске» гораздо больше, чем «в памяти», однако поскольку всё самое интересное происходит в секции «в памяти», я нарисовал её побольше.
Идея состоит в том, чтобы сжать часть хранящихся в памяти страниц:
Берём половину памяти системы и сжимаем. На диаграмме размер сжатых страниц меньше, чем несжатых. Это подчёркивает, что общее количество памяти не менялось. Поменялся лишь метод использования этой памяти.
Могу перерисовать диаграмму так, чтобы размеры всех страниц сохранились. С точки зрения приложений и менеджера памяти система теперь выглядит так:
С точки зрения остальной системы всё выглядит так, будто у вас появилось больше памяти.
К сжатой памяти нельзя обратиться сразу же, по необходимости, как это делается с обычной памятью. Когда приложение запрашивает доступ к памяти, которая оказывается сжатой, сжатые данные распаковываются в обычную страницу памяти, а страница с низшим рангом становится сжатой. На все эти сжатия и распаковки уходит время, но это всё равно получается быстрее, чем работать с диском.
Если я правильно помню, сжатые данные обрабатывались просто – через кольцевой буфер. После сжатия новая страница добавлялась к буферу, а при необходимости освободить место выбиралась страница в голове буфера. Таким способом очень просто следить за происходящим, однако эффективность при этом страдает. Когда страницу переносили из буфера со сжатием в память, а потом вносили обратно в буфер, память из-под старых сжатых данных просто тратила место. Теоретически можно было бы сразу же использовать освобождавшееся место, однако это усложнило бы алгоритмы работы с памятью из-за фрагментации. Тогда ещё все драйверы писали на ассемблере. Простота схемы была важна, поскольку реализовывать её приходилось при помощи низкоуровневых инструкций.
Расчёт шёл на то, что даже если вы убирали из памяти кучку быстрых страниц, вы меняли их на более количество страниц средней скорости, что уменьшало количество обращений к медленным страницам. В примере выше мы предполагаем сжатие в два раза, поэтому мы убираем 8 быстрых страниц и превращаем их в 16 страниц со средней скоростью доступа.
До | После | |
---|---|---|
Обычных страниц | 16 | 8 |
Сжатых страниц | 0 | 16 |
Страниц на диске | N | N-8 |
Получится ли выиграть время – зависит от закономерностей доступа к памяти у конкретных приложений. Если у вашего приложения 6 очень горячих страниц и ещё 14 тёплых, то прирост к скорости будет – очень горячие страницы могут оставаться в нормальной памяти, а 14 тёплых будут меняться местами, занимая два последних слота среди нормальных страниц. С другой стороны, если у приложения 10 очень горячих страниц и 10 тёплых, получится проигрыш, поскольку нормальных страниц в памяти для удержания всех горячих страниц не хватает, и вы постоянно будете сжимать и разжимать память, и всё это лишнее потраченное время может нивелировать время, выигранное на устранении обращений к диску, к тем страницам, что не помещались в память раньше.
Видно, что вся схема зависит от качества движка сжатия. Необходимо втискивать в сжатые страницы побольше данных, чтобы восполнить потерю доступа к нормальным страницам большим количеством сжатых страниц.
Я нашёл этот алгоритм сжатия. Он вызывался для того, чтобы извлечь память из буфера ввода/вывода и сжать её, поместив в буфер сжатия. Его напарник, алгоритм распаковки, использовался при запросах к страницам, распаковывал их из буфера сжатия и помещал в буфер ввода/вывода.
Разработчики реализовали только один алгоритм сжатия. И это был memcpy.
Иначе говоря, их хвалёный запатентованный алгоритм сжатия состоял в том, чтобы копировать данные без сжатия.
Архитектура для сжатия была реализована, а вставленная в качестве заглушки функция ничего не сжимала. Видимо, они решили, что в будущем вставят туда крутую функцию сжатия, а пока только протестируют всю схему. Однако времени на это у них не осталось, и они выпустили продукт с заглушкой.
В результате получилось что-то виртуального диска в памяти, где хранится файл подкачки. Некоторые страницы просто изымались из списка «быстрых» и превращались в страницы в списке «средней скорости».
Пока что получалось что-то вроде плацебо с небольшим падением эффективности. Но почему всё это в итоге падало? Наткнулась ли их программа на ошибку в менеджере памяти Windows 95?
Добавив код для реализации буфера сжатия, они не использовали ни критические секции, ни любые другие примитивы синхронизации для защиты структур данных. Если два потока начинали подкачку одновременно, драйвер портил структуры данных. В следующий раз, когда драйвер распаковывал страницу, он ошибался, и выдавал не ту страницу, которая была нужна.
Они, по сути, случайно симулировали сломанный жёсткий диск.
Также это объясняло, почему падения происходили, когда в системе активно шла подкачка страниц. В таких условиях вероятность наложения двух запросов к памяти повышалась.
Я упоминал, что их драйвер был основан на том, что шёл в DDK. Они даже не поменяли имя в блоке описания драйвера. Поэтому когда он падал, выдавалась ошибка PAGEFILE. А поскольку это название драйвера подкачки по умолчанию, всё выглядело так, будто упал штатный драйвер Windows, хотя на самом деле падала их замена с таким же названием.
Я, кстати, не единственный, кто разбирался в работе SoftRAM. Некий товарищ по имени Марк Руссинович тоже разобрал эту программу и пришёл к тем же выводам. Интересно, что с ним стало. Человек-то вроде умный.
Мой коллега отметил, что SoftRAM стала продолжением похожего продукта, разработанного для Windows 3.1. В этой ОС использовалось два способа увеличения количества программ, которые можно запускать одновременно. Во-первых, увеличивался размер файла подкачки – но это, кстати, можно было сделать и вручную. Во-вторых, там использовались определённые трюки, позволявшие не хранить определённые компоненты в обычной памяти. Эти трюки были известны инструментам для оптимизации системы. У Microsoft можно было даже бесплатно скачать программу, занимавшуюся тем же самым – её как раз написал этот мой коллега. В Windows 95 использовался динамический файл подкачки, и он также решал проблему с обычной памятью, из-за чего программе, портированной с Windows 3.1 на Windows 95, заниматься было уже нечем.
Говорят, кстати, что у той компании было два разработчика, один из которых разрабатывал драйвер, а другой – пользовательский интерфейс. Видимо, им не хватило третьего специалиста, который реализовал бы алгоритм сжатия.
Мой тогдашний начальник, кстати, как-то ездил на торговую выставку, на которой, в том числе, присутствовал и производитель программы SoftRAM. По окончанию выставки он обменялся футболками с представителем той компании, а потом отдал футболку мне.
Как же эта программа заработала стикер «Разработано для Windows 95?» Просто: она была разработана для Windows 95. Для этого нужно было просто следовать определённым правилам – типа использования стандартного диалога для открытия файла или установки в директорию Program Files. Всем требованиям программа соответствовала, поэтому ей выдали такую наклейку.
Некоторые специалисты говорят, что она не выполнила правило, по которому реальная работа программы должна в значительной степени соответствовать её рекламному описанию, однако никто не требовал, чтобы каждое заявление было проверено независимыми специалистами путём глубокого реверс-инжиниринга.
Если бы такой стандарт существовал, на утверждение каждого продукта уходили бы годы. Чтобы проверить программу «выучи немецкий за 30 дней», нужно было бы проверить, что в ней используется правильный словарь и правильная грамматика, а потом посадить бы кого-нибудь за неё на 30 дней, и посмотреть, выучил ли он немецкий. Правило предназначалось для того, чтобы отсекать программы, не соответствовавшие минимальным системным требованиям, или программы, которые заявлялись, как текстовый процессор, а на деле играли бы в крестики-нолики.