\chapter{Управление оперативной памятью}

Основной ресурс системы, распределением которого занимается ОС -- это
оперативная память. Поэтому организация памяти оказывает большое
влияние на структуру и возможности ОС.
В настоящее время сложилась даже более интересная ситуация -- переносимая
операционная система \sysname{UNIX}, рассчитанная на машины со страничным
диспетчером памяти, произвела жесткий отбор, и теперь практически все
серьезные машины, начиная от \machname{i386} и заканчивая суперкомпьютерами
или, скажем процессором \machname{Alpha}, имеют именно такую организацию
памяти.

\section{Открытая память}

Самый простой случай управления памятью - ситуация, когда
диспетчер памяти отсутствует, и в системе может быть загружена только
одна программа. Именно в таком режиме работают \sysname{CP/M} и
\sysname{RT-11 SJ} (Single-Job, однозадачная).

В этих системах программы загружаются с фиксированного адреса
\verb"PROG_START". В \sysname{CP/M} это \verb"0x100";
в \sysname{RT-11} -- \verb"01000".
В адресах от 0 до начала программы находятся
вектора прерываний, а в \sysname{RT-11} -- также и стек программы.
Сама система размещается в старших адресах памяти.
Адрес \verb"SYS_START", с которого она начинается,
зависит от количества памяти у машины и от конфигурации самой ОС.

В этом случае управление памятью со стороны системы состоит
в том, что загрузчик проверяет, поместится ли загружаемый модуль в
пространство от \verb"PROG_START" до \verb"SYS_START".
Если объем памяти, который использует программа, не будет меняться
во время ее исполнения, то на этом все управление и заканчивается.

Однако программа может использовать динамическое управление
памятью, например функцию \verb"malloc()" или что-то в этом роде.
В этом случае уже код \verb"malloc()" должен следить за тем, чтобы
не залезть в системные адреса.
Как правило, динамическая память начинает размещаться с адреса
\verb"PROG_END = PROG_START + PROG_SIZE".
\verb"PROG_SIZE" в данном случае обозначает полный размер программы,
то есть размер ее кода, статических данных и области,
выделенной под стек.

Функция \verb"malloc()" поддерживает некоторую структуру
данных, следящую за тем, какие блоки памяти из уже выделенных были
освобождены. При каждом новом запросе она сначала ищет блок подходящего
размера в своей структуре данных и, только когда этот поиск завершится
неудачей, откусывает новый блок памяти у системы. Для этого используется
переменная, которая в библиотеке языка C называется \verb"brklevel".
Изначально эта переменная равна \verb"PROG_END", ее значение увеличивается
при выделении новых блоков, но в некоторых случаях может и уменьшаться.
Это происходит, когда программа освобождает блок, который заканчивается
на текущем значении \verb"brklevel".

\section[Динамическая память]{Алгоритмы динамического упpавления памятью}

Динамическое распределение памяти (его еще иногда называют
управлением \termin{кучей}{pool {\rm или} heap}) представляет собой
нетривиальную проблему. Действительно, активное использование функций
\verb"malloc/free" может привести к тому, что вся доступная память
будет разбита на блоки маленького размера, и попытка выделения
большого блока завершится неудачей,
даже если сумма длин маленьких блоков намного больше требуемой.
Это явление называется фрагментацией памяти.
Кроме того, большое количество блоков требует длительного поиска.
Существует также много мелких трудностей разного рода.
К счастью, человечество занимается проблемой распределения памяти
уже давно, и найдено много хороших или приемлемых решений.

В зависимости от решаемой задачи используются различные алгоритмы
поиска свободных блоков памяти. Действительно, программа может требовать
множество блоков одинакового размера, или нескольких фиксированных размеров.
Это сильно облегчает решение проблемы фрагментации и поиска.
Возможны ситуации, когда блоки освобождаются в порядке, обратном тому,
в котором они выделялись. Это позволяет свести выделение памяти к
стековой структуре.
Возможны ситуации, когда некоторые из занятых блоков можно переместить
по памяти. Так, например, функцию \verb"realloc()" в ранних реализациях
системы \sysname{UNIX} можно было использовать именно для этой цели.

