Системные функции распределения памяти

Кроме незаметного для выполняемой программы использования виртуальной памяти, современные ОС содержат средства явного получения заказываемых объемов памяти. Напомним, что в языках программирования высокого уровня большая часть данных используется с помощью имен. Место в памяти для данных сопоставляется с именем транслятором. (Компиляторы выполняют только предварительное сопоставление, которое далее уточняется компоновщиком, а затем, возможно, еще и загрузчиком программы.) Тем не менее, в современных языках программирования есть возможность использовать область памяти, задаваемую уже на стадии выполнения. Для этих целей служат именованные указатели, которые можно настроить на почти любое место в оперативной памяти (точнее на место, доступное по правам чтения или записи). Операционные системы содержат системные средства, которые позволяют запрашивать для работы программы дополнительные области данных, причем для последующего доступа к этим областям системная функция возвращает значение указателя, которое может быть запомнено в именованном указателе.

Проще всего такие средства организованы в Unix. Здесь имеются всего четыре системные функции, связанные с выделением и распределением оперативной памяти, называемые malloc, free, calloc и realloc. Первая из них имеет прототип

void* malloc(size_t nbytes).

Единственным аргументом этой функции служит число байтов в запрашиваемой области данных, а возвращается указатель на не детализированный тип данных. При невозможности выделить операционной системой заданный объем памяти, эта функция возвращает нулевой указатель (NULL). Данная функция имеет полные аналоги в системах программирования на языке Си для практически всех операционных систем, и использование ее обычно хорошо знакомо любому профессиональному программисту.

Как только запрошенная функцией malloc() область памяти становится ненужной программе, настоятельно рекомендуется отдать приказ об ее освобождении. Такое решение следует применять даже для персональных и "сильно накрученных" компьютеров, так как оперативная память очень важный ресурс, и ее недостаток может существенно осложнить работу других программ. Для этого освобождения предназначена функция с именем free. Она имеет прототип

void free(void* ptr),

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

Функция calloc имеет прототип

void* calloc(size_t nelem, size_t nbytes)

и позволяет выделять область данных для размещения массива из nelem элементов, каждый из которых занимает nbytes байтов. (Практически эта функция сводится к соответствующему вызову исходной функции malloc.)

Наконец последняя из перечисленных функций имеет прототип

void* realloc(void* oldptr, size_t newsize).

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

В операционных системах OS/2 и Windows совокупность основных функций распределения памяти гораздо сложнее. В ней отразилась структурная сложность страничной переадресации для архитектуры IA32, которая построена как двухуровневая и рассматривалась в начале этой главы.

Практическое получение памяти программой, запрашивающей у ОС дополнительную область памяти (блок памяти), включает два этапа. На первом этапе у ОС запрашивается диапазон адресов виртуального адресного пространства, для которого ОС должна построить таблицы страниц в памяти и заполнить строки таблицы каталога. На этом этапе память выделяется только для хранения таблиц страничного преобразования (в лучшем случае – оперативная, но временно таблицы страниц могут находиться и во внешней памяти – в области свопинга). На втором этапе память выделяется уже непосредственно для запрошенного блока памяти, и при этом ОС заполняет соответствующие строки ранее построенных таблиц страниц текущего процесса. Вся эта сложность возникает от громоздкости суммарных таблиц страничного преобразования и благородной цели – по возможности сократить этот объем. Альтернативой было исходное построение всей теоретически возможной совокупности таблиц страниц для процесса (более 4 Мбайт на процесс) или перестраивание совокупности таблиц страничных преобразований почти при всяком запросе на новый блок данных. Первый вариант расточителен по расходу памяти, второй – по времени выполнения.

Для первого из перечисленных этапов сложилось название резервирование памяти в адресном пространстве или просто резервирование памяти (reserve). Второй этап обозначается английским словом commit. На русском языке пока не сложилось устойчивого наименования этого этапа. Его можно называть передачей региону физической памяти [13], что совершенно правильно, но очень длинно. Мы будем называть этот этап – задействовать память или ввести в использование.

