В принципе, программы, работающие в MS-DOS, могут получить доступ к памяти за пределами 1 Мб, но для этого требуется специальный драйвер расширенной памяти.
Поскольку делить имеющуюся память между несколькими процессами не приходится, распределение получается бесхитростное. Основные области памяти показаны на рис. 5‑4.
Рис. 5‑4
Нижнюю часть памяти занимают модули ОС: обработчики прерываний, резидентная чисть интерпретатора команд, драйверы устройств. Некоторые системные программы могут быть ради экономии загружены в верхний блок памяти (выше 640 Кб). Все, что остается в середине, может быть предоставлено процессу пользователя.
Для пущей экономии памяти некоторые нерезидентные модули DOS могут занимать верхнюю часть области пользователя, но только до тех пор, пока не будут затерты пользовательской программой, которой потребуется вся имеющаяся память.
Часть системной памяти и вся область пользователя разбита на прилегающие друг к другу блоки, размер которых кратен параграфу. Перед началом каждого блока памяти размещается блок управления памятью (MCB, MemoryControlBlock), который занимает один параграф и содержит следующие данные:
· признак, определяющий, последний ли это блок памяти или за ним будут еще блоки (соответственно буква ‘Z’ или ‘M’, это, видимо, опять Марк Збиковский отметился);
· адрес PSP программы, владеющей этим блоком (0 означает свободный блок);
· размер блока в параграфах;
· имя программы-владельца (до 8 символов); это поле избыточно (зная PSP программы, можно найти имя ее файла), оно было добавлено, вероятно, чтобы хоть как-то занять пустующие байты параграфа MCB.
Когда система должна выделить блок памяти для собственных нужд или по запросу программы пользователя, она просматривает список блоков от начала, перемещаясь от одного MCB к следующему. Найдя свободный блок достаточного размера, система отмечает его как занятый соответствующим владельцем. Если выделяется не весь свободный блок, то после выделенного блока система записывает еще один MCB, описывающий свободный остаток блока.
При освобождении блока система записывает 0 в поле владельца MCB. Если с одной или с двух сторон от освобождаемого блока лежат свободные блоки, то два или три свободных блока сливаются в один.
При запуске программы система выделяет ей два блока памяти: сначала небольшой блок для переменных среды, затем самый большой среди оставшихся свободных блоков для самой программы (блок PSP, см. п. 4.4.3). Обычно этот блок занимает всю свободную память. Такое решение приемлемо, поскольку других претендентов на память нет.
Почему блок среды выделяется раньше, чем блок PSP?
При завершении программы система просматривает все блоки памяти и освобождает те из них, владельцем которых указана завершаемая программа. Исключением является случай завершения с установкой резидента (п. 4.4.4), при этом блок PSP не освобождается, но уменьшается до указанного размера. В дальнейшем этот блок остается занятым до перезагрузки системы.
MS-DOS предоставляет в распоряжение пользователя функции, позволяющие выполнять основные действия с блоками памяти.
· Выделение блока указанного размера. Если свободного блока достаточной величины не имеется, то система возвращает максимальный размер, который может быть выделен.
· Освобождение ранее выделенного блока.
· Изменение размера блока. Уменьшение блока возможно всегда, увеличение – только в том случае, если после данного блока расположен свободный блок достаточного размера.
Одним из немногих случаев, когда эти функции оказываются полезны, является запуск порожденного процесса. Система должна иметь достаточно свободного места, чтобы разместить блок среды и блок PSP загружаемой программы. Однако, как было сказано выше, вся свободная память обычно отдается под блок PSP текущей программы. Поэтому прежде чем запускать порожденный процесс, программа должна уменьшить свой собственный блок PSP, оставив себе необходимый минимум.
5.8.1. Структура адресного пространства
Принято считать, что каждый процесс, запущенный в Windows, получает в свое распоряжение виртуальное адресное пространство размером 4 Гб. Это число определяется разрядностью адресов в командах: 232 байт = 4 Гб.
Конечно, трудно рассчитывать, что для каждого процесса найдется такое количество физической памяти, речь идет только о диапазоне возможных адресов.
Но даже и в этом смысле процессу доступно лишь около 2 Гб младших адресов виртуальной памяти. В частности, для WindowsNT старшие 2 Гб с адресами от 8000000016 до FFFFFFFF16 доступны только системе. Такое решение позволило уменьшить время, затрачиваемое при вызове системных функций, поскольку отпадает необходимость изменять при этом отображение страниц, нужно только разрешить их использование. Однако, чтобы сам вызов API-функций был возможен, системные библиотеки, которые содержат эти функции, размещаются в младшей, пользовательской половине виртуального пространства.
В Windows 95 принято хулиганское решение: система и здесь располагается в старшей половине памяти, но эта половина доступна процессу пользователя и для чтения, и для записи. При этом вызов системы становится еще проще, но зато система становится беззащитной перед любой некорректной программой, лезущей куда не надо.
Кроме старших 2 Гб, процессу недоступны еще некоторые небольшие области в начале и в конце виртуального пространства. В WindowsNT недоступны адреса с 0000000016 по 0000FFFF16 и с 7FFF000016 по 7FFFFFFF16, т.е. два кусочка по 64 Кб. Это сделано с целью выявления такой типичной ошибки программирования, как использование неинициализированных указателей, которые обычно попадают в запретные диапазоны адресов.
Для 64-разрядных процессоров размер виртуального адресного пространства возрастает до трудно представимых 264 байт (17 миллиардов гигабайт, если угодно), однако WindowsXP выделяет в распоряжение каждого процесса «всего лишь» 7152 гигабайта с адресами от 0 до 6FBFFFFFFFF16, а остальное адресное пространство может использоваться только системой.
5.8.2. Регионы
Рассмотрим теперь, каким образом программа процесса может использовать свое адресное пространство.
Попытка просто-напросто использовать в программе произвольно выбранный адрес в пределах адресного пространства процесса, скорее всего, приведет к выдаче сообщения об ошибке защиты памяти. На самом деле, использовать виртуальный адрес можно только после того, как ему поставлен в соответствие адрес физический. Такое сопоставление выполняется путем выделения регионов виртуальной памяти.
Регион памяти всегда имеет размеры, кратные 4 Кб (т.е. он содержит целое число страниц), а его начальный адрес кратен 64 Кб.
Для выделения региона используется функция VirtualAlloc. Она требует указания следующих параметров.
· Начальный виртуальный адрес региона. Если указана константа NULL, то система сама выбирает адрес. Если указан адрес, не кратный 64 К, то система округляет его вниз.
· Размер региона. При необходимости система округляет его до величины, кратной 4 Кб.
· Тип выделения. Здесь указывается одна из констант MEM_RESERVE (резервирование памяти) или MEM_COMMIT (передача физической памяти), смысл которых будет подробно рассмотрен ниже, или комбинация обеих констант.
· Тип доступа. Он определяет, какие операции могут выполняться со страницами выделенной памяти. Наиболее важны следующие типы доступа.
- PAGE_READONLY – доступ только для чтения, попытка записи в память приводит к ошибке.
- PAGE_READWRITE – доступ для чтения и записи.
- PAGE_GUARD – дополнительный флаг «охраны страниц», который должен комбинироваться с одним из предыдущих. При первой же попытке доступа к охраняемой странице генерируется прерывание, извещающее об этом систему. При этом флаг охраны автоматически снимается, так что дальнейшая работа со страницей выполняется без проблем.
Самое важное, что следует понять про выделение регионов, это смысл операций резервирования и передачи памяти.
Резервирование региона памяти (MEM_RESERVE) означает всего лишь то, что диапазон виртуальных адресов, соответствующих данному региону, не будет использован ни под какие другие цели, система считает его занятым. Это как резервирование авиабилета: вы пока что не владеете билетом, но и никому другому его не продадут.
Попытка программы обратиться к адресу в зарезервированном, но не переданном регионе приведет к ошибке.
Передача физической памяти (MEM_COMMIT) означает, что за каждой страницей виртуальной памяти региона система закрепляет… нет, вовсе не страницу физической памяти, как можно подумать. Закрепляется блок размером 4 Кб в страничном файле. В таблице страниц процесса переданные страницы помечаются как отсутствующие в памяти.
Теперь попытка обращения к адресу в регионе приведет уже к совсем другому результату. Поскольку страница отсутствует в памяти, произойдет прерывание. Однако это не будет рассматриваться как ошибка в программе. Система, обрабатывая прерывание, выполнит операцию чтения страницы с диска, из страничного файла, в основную память и занесет в таблицу страниц физический адрес, который теперь соответствует виртуальной странице. После этого команда, вызвавшая прерывание, будет повторена, но теперь уже с успехом, поскольку требуемая страница находится в памяти. Дальнейшие обращения к той же виртуальной странице будут выполняться без проблем, пока страница находится в памяти.
Резервирование и передача памяти могут выполняться одновременно, при одном обращении к функции VirtualAlloc, которой для этого нужно передать комбинацию обеих констант: MEM_RESERVE + MEM_COMMIT. Есть и другой вариант: сначала зарезервировать регион памяти, а затем, по мере необходимости, передавать физическую память либо всему региону сразу, либо его отдельным частям (субрегионам). Для этого в первый раз функция VirtualAlloc вызывается с константой MEM_RESERVE и, как правило, без указания конкретного адреса. Затем вызывается VirtualAlloc с константой MEM_COMMIT и с указанием адреса ранее зарезервированного региона или соответствующего субрегиона.