В стандартных библиотеках языков высокого уровня, таких как
\verb"malloc/free/realloc" в C, \verb"new/dispose" в Pascal и т.д.,
как правило, используются алгоритмы, рассчитанные на худший случай:
программа требует блоки случайного размера в случайном порядке и
освобождает их также случайным образом.
Возможен, правда, более непpиятный случай:

{\samepage
\begin{example}\label{ugly_malloc}
\small\begin{verbatim}

   while(TRUE) {
        void * b1 = malloc(random(10));
        /* Случайный размер от 0 до 10 байт */
        void * b2 = malloc(random(10)+10);
        /*   ........      от 10 до 20 байт */

        if(b1 == NULL && b2 == NULL) /* Если памяти нет */
           break;                    /* выйти из цикла  */

        free(b1);
    }
    void * b3 = malloc(150);

    /* Скорее всего, память не будет выделена */
\end{verbatim}
\end{example}
}

В результате исполнения такой программы вся доступная память будет
<<порезана на лапшу>>: между любыми двумя свободными блоками будет размещен
занятый блок меньшего размера.
К счастью, пример \ref{ugly_malloc} носит искусственный характер.
В обычных программах такая ситуация встречается редко, и часто
оказывается проще исправить программу, чем вносить изменения в
универсальный алгоритм управления кучей.

Варианты алгоритмов распределения памяти исследовались еще в 50-е годы.
Итоги многолетнего изучения этой проблемы приведены в \cite{Knuth}
и многих других учебниках.

Возможны алгоритмы распределения памяти двух типов: когда размер
блока является характеристикой самого блока, и когда его сообщают отдельно
при освобождении. К первому типу относится \verb"malloc/free", ко второму --
\verb"GetMem/FreeMem" в Turbo Pascal. В первом случае с каждым блоком
ассоциируется некоторый дескриптор, который содержит длину этого блока и,
возможно, какую-то еще информацию.
Этот дескриптор может храниться отдельно от блока, или быть его заголовком.
Иногда такой дескриптор <<окружает>> блок, то есть состоит из двух меток --
в начале блока и в его конце.
Для чего это может быть полезно, будет обсуждаться ниже.

Обычно все свободные блоки памяти объединяются в двунаправленный
связанный список. Список должен быть двунаправленным для того, чтобы из него
в любой момент можно было извлечь любой блок. Впpочем, если все действия по
извлечению блока пpоизводятся после поиска, то можно слегка усложнить
пpоцедуpу поиска и всегда сохpанять указатель на пpедыдущий блок. Это
pешает пpоблему извлечения и можно огpаничиться однонапpавленным списком.
Беда только в том, что многие алгоpитмы пpи объединении свободных блоков
извлекают их из списка в соответствии с адpесом, поэтому для таких алгоpитмов
двунапpавленный список остpо необходим.

При первом взгляде на проблему возникает желание отсортировать
список по размеру блока. На самом деле это бессмысленно:
время поиска в сортированном списке улучшается всего в два раза по
сравнению с несортированным (вот в массиве или в дереве -- совсем другое дело),
зато добавляется время вставки в список, пропорциональное $O(n)$, где $n$ --
размер списка. Помещать блоки в сортированный массив еще хуже --
время вставки становится $O(n + \log(n))$ и появляется ограничение на
количество блоков.
Использование хэш-таблиц или двоичных деревьев требует больших
накладных расходов и усложнений программы, которые себя в итоге
не оправдывают. Поэтому используют несортированный список.

Поиск в списке может вестись двумя способами:
до нахождения \termin{первого подходящего}{first fit} блока
или до блока, размер которого ближе всего к заданному --
\termin{наиболее подходящего}{best fit}.
Для нахождения наиболее подходящего мы обязаны просматривать весь список,
в то время как первый подходящий может оказаться в любом месте,
и среднее время поиска будет меньше.
Насколько меньше --  зависит от отношения количества подходящих
блоков к общему количеству. (Читатели, знакомые с теорией вероятности,
могут самостоятельно вычислить эту зависимость).