В операционной системе OS/2 системные функции для выполнения перечисленных этапов явно разделены. Для этапа резервирования предназначена здесь функция с прототипом

APIRET DosAllocMem(void **ppmem, ULONG size, ULONG flag),

где ppmem – указатель на переменную, которая будет получать базовый виртуальный адрес выделенного блока памяти, size – заказываемый размер блока памяти в байтах, а аргумент flag определяет атрибуты распределения и желаемого доступа. Он может задаваться с помощью следующих символических констант: PAG_COMMIT, PAG_EXECUTE, PAG_READ, PAG_WRITE, PAG_GUARD.

Константа PAG_COMMIT дополнительно задает выполнение второго этапа (введение в действие памяти) во время выполнения рассматриваемой функции. Остальные константы задают вид доступа к запрашиваемому блоку памяти и могут использоваться совместно как с первой константой, так и друг с другом. Константа PAG_GUARD задает особый вид доступа к блоку данных, предназначенному, как правило, для использования в качестве стека. При этом виде доступа обращение к последней странице блока памяти вызывает специальное исключение, обычно используемое программой для расширения области стека. При наличии флага PAG_COMMIT обязательно должен быть задан хотя бы один из флагов PAG_EXECUTE, PAG_READ, PAG_WRITE (первые два из них для архитектуры IA32 совершенно равносильны). Функция DosAllocMem выделяет целое число страниц оперативной памяти, это, в частности, обозначает, что при задании значения для параметра size, не кратного 4096 байтам, выделяется (в частности, резервируется) больше памяти, чем запрошено, а именно, до следующей границы, кратной 4096.

Для освобождения ранее выделенного блока памяти в OS/2 служит функция с прототипом

APIRET DosFreeMem(void* pb),

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

Если для выполнения функции DosAllocMem не был задан флаг PAG_COMMIT, то после выполнения начального резервирования памяти необходимо выполнить системную функцию с прототипом

APIRET DosSetMem(void* pb, ULONG size, ULONG flag).

Здесь параметр pb задает значение указателя (виртуальный адрес) внутри ранее полученного блока памяти, аргумент size определяет размер вводимого в действие участка блока памяти, а последний аргумент задает виды доступа к этому участку и, возможно, изменение статуса диапазона адресов этого участка. Виды доступа задаются теми же константами PAG_EXECUTE, PAG_READ, PAG_WRITE, PAG_GUARD, которые уже рассматривались. Кроме того, для этих же целей допустима константа PAG_DEFAULT, применимая к участку, на котором ранее уже задавались виды доступа. Изменение статуса диапазона адресов участка задается одной из констант PAG_COMMIT, PAG_DECOMMIT. Первая из них вводит в действие участок адресов, задаваемый рассматриваемой системной функцией, а вторая выводит его из такого действия. Во втором случае физическая память, ранее выделенная участку адресов, отбирается и возвращается в резерв операционной системы.

Функция DosSetMem может воздействовать только на целое число страниц. Это значит, что если значение адреса виртуальной памяти, задаваемое параметром pb, не кратно 4096, то изменению атрибутов доступа и действия подвергается вся страница, внутри которой находится базовый адрес, задаваемый указателем pb. Если же значение адреса, получаемого от суммирования параметра size со значением базового адреса участка не кратно 4096, то задаваемому изменению атрибутов доступа и действия подвергается вся страница, внутри которой находится значение этого суммарного адреса. Кроме того, указанному действию подвергаются и все страницы, лежащие между двумя указанными адресами.

 

Действия двух рассмотренных системных функций в Windows может задавать универсальная функция с прототипом

VOID* VirtualAlloc(VOID *pvAddress, DWORD size,

DWORD type, DWORD protect ).

