Как известно, при работе в IBM OS/2 программисту доступно 32-разрядное линейное (плоское) адресное пространство со страничной организацией, т. е. все операции с памятью производятся над страницами заданного размера. Каждому запущенному процессу OS/2 выделяет виртуальное адресное пространство размером 512 Мбайт, т. е. максимальный адрес, доступный программе, равен 0x1FFFFFFF (три старших разряда 32-разрядного указателя не используются).
Распределение виртуальной памяти отделено от ее отображения на физическую память компьютера. Благодаря этому разные процессы выполняются "бок о бок", но их виртуальные адресные пространства полностью изолированы друг от друга. Конечно, память, которую процесс фактически может занять при выполнении, ограничена размером ОЗУ компьютера и объемом свободного пространства в том разделе жесткого диска, где расположен файл страничной подкачки SWAPPER.DAT.
Младшие 64 Кбайт адресного пространства зарезервированы для нужд операционной системы, так что минимальный адрес равен 0x10000; он соответствует точке начала EXE-модуля. Вся остальная память делится на совместно используемую (ее должно быть не менее 64 Мбайт) и частную память процесса. Частная память, доступ к которой может получить только процесс-владелец, начинается с младших адресов и растет вверх. Совместно используемая, наоборот, выделяется начиная со старших адресов и растет вниз. Она доступна процессу всегда, даже если реально им не используется.
Для работы с памятью необходимо, во-первых, выделить виртуальную память, а во-вторых, отобразить виртуальные адреса на физические - зафиксировать память. Фиксация производится либо одновременно с выделением (тогда фиксируется сразу вся выделенная память), либо позднее (этот подход позволяет зафиксировать как всю выделенную память, так и только ее часть).
Операция, обратная фиксации, - расфиксирование (открепление), обратная выделению - освобождение. Закончив работу с виртуальной памятью, ее обязательно следует освободить, чтобы вернуть соответствующие ресурсы операционной системе. Если программа завершится, не освободив память, возникнет так называемая "утечка памяти": операционная система будет считать фактически свободную память используемой. Характерным признаком утечки памяти является увеличение объема файла SWAPPER.DAT.
Память выделяется и фиксируется порциями по 4 Кбайт, которые, как уже говорилось, называются страницами. Страница памяти может разрешать доступ для чтения, записи, выполнения или быть защищенной (guard page) - в последнем случае обращение к ней приводит к нарушению защиты.
Защищенные страницы, очевидно, нельзя использовать как ограничители, контролирующие границы массивов: если, скажем, запросить память для массива из 512 однобайтовых элементов, система выделит целую страницу, т. е. 4096 байт, так что обращение к элементу номер 513 не вызовет нарушения защиты. Основная функция защищенных страниц - обеспечивать автоматическое возрастание стека. Для массива, размер которого должен увеличиваться в процессе работы программы, можно выделить и зафиксировать две страницы, первая из которых будет доступной для чтения и записи, а вторая - защищенной. При попытке выйти за пределы отведенной страницы произойдет нарушение защиты, и в ответ операционная система (если только программа не обрабатывает эту ошибку самостоятельно) попытается зафиксировать защищенную страницу и отметить как защищенную следующую за ней. В случае неудачи возникает ошибка "Невозможно нарастить стек", после чего программа сможет продолжить работу, но для размещения массива в ее распоряжении будет всего 4096 байт.
В OS/2 существует несколько способов работы с памятью, или, иначе, типов памяти. Это объекты в памяти, куча, локальная память нити и совместно используемая память. Рассмотрим их особенности.
Основным способом работы с памятью в OS/2 является использование объектов в памяти (memory objects). Объект в памяти всегда состоит из целого числа страниц: при его создании запрошенное число байтов округляется в большую сторону до ближайшего значения, кратного 4 Кбайт. Внутри объекта память резервируется блоками от 1 байта до размера всего объекта.
Для выделения памяти объектам служит функция DosAllocMem, для ее освобождения - функция DosFreeMem, для фиксации и открепления - функция DosSetMem.
При работе с OS/2 рекомендуется не менять размер объектов в памяти, а сразу выделять достаточно большой "разреженный" объект, в котором затем по мере необходимости фиксировать и откреплять порции нужного размера. Например, если программе в большинстве случаев должно хватить около 512 Кбайт памяти, но при определенных условиях потребуется 5 Мбайт, нужно поступить следующим образом:
Куча (heap) - это область внутри объекта в памяти, из которой программа может выделять небольшие блоки; размер блока округляется в большую сторону до ближайшего значения, кратного 8 байтам.
Программисты, пишущие на Си, могут воспользоваться библиотечными функциями управления кучей - new, delete, malloc, strdup, free. Собственные функции менеджера кучи OS/2 называются DosSubSetMem, DosSubUnsetMem, DosSubAllocMem и DosSubFreeMem. Программа инициализирует объект в памяти, вызывая функцию DosSubSetMem, а затем выделяет и освобождает память внутри него с помощью соответственно DosSubAllocMem и DosSubFreeMem. В листинге содержится фрагмент программы, в котором создается объект для подвыделения памяти (размер объекта 8192 байт) и затем выделяются два небольших блока памяти.
По окончании работы с кучей, которая была инициализирована функцией DosSubSetMem, следует вызвать функцию DosSubUnsetMem: она освобождает ресурсы, использовавшиеся для управления кучей. Завершив работу с объектом в памяти, частью которого была куча, память освобождают обычным образом, с помощью функции DosFreeMem. Необходимо следить, чтобы на каждый вызов DosSubSetMem приходился вызов DosSubUnsetMem и чтобы для объектов, содержавших кучу, перед вызовом DosFreeMem вызывалась DosSubUnsetMem.
Следует помнить, что при работе с кучей память фиксируется постранично, т. е. при запросе на подвыделение для массива 512 байт будет зафиксировано 4096 байт. В результате обращение к 513-му байту массива не приведет к нарушению защиты, но при этом может оказаться, что программа обратилась к области памяти, подвыделенной другой нитью процесса.
Локальная память нити управления представляет собой небольшую (не более 32 двойных слов, т. е. 128 байт) область специфической памяти, которая отводится нити при инициализации и обычно используется для хранения небольшого числа указателей. Она выделяется функцией DosAllocThreadLocalMemory и освобождается функцией DosFreeThreadLocalMemory.
Функция DosAllocThreadLocalMemory выделяет за один раз восемь двойных слов; если требуется выделить больше, ее необходимо вызвать повторно. Локальная память нити должна использоваться бережно; не следует занимать весь 128-байтовый диапазон.
Совместно используемая память представляет собой объект в памяти, диапазон линейных адресов которого зарезервирован, как уже говорилось, в адресных пространствах всех процессов. Различается именованная и безымянная совместно используемая память. К именованной памяти процесс может обратиться непосредственно по имени, для обращения к безымянной ему необходим указатель, который он должен получить от процесса, создавшего данную безымянную память (не обязательно своего родительского).
Когда несколько процессов работают с одной и той же областью памяти, важно исключить конфликты. Для этого применяются два основных метода. Первый состоит в том, что процессы считывают и записывают информацию по очереди; доступ к памяти обычно контролируется при помощи семафора. При втором методе один процесс подготавливает данные в памяти и затем передает их другому процессу для дальнейшей обработки, а сам освобождает память. Таким образом, при любом методе память в каждый момент времени доступна только одному процессу.
#define INCL_DOSMEMMGR /* Типы и константы системы управления памятью */ #include <os2.h> APIRET rc; PBYTE pbBase, pb1, pb2; rc = DosAllocMem((PVOID *) &pbBase, 8192, fALLOC); /* Разместим объект размером 8 Кбайт */ rc = DosSubSetMem(pbBase, DOSSUB_INIT, 8192); /* Создадим объект для подвыделения */ rc = DosSubAllocMem(pbBase, (PVOID *) &pb1, 100); /* Выделим в нем 100 байтов */ rc = DosSubAllocMem(pbBase, (PVOID *) &pb2, 500); /* Выделим в нем еще 500 байтов */ rc = DosSubFreeMem(pbBase, pb1, 100); /* Освободим первое подвыделение */ rc = DosSubAllocMem(pbBase, (PVOID *) &pb1, 50); /* Выделим еще 50 байтов */