Кроме того, в общем случае best fit увеличивает фрагментацию памяти.
Действительно, если мы нашли блок с размером больше
заданного, мы должны отделить <<хвост>> и пометить его как новый
свободный блок. Понятно, что в случае best fit средний размер этого хвоста
будет маленьким, и мы в итоге получим большое количество мелких блоков,
которые невозможно объединить, так как пространство между ними занято.

При использовании first fit с линейным двунаправленным списком
возникает специфическая проблема. Если каждый раз просматривать список
с одного и того же места, то большие блоки, расположенные ближе к началу,
будут чаще удаляться. Соответственно, мелкие блоки будут иметь тенденцию
скапливаться в начале списка, что увеличит среднее время поиска.
Простой способ борьбы с этим явлением состоит в том, чтобы просматривать
список то в одном направлении, то в другом.
Более радикальный и еще более простой метод состоит в том,
что список делается кольцевым, и поиск каждый начинается с того места,
где мы остановились в прошлый раз.
В это же место добавляются освободившиеся блоки.
В результате список очень эффективно перемешивается и никакой
<<антисортировки>> не возникает.

В ситуациях, когда мы размещаем блоки нескольких
фиксированных размеров, алгоритмы best fit оказываются лучше. Однако
библиотеки распределения памяти рассчитывают на худший случай, и в них
обычно используются алгоритмы first fit.

В случае работы с блоками нескольких фиксированных размеров
напрашивается такое решение: создать для каждого типоразмера свой список.
Это избавляет программиста от необходимости выбирать между first и best fit,
устраняет поиск в списках как явление\ellipsis короче, решает
сразу много проблем.

Интересный вариант этого подхода для случая, когда различные размеры
являются степенями числа 2, как 512 байт, 1Кбайт, 2Кбайта и т.д.,
называется \term{алгоритмом близнецов}.
Он состоит в том, что мы ищем блок требуемого размера в соответствующем списке.
Если этот список пуст, мы берем список блоков вдвое большего размера.
Получив блок вдвое большего размера, мы делим его пополам.
Ненужную половину мы помещаем в соответствующий список свободных блоков.
Любопытно, что нам совершенно неважно, получили ли мы этот блок просто из
соответствующего списка, или же делением пополам вчетверо большего блока,
и далее по рекурсии. Одно из преимуществ этого метода состоит в простоте
объединения блоков при их освобождении. Действительно, адрес блока-близнеца
получается простым инвертированием соответствующего бита в адресе нашего
блока. Нужно только проверить, свободен ли этот близнец. Если он свободен,
то мы объединяем братьев в блок вдвое большего размера, и т.д.

Алгоритм близнецов значительно снижает фрагментацию памяти и резко ускоряет
поиск блоков. Наиболее важным преимуществом этого подхода является то,
что даже в наихудшем случае время поиска не превышает
$O(log(S_{max}) - log(S_{min}))$, где $S_{max}$ и $S_{min}$ обозначают
соответственно максимальный и минимальный размеры используемых блоков.
Это делает алгоритм близнецов труднозаменимым для ситуаций, когда
необходимо гарантированное время реакции -- например, для задач реального
времени. Часто этот алгоритм или его варианты используются для выделения
памяти внутри ядра ОС. Например, функция \verb"kmalloc", используемая
в ядре ОС \sysname{Linux}, основана именно на алгоритме близнецов.

Разработчик программы динамического распределения памяти обязан
решить еще одну важную проблему, а именно -- объединение свободных блоков.
Действительно, обидно, если мы имеем сто свободных блоков по одному
килобайту и не можем сделать из них один блок в сто килобайт.
Но если все эти блоки расположены в памяти один за другим, а мы не
можем их при этом объединить -- это просто унизительно. Кроме того, если
мы умеем объединять блоки и видим, что объединенный блок ограничен сверху
значением \verb"brklevel", то мы можем вместо помещения этого блока в список
просто уменьшить значение \verb"brklevel" и, таким образом, вернуть ненужную
память системе.