Эта функция может выполнять как первый из перечисленных выше, так и второй этапы распределения памяти, но может выполнять и их оба одновременно. В ней аргумент pvAddress задает адрес виртуальной памяти, начиная с которого желательно выделить блок, называемый в этой системе регионом. Можно задавать этот параметр нулевым указателем, что равносильно пожеланию к ОС ею самой выбрать базовый адрес региона. Подчеркнем, что при ненулевом значении первого аргумента непосредственно в нем следует задавать адрес, а не указатель на адрес (адрес с точки зрения формализмов языка Си и является указателем). При ненулевом аргументе pvAddress и использовании функции распределения для резервирования региона значение этого аргумента должно быть кратно 64 Кбайт. Если функция выполняется успешно, то базовый адрес выделенного блока (региона) возвращается в виде значения функции. Параметр size задает желаемый размер запрашиваемого блока, причем следует иметь в виду, что память выделяется целым числом страниц, поэтому целесообразно для ясности самому программисту запрашивать резервирование региона размером в целое число страниц (иначе имеет место неявное выделение дополнительного участка за явно заказанным).

Параметр type может задаваться символическими константами MEM_COMMIT, MEM_RESERVE, определяя тем самым либо резервирование региона, либо введение региона в действие. Использование обеих констант, объединенных логической операцией ИЛИ задает обе эти операции при распределении памяти.

Аргумент protect определяет для региона тип защиты, запрашиваемый при вызове функции. Значением этого параметра может быть одна из символических констант PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE (равносильна PAGE_READONLY). Дополнительно (объединением по операции ИЛИ) может быть указана еще и константа PAGE_GUARD.

Обратной функцией к рассмотренной служит функция с прототипом

BOOL VirtualFree(VOID *pvAddress, DWORD size, DWORD type),

где в качестве значения последнего из аргументов может быть использована одна из констант MEM_DECOMMIT, MEM_RELEASE.

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

 

В практической работе с перечисленными функциями распределения памяти очень полезными, особенно при отладке приложений, оказываются информационные функции запроса информации о блоках виртуальной памяти. Для OS/2 такая функция имеет прототип

APIRET DosQueryMem(void* pb, ULONG *size, ULONG *pflag),

где аргумент pb задает базовый адрес блока, о котором запрашивается информация, аргумент size определяет размер участка блока, для которого запрашивается информация, причем этот же параметр возвращает размер того участка, о котором выдается такая информация. (Если оказывается, что запрошена информация о достаточно протяженном участке, части которого имеют различные виды доступа или характер выделения, то информация выдается только о начальном из них вместе с размером такого участка в данном возвращаемом аргументе.) Параметр pflag задает адрес переменной, в которой возвращается информация об участке. Эта информация имеет битовое кодирование и может быть проанализирована с помощью символических констант: PAG_COMMIT (со значением 0x00000010), PAG_SHARED (со значением 0x00002000), PAG_FREE (со значением 0x00004000), PAG_BASE (со значением 0x00010000), PAG_READ (со значением 0x00000001), PAG_WRITE (со значением 0x00000002), PAG_EXECUTE (со значением 0x00000004), PAG_GUARD (со значением 0x00000008). Причем значение константы PAG_BASE обозначает, что заданный в вызове адрес отвечает самому началу выделенного региона, а PAG_SHARED – что память выделена для совместного использования различными процессами.

В операционных системах Windows аналогичная функция имеет прототип

DWORD VirtualQuery(void* pvAddress,

MEMORY_BASIC_INFORMATION *pmbiBuffer, DWORD size),

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

typedef struct _MEMORY_BASIC_INFORMATION {

VOID *BaseAddress; // base address of region

VOID AllocationBase; // allocation base address

DWORD AllocationProtect; // initial access protection

DWORD RegionSize; // size, in bytes, of region

DWORD State; // committed, reserved, free

DWORD Protect; // current access protection

DWORD Type; // type of pages

} MEMORY_BASIC_INFORMATION;