Рассмотрим способы решения этой проблемы. Один способ упоминался
в описании алгоритма близнецов, но он пригоден только в особых случаях.

Представим себе для начала, что все, что мы знаем о блоке, -- это его
начальный адрес и размер. Такая ситуация возможна при выделении памяти
<<второго рода>>, т.е. когда размер освобождаемого блока передается как
параметр процедуры \verb"FreeMem".
Кроме того, такое возможно и в случаях, когда дескриптор блока содержит
только его длину.

Легко понять, что это очень плохая ситуация. Действительно, для
объединения блока с соседями мы должны найти их в списке свободных, или
же убедиться, что там их нет. Для этого мы должны просмотреть весь
список. В порядке мозгового штурма можно высказать идею сортировать список
свободных блоков по адресу\ellipsis

Гораздо проще запоминать в дескрипторе блока указатели на
дескрипторы соседних блоков. Немного развив эту идею, мы приходим к методу,
который упоминался в начале раздела. Этот метод называется алгоритмом парных
меток и состоит в том, что мы добавляем к каждому блоку по два слова памяти.

\note{Именно сл\'{о}ва, а не байта. Дело в том, что требуется добавить
достаточно места, чтобы хранить там размер блока в байтах или словах.
Обычно такое число занимает столько же места, сколько и адрес, а размер
слова обычно равен размеру адреса. На \machname{x86} в реальном режиме
это не так, но это вообще довольно странный процессор.}

Итак, мы добавляем к блоку два слова -- одно спереди,
другое сзади. В оба слова мы записываем размер блока. Это и есть
тот дескриптор, который окружает блок. При этом мы говорим, что значения
длины будут положительными, если блок свободен, и отрицательными, если
блок занят. Можно сказать и наоборот, важно только потом соблюдать это
соглашение.

Представим, что мы освобождаем блок с адресом \verb"addr".
Считаем, что addr имеет тип \verb"word *", и при добавлении к нему
целых чисел результирующий адрес будет отсчитываться в словах, как в языке C.
Для того чтобы проверить, свободен ли его сосед спереди, мы должны
посмотреть слово с адресом \verb"addr - 2". Если оно отрицательно, то сосед
занят, и мы должны оставить его в покое. Если же оно положительно, то
мы можем легко определить адрес начала этого блока как \verb"addr - addr[-2]".
Определив адрес начала блока, мы можем легко объединить этот блок с блоком
\verb"addr", нам нужно только сложить значения меток-дескрипторов
и записать их в дескрипторы нового большого блока.
Нам даже не нужно будет добавлять
освобождаемый блок в список и извлекать оттуда его соседа!

Похожим образом присоединяется и сосед сзади. Единственное отличие
состоит в том, что этого соседа все-таки нужно извлекать из списка свободных
блоков.

Дополнительное преимущество приведенного алгоритма состоит в том, что
мы можем отлавливать такие ошибки, как многократное освобождение одного блока,
запись в память за границей блока и иногда даже обращение к уже
освобожденному блоку.
Действительно, мы в любой момент можем проверить всю цепочку блоков
памяти и убедиться в том, что {\it все} свободные блоки стоят в списке,
что в нем стоят {\it только} свободные блоки, что сами цепочка и список
не испорчены, и т.д.

\note{
Это действительно большое преимущество, так как оно значительно
облегчает ловлю ошибок работы с указателями, о которых в руководстве по
\progname{Zortech C/C++} сказано, что <<опытные программисты, услышав это
слово [<<pointer bug>> -- прим. авт.], бледнеют и прячутся под стол>>
(\cite{ZTCman}).
}

Итак, наилучшим из известных универсальных алгоритмов
динамического распределения памяти является алгоритм парных меток
с объединением свободных блоков в двунаправленный кольцевой список
и поиском по принципу first fit. Этот алгоритм обеспечивает приемлемую
производительность почти для всех стратегий распределения памяти,
используемых в прикладных программах.
Такой алгоритм используется практически во всех реализациях стандартной
библиотеки языка C и во многих других ситуациях. Другие известные
алгоритмы либо просто хуже, чем этот, либо проявляют свои преимущества
только в специальных случаях.

К основным недостаткам этого алгоритма относится отсутствие верхней границы
времени поиска подходящего блока, что делает его неприемлемым для задач
реального времени.

Некоторые системы программирования используют специальный метод
освобождения динамической памяти, называемый \term{сборкой мусора}.
Этот метод состоит в том, что ненужные блоки памяти не освобождаются явным
образом. Вместо этого используется некоторый более или менее изощренный
алгоритм, следящий за тем, какие блоки еще нужны, а какие -- уже нет.

Самый простой метод отличать используемые блоки от ненужных -- считать,
что блок, на который есть ссылка, нужен, а блок, на который ни одной
ссылки не осталось -- не нужен. Для этого к каждому блоку присоединяют
дескриптор, в котором подсчитывают количество ссылок на него. Каждая
передача указателя на этот блок приводит к увеличению счетчика ссылок на 1,
а каждое уничтожение объекта, содержавшего указатель -- к уменьшению.

Впpочем, пpи таком подходе возникает специфическая проблема. Если у нас
есть циклический список, на который нет ни одной ссылки извне, то все объекты
в нем будут считаться используемыми, хотя они и являются мусором.
Если мы по тем или иным причинам уверены, что кольца не возникают или
возникают очень редко, метод подсчета ссылок вполне приемлем; если же мы
используем графы произвольного вида, необходим более умный алгоритм.

Все остальные методы сборки мусора так или иначе сводятся к поддержанию
базы данных о том, какие объекты на кого ссылаются.
Использование такой техники возможно практически только в
интерпретируемых языках типа \progname{Lisp} или \progname{Prolog},
где с каждой операцией можно ассоциировать неограниченно большое
количество действий.

\section{Открытая память (продолжение)}\label{OpenMemory}

Описанные выше алгоритмы распределения памяти используются
не операционной системой, а библиотечными функциями, пришитыми к
программе.
Однако многозадачная или многопрограммная%
\footnote{\sysname{MS~DOS} -- типичный пример однозадачной многопрограммной ОС.
Действительно, в ней может быть одновременно загружено несколько программ,
но система выполняет только одно задание.}
ОС также должны использовать тот или иной алгоритм размещения памяти.

Отчасти такие алгоритмы могут быть похожи на работу malloc.
Однако режим работы ОС может вносить существенные упрощения в алгоритм.