В поле Protect этой структуры отдельными битами возвращаются значения символических констант PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE, PAGE_WRITECOPY, PAGE_GUARD, PAGE_NOACCESS. Последняя константа отражает состояние участка виртуальной памяти, при котором к нему нельзя обращаться ни для чтения, ни для записи, ни для выполнения программных кодов из этого участка. Из поля state можно получить присутствие в нем констант MEM_COMMIT, MEM_RESERVE или MEM_FREE. Наконец, поле type позволяет опросить информацию, задаваемую константами MEM_IMAGE, MEM_MAPPED, MEM_PRIVATE. (Функция возвращает число байтов в информационной структуре данных типа MEMORY_BASIC_INFORMATION.)

Дополнительно к только что рассмотренным функциям в Windows предлагается функция для изменения прав доступа к участку виртуальной памяти, имеющая прототип

BOOL VirtualProtect(void * pAddress, DWORD size,

DWORD protect, DWORD *OldProtect).

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

Использование всех перечисленных для Windows функций распределения памяти демонстрирует программа, приведенная в листинге 10.3.1.

 

#include <windows.h>

#include <stdio.h>

#include <stdlib.h>

 

void QueryMem(void *pmem, ULONG region);

void main()

{char *pm, *pm1, *pm2, ch;

BOOL rc;

int n;

DWORD oldProtect;

 

pm=VirtualAlloc(NULL, 16000, MEM_RESERVE,

PAGE_READONLY); // RESERVE

if (pm= =NULL) {printf(“Error VirtualAlloc with RC=%ld\n”,

GetLastError()); getchar(); exit(0);}

QueryMem(pm, 16384); printf(“After VirtualAlloc\n”);

 

VirtualAlloc(pm,1000, MEM_COMMIT, PAGE_READWRITE);

if (pm= =NULL) {printf(“Error Commit with RC=%ld\n”,

GetLastUrror()); getchar(); exit(0);}

QueryMem(pm, 16384); printf(“\nAfter Commit Mem\n”);

 

*(pm+999)=’a’; printf(“\nPause after writing into 999 offset.\n”);

getchar8); 0

0 *(pm+1000)=’b’; printf(“Pause after writing0into 1000 offset\n”);

getchar();

*(pm+4095)=’c’; printf(“Pause after writing into 4095 offset\n”);

getchar();

 

rc=VirtualProtect(pm, 4096,PAGE_READONLY, &oldProtect);

if (!rc) {printf(“Error VirtualProtect with RC=%ld\n”,

GetLastError()); getchar(); exit(0);}

QueryMem(pm, 16384); printf(“\nAfter VirtualProtect to READONLY\n”);

ch=*(pm+999);printf(“\nPause after reading from 999 offset ch=%c\n”,

ch); getchar();

ch=*(pm+1000);

printf(“Pause after reading from 1000 offset ch=%c\n”, ch); getchar();

ch=*(pm+4095);

printf(“Pause after reading from 4095 offset ch=%c\n”, ch); getchar();

if (VirtualAlloc(pm+4096,1000, MEM_COMMIT,

PAGE_READWRITE)= =NULL)

//можно было бы с адреса pm+4000, но тогда обе page будут READWRITE !

{printf(“Error Commit with RC=%ld\n”, GetLastError()); getchar();

exit(0);}

printf(“\nAfter SetMem WRITE next page and READ on first\n”);

QueryMem(pm, 16384) ; printf(“\n”);

QueryMem(pm+4096, 16384);

 

*(int*)(pm+4096)= 137;

printf(“\nPause after writing word into 4096 offset.\n”);

getchar();

n=*(int*)(pm+4096);

printf(“Reading from 4096 offset number=%d\n”, n);

VirtualFree(pm, 16384 ,MEM_RELEASE); ExitProcess(0);

}

 