{\small
Так, например, пpоцедуpа управления памятью \sysname{MS~DOS} рассчитана
на случай, когда программы выгружаются из памяти только в порядке,
обратном тому, в каком они туда загружались\footnote{Hа самом деле,
программы могут выгружаться и в другом порядке, но обычно это приводит
к проблемам.}. Это позволяет свести управление памятью к стековой дисциплине.

Каждой программе в \sysname{MS~DOS} отводится блок памяти. С
каждым таким блоком ассоциирован дескриптор, называемый \verb"MCB" --
Memory Control Block. Этот дескриптор содержит размер блока, идентификатор
программы, которой принадлежит этот блок и признак того,
является ли данный блок последним в цепочке. Нужно отметить, что программе
всегда принадлежит {\it несколько} блоков, но это уже несущественные детали.
Другая малосущественная деталь та, что размер сегментов и их адреса
отсчитываются в параграфах размером 16 байт. Знакомые с архитектурой
процессоров \machname{80x86} должны понять, что адрес \verb"MCB" в этом
случае будет состоять только из сегментной части с нулевым смещением.

\note{
Идентификатор программы в \sysname{MS~DOS} может служить темой для отдельного
разговора. Во многих документах он носит громкое название \verb"pid"
(идентификатор процесса) -- почти как в \sysname{UNIX}, но на самом деле
это всего лишь адрес \verb"PSP" (Program Segment Prefix).
}

После запуска \verb".com"-файл получает сегмент размером 64К,
а \verb".exe" -- всю доступную память. Обычно \verb".exe"-модули
сразу после запуска освобождают ненужную им память и устанавливают
\verb"brklevel" на конец своего сегмента, а потом увеличивают
\verb"brklevel" и наращивают сегмент по мере необходимости.
Естественно, что наращивать сегмент можно только за счет следующего
за ним в цепочке \verb"MCB", и \sysname{MS~DOS} разрешит делать это только
в случае, если этот сегмент не принадлежит никакой программе.

При запуске программы \sysname{DOS} берет {\it последний}
сегмент в цепочке, и загружает туда программу, если этот сегмент
достаточно велик. Если он недостаточно велик, \sysname{DOS} говорит
\verb"Not enough memory" и отказывается загружать программу.

При завершении программы \sysname{DOS} освобождает
все блоки, принадлежавшие программе. При этом соседние блоки объединяются.
Пока программы, действительно, завершаются в порядке, обратном тому,
в котором они запускались, -- все вполне нормально.
Другое дело, что в реальной жизни возможны отклонения от этой схемы.

Например, неявно предполагается, что \verb"TSR"-программы (Terminate,
but Stay Resident) никогда не пытаются завершиться. Тем не менее
любой уважающий себя хакер считает своим долгом сделать резидентную программу
выгружаемой. У некоторых хакеров она, в результате, выбрасывает при выгрузке
все резиденты, которые сели в память после них.
Другой пример -- отладчики обычно загружают программу в обход обычной
DOS-овской функции \verb"LOAD & EXECUTE", а при завершении отлаживаемой
программы сами освобождают память из-под нее.

Один из авторов в свое время занимался прохождением некоторой
программы под отладчиком. Честно говоря, речь шла о взломе некоторой
игрушки\ellipsis Эта программа производила какую-то инициализацию, а потом
вызывала функцию \sysname{DOS} \verb"LOAD & EXECUTE". Я об этом не знал
и, естественно, провалился внутрь новой программы, которую и должен был,
по-хорошему, взламывать. После нескольких нажатий \verb"<CTRL> <Break>" я
наконец-то попал в отладчик, но при каком-то очень странном состоянии
программы. Полазив по программе некоторое время и убедившись, что она
не хочет приходить в нормальное состояние, я вышел из отладчика и увидел
следующую картину:
системе доступно около 100Кбайт в то время, как сумма длин свободных блоков
памяти более 300Кбайт, а размер наибольшего свободного блока около 200Кбайт.
Отладчик, выходя, освободил свою память и память отлаживаемой программы,
но не освободил память из-под нового загруженного модуля. В результате
посредине памяти остался никому не нужный блок памяти изрядного размера,
помеченный как используемый.
Самым обидным было то, что \sysname{DOS} не пыталась загрузить ни одну
программу в память {\it под} этим блоком, хотя там было гораздо больше места,
чем {\it над} ним.
}

В качестве итога можно сказать, что основными проблемами
многопрограммных систем без диспетчера памяти являются:
\begin{itemize}
\item Проблема выделения дополнительной памяти программе, которая загружалась
не последней.
\item Проблема освобождения памяти после завершения программы.
В системах с монолитным загружаемым модулем иногда просто запрещают
программам выгружаться. В \sysname{MS~DOS} была сделана
попытка запретить выгружать \verb"TSR" и драйверы, но это привело к поиску
задних дверей.
\item Низкая надежность. Ошибка в одной из программ может привести
к порче кода или данных других программ или самой системы.
\item Проблемы безопасности.
В системах с открытой памятью невозможны эффективные средства
разделения доступа. Любая программная проверка прав доступа
может быть легко обойдена прямым вызовом <<защищаемых>> модулей ядра.
Даже криптографические средства не обеспечивают достаточно эффективной
защиты, потому что можно посадить в память троянскую программу, которая
будет анализировать код программы шифрования и считывать значение ключа...
\end{itemize}

В системах с динамической сборкой первые две
проблемы не так остры, потому что память выделяется и освобождается
небольшими кусочками, по блоку на каждый объектный модуль, поэтому код
программы обычно не занимает непрерывного пространства. Соответственно,
такие системы часто разрешают и данным программы занимать несмежные
области памяти.

Такой подход используется многими системами с открытой памятью --
\sysname{AmigaDOS}, \progname{Oberon}, системами программирования
для транспьютера и т.д. Однако в таких системах очень острую форму
приобретает проблема фрагментации свободной памяти.

Количество фрагментов приблизительно пропорционально количеству операций
выделения/освобождения памяти. Вероятность не найти блок подходящего
размера из-за фрагментации оценить сложнее, но она также растет с
количеством операций.

Понятно, что если эти операции осуществляются
сразу многими программами, то фрагментация пропорционально возрастает.
Вышеперечисленные системы не предоставляют никаких средств для борьбы с
фрагментацией.

Напротив, в системе \sysname{MacOS} было предложено достаточно оригинальный
метод борьбы с фрагментацией. Метод этот заслуживает отдельного обсуждения.

\subsection{Управление памятью в MacOS и MS~Windows}\label{MemoryHandles}

В этих системах предполагается, что пользовательские программы не
сохраняют указателей на динамически выделенные блоки памяти.
Вместо этого каждый такой блок идентифицируется целочисленным дескриптором
или <<ручкой>> (\term{handle}). Когда программа непосредственно обращается
к данным в блоке, она выполняет системный вызов \verb"GlobalLock" (запереть).
Этот вызов возвращает текущий адрес блока. Пока программа не исполнит
вызов \verb"GlobalUnlock" (отпереть), система не пытается изменить адрес
блока.
Напротив, если блок не заперт, система считает себя вправе передвигать
его по памяти или даже сбрасывать на диск.

Таким образом, <<ручки>> представляют собой попытку создать программный
аналог аппаратных диспетчеров памяти. Они позволяют решить проблему
фрагментации и даже организовать некое подобие виртуальной памяти.
Можно рассматривать их как средство организации оверлейных данных --
поочередного отображения разных блоков данных на одни и те же адреса.
Однако за это приходится платить очень дорогой ценой.

Использование <<ручек>> сильно усложняет программирование
вообще и в особенности перенос ПО из систем, использующих линейное
адресное пространство. Все указатели на динамические структуры данных
в программе нужно заменить на <<ручки>>, а каждое обращение к таким
структурам необходимо окружить вызовами \verb"GlobalLock/GlobalUnlock".
Эти вызовы:
\begin{itemize}
\item сами по себе увеличивают объем кода и время исполнения;
\item мешают компиляторам выполнять оптимизацию, прежде всего не
позволяют оптимально использовать регистры процессора, потому что
далеко не все регистры сохраняются при вызовах;
\item требуют разрыва конвейера команд и перезагрузки командного кэша;
в современных суперскалярных процессорах это может приводить к падению
производительности во много раз.
\end{itemize}

Попытки уменьшить число блокировок требуют определенных интеллектуальных
усилий. Фактически, к обычному циклу разработки ПО: проектирование, выбор
алгоритма, написание кода и его отладка -- добавляется еще две фазы:
микрооптимизация использования <<ручек>> и отладка оптимизированного кода.
Последняя фаза оказывается, пожалуй, самой сложной и ответственной.

Наиболее неприятной и опасной ошибкой, возникающей на фазе микрооптимизации,
является вынос указателя на динамическую структуру за пределы скобок
\verb"GlobalLock/GlobalUnlock". Эту ошибку очень сложно обнаружить при
тестировании, так как она проявляется только если система пыталась
передвигать блоки в промежутках между обращениями. Иными словами, ошибка
может проявлять или не проявлять себя в зависимости от набора приложений,
исполняющихся в системе и от характера деятельности этих приложений.
В результате мы получаем то, чего больше всего боятся инженеры, механики
и программисты -- систему, которая работает {\it иногда}.

Не случайно фирма \entname{MicroSoft} полностью отказалась от <<ручечного>>
управления памятью в новой версии \sysname{MS~Windows} --
\sysname{Windows~95}, где реализована полноценная виртуальная память.