void QueryMem(void *pmem, DWORD region)

{DWORD rc;

DWORD state, type, protect;

MEMORY_BASIC_INFORMATION rec;

void *pbase, *pregion;

 

rc=VirtualQuery(pmem, &rec, sizeof(rec));

state=rec.State; protect=rec.Protect;

region=rec.RegionSize;

pbase=rec.AllocationBase; pregion=rec.BaseAddress;

type=rec.Type;

 

if (rc= =0) {printf(“Error QueryMem with RC=%ld\n”, rc); exit(0);}

printf(“Base address_=%08X, region=%ld, BaseRegion=%ld\n”,

pbase, region, pregion);

if (protect&PAGE_READWRITE) printf(“PAGE_READWRITE “);

if (protect&PAGE_WRITECOPY) printf(“PAGE_WRITECOPY “);

if (protect&PAGE_READONLY) printf(“PAGE_READONLY “);

if (protect&PAGE_EXECUTE) printf(“PAGE_EXECUTE “);

if (protect&PAGE_EXECUTE_READWRITE)

printf(“PAGE_EXECUTE_READWRITE “);

if (protect&PAGE_EXECUTE_WRITECOPY)

printf(“PAGE_EXECUTE_WRITECOPY “);

if (protect&PAGE_GUARD) printf(“PAGE_GUARD “);

if (protect&PAGE_NOACCESS) printf(“PAGE_NOACCESS “);

if (state&MEM_COMMIT) printf(“MEM_COMMIT “);

if (state&MEM_RESERVE) printf(“MEM_RESERVE “);

if (state&MEM_FREE) printf(“MEM_FREE “);

if (type&MEM_IMAGE) printf(“MEM_IMAGE “);

if (type&MEM_MAPPED) printf(“MEM_MAPPED “);

if (type&MEM_PRIVATE) printf(“MEM_PRIVATE “);

}

Листинг 10.3.1. Распределение памяти в Windows

 

В последней программе вначале запрашивается резервирование региона памяти размером в 16 000 байтов, причем сразу задается, что доступ к этому региону будет только по чтению. Затем служебной подпрограммой, построенной на основе функции VirtualQuery, запрашивается информация об участке виртуальной памяти, начинающейся с возвращенного базового адреса, но охватывающего несколько больше запрошенного, а именно 4 страницы. Отображаемая подпрограммой информация свидетельствует, что действительно выделено больше чем 16000, а 16384 байта, т.е.- ровно четыре страницы.

Далее в программе перераспределяется часть уже распределенного региона, а именно с помощью той же универсальной функции VirtualAlloc задается введение в использование начального участка региона размером в 1000 байтов и с новым видом доступа по чтению и записи. Полученный результат опрашивается с помощью все той же служебной программы для того же большого интервала адресов в 4 страницы. Эта подпрограмма возвращает, как сама указывает, информацию только для участка памяти размером в 4096 байтов. Для этого участка выдается, что память введена в действие и имеет уже доступ и по записи. (Далее на большом запрошенном участке действуют уже другие права доступа и распределения.)

После этого в байты распределенного региона, имеющие смещение от его начала 999, 1000 и 4095, записываются данные. Несмотря на то, что и байт по смещению 1000 и тем более байт по смещению 4095 находятся за пределами участка в 1000 байтов, на котором явно задан доступ по записи и который, казалось бы, только и введен в действие, эти записи свободно осуществляются. Тем самым демонстрируется, что заказанные действия неизбежно распространяются на всю текущую страницу памяти.

Далее в программе функцией VirtualProtect меняется вид доступа к первой странице региона, а именно меняется на "только для чтения". Затем опрашивается состояние первых четырех страниц, получается информация только о первой, которая и введена в действие, несмотря на то, что и дальнейшие и эта страница имеют доступ только для чтения. Из записанных ранее байтов читается информация, что удается сделать, так как такой доступ разрешен и теперь.

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