Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6274
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Глава 18. Динамически распределяемая память.docx 603

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

Создание дополнительной кучи

Дополнительные кучи в процессе создаются вызовом HeapCreate:

HANDLE HeapCreate(

DWORD fdwOptions,

SIZE_T dwInitialSize,

SIZE_T dwHaximumSize);

Параметр fdwOptions модифицирует способ выполнения операций над кучей. В нем можно указать 0, НЕАР_NO_SERIALIZE,

НЕАР_GENERATE_EXCEPTIONS, HEAP_CREATE_ENABLE_EXECUTE или комбинацию ЭТИХ флагов.

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

1.Просматривает связанный список выделенных и свободных блоков памяти.

2.Находит адрес свободного блока.

3.Выделяет новый блок, помечая свободный как занятый.

4.Добавляет новый элемент в связанный список блоков памяти.

Флаг HEAP_NO_SERIALIZE использовать не следует, и вот почему. Допустим, два потока одновременно пытаются выделить блоки памяти из одной кучи. Первый поток выполняет операции по пп. 1 и 2 и получает адрес свободного блока памяти. Но только он соберется перейти к третьему этапу, как его вытеснит второй поток и тоже выполнит операции по пп. 1 и 2. Поскольку первый поток не успел дойти до этапа 3, второй ноток обнаружит тот же свободный блок памяти.

Итак, оба потока считают, что они нашли свободный блок памяти в куче. Поэтому поток 1 обновляет связанный список, помечая новый блок как занятый. После этого и поток 2 обновляет связанный список, помечая тот же блок как занятый. Ни один из потоков пока ничего не подозревает, хотя оба получили адреса, указывающие на один и тот же блок памяти.

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

Повреждение связанного списка блоков памяти. Эта проблема не проявится до попытки выделения или освобождения блока.

Оба потока делят один и тот же блок памяти. Оба записывают в него свою информацию. Когда поток 1 начнет просматривать содержимое блока, он не поймет данные, записанные потоком 2.

604 Часть III. Управление памятью

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

Решение этих проблем — предоставить одному из потоков монопольный дос-

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

в процессе существует лишь один поток;

в процессе несколько потоков, но с кучей работает лишь один из них;

в процессе несколько потоков, но он сам регулирует доступ потоков к куче, применяя различные формы взаимоисключения, например критические секции, объекты-мьютексы или семафоры (см. главы 8 и 9).

Если вы не уверены, нужен ли вам флаг HEAP_NO_SERIALIZE, лучше не

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

Другой флаг, HEAP_GENERATE_EXCEPTIONS, заставляет систему генерировать исключение при любом провале попытки выделения блока в куче. Исключение (см. главы 23,24 и 25) — еще один способ уведомления программы об ошибке. Иногда приложение удобнее разрабатывать, полагаясь на перехват исключений, а не на проверку значений, возвращаемых функциями.

Примечание. По умолчанию, если при вызове Heap*-функций операционная система обнаружит повреждение кучи (например, запись за пределами выделенного блока), не произойдет ничего особенного, если вы не отлаживаете программу. Эти недостатки часто использовались для хакерских атак, что вынудило Майкрософт реализовать дополнительные механизмы для обнаружения повреждений кучи.

Теперь диспетчер кучи может генерировать исключение, если при вызове любой из Неар*-функций будет обнаружено повреждение кучи. Для этого следует исполнить следующий код:

HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);

Первый параметр HeapSetInformation игнорируется, если во втором пара-

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

Для хранения в куче исполняемого кода служит последний флаг — HEAP_CREATE_ENABLE_EXECUTE. Это особенно важно, если активен защитный механизм Data Execution Prevention (см. главу 13). Если вы не установите этот флаг для блока с кодом, хранящегося в куче, Windows сгенерирует исключе-

ние EXCEPTION_ACCESS_VIOLATION.

Глава 18. Динамически распределяемая память.docx 605

Второй параметр функции HeapCreate dwInitiallSize — определяет количество байтов, первоначально передаваемых куче. При необходимости функция округляет это значение до ближайшей большей величины, кратной размеру страниц. И последний параметр, dwMaximumSize, указывает максимальный объем, до которого может расширяться куча (предельный объем адресного пространства, резервируемого под кучу). Если он больше 0, вы создадите кучу именно такого размера и нс сможете его увеличить. А если этот параметр равен 0, система резервирует регион и, если надо, расширяет его до максимально возможного объема. При успешном создании кучи HeapCreate возвращает описатель, идентифицирующий новую кучу. Он используется и другими функциями, работающими с кучами.

Выделение блока памяти из кучи

Для этого достаточно вызвать функцию HeapAlloc:

PV0ID HeapAlloc(

HANDLE hHeap,

DWORD fdwFlags,

SIZE_T dwBytes);

Параметр hHeap идентифицирует описатель кучи, из которой выделяется память. Параметр dwBytes определяет число выделяемых в куче байтов, а параметр fdwFlags позволяет указывать флаги, влияющие на характер выделения памяти. В настоящее время поддерживается только три флага: HEAP_ZERO_MEMORY, HEAP_GENERATE_EXCEPTIONS и HEAP_NO_SERIALIZE.

Назначение флага HEAP_ZERO_MEMORY очевидно. Он приводит к заполнению содержимого блока нулями перед возвратом из HeapAlloc. Второй флаг заставляет эту функцию генерировать программное исключение, если в куче не хватает памяти для удовлетворения запроса. Вспомните, этот флаг можно указывать и при создании кучи функцией HeapCreate; он сообщает диспетчеру, управляющему кучами, что при невозможности выделения блока в куче надо генерировать соответствующее исключение. Если вы включили данный флаг при вызове HeapCreate, то при вызове HeapAlloc указывать его уже не нужно. С другой стороны, вы могли создать кучу без флага HEAP_GENERATE_EXCEPTIONS. В таком случае, если вы укажете его при вызове HeapAlloc, он повлияет лишь на данный ее вызов.

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

606 Часть III. Управление памятью

Табл. 18-1. Исключения, генерируемые функцией HeapAlloc

Идентификатор

Описание

STATUS_NO_MEMORY

Попытка выделения памяти не удалась из-за ее нехватки

STATUS_ACCESS_VIOLATION

Попытка выделения памяти не удалась из-за повреждения

 

кучи или неверных параметров функции

При успешном выделении блока HeapAlloc возвращает его адрес. Если памяти недостаточно и флаг HEAP_GENERATE_EXCEPTIONS не указан, функция возвращает NULL.

Флаг HEAP_NO_SERIALIZE заставляет HeapAlloc при данном вызове не применять принцип последовательного доступа к куче. Этим флагом нужно пользоваться с величайшей осторожностью, так как куча (особенно стандартная куча процесса) может быть повреждена при одновременном доступе к ней нескольких потоков.

Примечание. Для выделения больших блоков памяти (от 1 Мб) рекомендуется использовать функцию VirtualAlloc, а не функции, оперирующие с кучами.

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

UL0NG HeapInformationValue = 2; if (HeapSetInformation(

hHeap, HeapCompatibilityInformation, &HeapInformationValue, sizeof(HeapInformationValue)) {

//переключаем hHeap в режии низкой фрагментации

}else {

//hHeap невозможно переключить в режим низкой фрагментации,

//возможно, из-за того, что куча создана с флагом HEAP_NO_SERIALIZE.

Если передать описатель, который вернула GetPivcessHeap, функции HeapSetInformation, можно переключить кучу в режим низкой фрагментации. Вызов HeapSetInformation окончится неудачей, если передать ей флаг HEAP_NO_SERIALIZE. Заметьте, что некоторые параметры отладчика также блокируют переключение кучи в режим низкой фрагментации. Чтобы

Глава 18. Динамически распределяемая память.docx 607

отключить их, установите переменную окружения _NO_DEBUG_HEAP в 1. Заметьте также, что диспетчер кучи отслеживает выделяемую память и оптимизирует кучу. Например, диспетчер может автоматически переключить кучу в режим низкой фрагментации, если сочтет, что это повысит производительность вашей программы.

Изменение размера блока

Часто бывает необходимо изменить размер блока памяти. Некоторые приложения изначально выделяют больший, чем нужно, блок, а затем, разместив в нем данные, уменьшают его. Но некоторые, наоборот, сначала выделяют небольшой блок памяти и потом увеличивают его по мере записи новых данных. Для изменения размера блока памяти вызывается функция HeapReAlloc.

PVOID HeapReAlloc(

HANDLE hHeap,

DWORD fdwFlags,

PVOID pvMem,

SIZE_T dwBytes);

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

нии размера блока: HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_ZERO_MEMORY или HEAP_REALLOCK_IN_PLACE_ONLY.

Первые два флага имеют тот же смысл, что и при использовании с HeapAlloc. Флаг HEAP_ZERO_MEMORY полезен только при увеличении размера блока памяти, в этом случае дополнительные байты, включаемые в блок, предварительно обнуляются. При уменьшении размера блока этот флаг не действует.

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

Остальные два параметра (pvMem и dwBytes) определяют текущий адрес изменяемого блока и его новый размер (в байтах). Функция HeapReAlloc возвращает либо адрес нового, измененного блока, либо NULL, если размер блока изменить не удалось.

608 Часть III. Управление памятью

Определение размера блока

Выделив блок памяти, можно вызвать HeapSize и узнать его истинный размер:

SIZE_T HeapSize(

HANDLE hHeap,

DWORD fdwFlags,

LPCVOID pvMem);

Параметр hHeap идентифицирует кучу, а параметр pvMem сообщает адрес блока. Параметр fdwFlags принимает два значения: 0 или HEAP_NO_SERIALIZE.

Освобождение блока

Для этого служит функция HeapFree:

BOOL HeapFree(

HANDLE hHeap,

DWORD fdwFlags,

PVOID pvMem);

Она освобождает блок памяти и при успешном вызове возвращает TRUE. Параметр fdwFtegs принимает два значения: 0 или HEAP_NO_SERIALIZE. Обращение к этой функции может привести к тому, что диспетчер, управляющий кучами, вернет часть физической памяти системе, но это не обязательно.

Уничтожение кучи

Кучу можно уничтожить вызовом HeapDestroy:

BOOL HeapDestroy(HANDLE hHeap);

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

Система не позволит уничтожить стандартную кучу процесса — она разрушается только при завершении процесса. Если вы передадите описатель этой кучи функции HeapDestroy, система просто проигнорирует ваш вызов.

Использование куч в программах на C++

Чтобы в полной мере использовать преимущества динамически распределяемой памяти, следует включить ее поддержку в существующие программы, написанные на С++. В этом языке выделение памяти для объекта класса выполняется вызовом оператора new, а не функцией malloc, как в обычной биб-

Глава 18. Динамически распределяемая память.docx 609

лиотеке С. Когда необходимость в данном объекте класса отпадает, вместо библиотечной С-функции free следует применять оператор delete. Скажем, у нас есть класс CSomcClass, и мы хотим создать экземпляр этого класса. Для этого нужно написать что-то вроде:

CSomeClass* pSomeClass = new CSomeClass;

Дойдя до этой строки, компилятор C++ сначала проверит, содержит ли класс CSomeClass функцию-член, переопределяющую оператор new. Если да, компилятор генерирует код для вызова этой функции. Нет — создает код для вызова стандартного С++-оператора new.

Созданный объект уничтожается обращением к оператору detete:

delete pSomeClass;

Переопределяя операторы new и delete для нашего С++-класса, мы получаем возможность использовать преимущества функций, управляющих кучами. Для этого определим класс CSomeClass в заголовочном файле, скажем, так:

class CSomeClass { private:

static HANDLE s_hHeap;

static UINT s_uNuroAllocsInHeap;

// здесь располагаются закрытые данные и функции-члены

public:

void* operator new (size_t size); void operator delete (void* p);

// здесь располагаются открытые данные и функции-члены

};

Я объявил два элемента данных, s_hHeap и s_hNumAllocsInHeap, как статические переменные. А раз так, то компилятор C++ заставит все экземпляры класса CSomeClass использовать одни и те же переменные. Иначе говоря, он не станет выделять отдельные переменные s_hHeap и s_uNumAllocsInHeap для каждого создаваемого экземпляра класса. Это очень важно: ведь мы хотим, чтобы все экземпляры класса CSomeClass были созданы в одной куче.

Переменная s_hHeap будет содержать описатель кучи, в которой создаются объекты CSomeClass. Переменная s_uNumAllocsInHeap — просто счетчик созданных в куче объектов CSomeClass. Она увеличивается на 1 при создании в куче нового объекта CSomeClass и соответственно уменьшается при его уничтожении. Когда счетчик обнуляется, куча освобождается. Для управления кучей в СРРфайл следует включить примерно такой код:

HANDLE CSomeClass::s_hHeap = NULL;

UINT CSomeClass::s_uNumAllocsInHeap = 0;

610 Часть III. Управление памятью

void* CSomeClass::operator new (size_t size) { if (s_hHeap == NULL) {

// куча не существует; создаем ее

s.hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);

if (s.hHeap == NULL) return(NULL);

}

// куча для объектов CSomeClass существует void* p = HeapAlloc(s_hHeap, 0, size);

if (p != NULL) {

// память выделена успешно; увеличиваем счетчик объектов CSomeClass в куче s_uNumAllocsInHeap++;

}

// возвращаем адрес созданного объекта CSomeClass return(p);

}

Заметьте, что сначала я объявил два статических элемента данных, s_hHeap и s_uNumAllocsInHeap, а затем инициализировал их значениями NULL и 0 соответственно.

Оператор new принимает один параметр — size, указывающий число байтов, нужных для хранения CSomeClass. Первым делом он создает кучу, если таковой нет. Для проверки анализируется значение переменной s_hHeap: если оно NULL, кучи нет, и тогда она создается функцией HeapCreate, а описатель, возвращаемый функцией, сохраняется в переменной s_hHeap, чтобы при следующем вызове оператора new использовать существующую кучу, а не создавать еще одну.

Вызывая HeapCreate, я указал флаг HEAP_NO_SERIALIZE, потому что данная программа построена как однопоточная. Остальные параметры, указанные при вызове HeapCreate, определяют начальный и максимальный размер кучи. Я подставил на их место по нулю. Первый нуль означает, что у кучи нет начального размера, второй — что куча должна расширяться по мере необходимости.

Не исключено, что вам показалось, будто параметр size оператора new стоит передать в HeapCreate как второй параметр. Вроде бы тогда можно инициализировать кучу так, чтобы она была достаточно большой для размещения одного экземпляра класса. И в таком случае функция HeapAlloc при первом вызове работала бы быстрее, так как не пришлось бы изменять размер кучи под экземпляр класса. Увы, мир устроен не так, как хотелось бы. Из-за того, что с каждым выделенным внутри кучи блоком памяти связан свой заголовок, при вызове HeapAlloc все равно пришлось бы менять размер кучи, чтобы в нее поместился не только экземпляр класса, но и связанный с ним заголовок.

Глава 18. Динамически распределяемая память.docx 611

После создания кучи из нее можно выделять намять под новые объекты CSomeClass с помощью функции НеарAllос. Первый параметр — описатель кучи, второй — размер объекта CSomeClass. Функция возвращает адрес выделенного блока.

Если выделение прошло успешно, я увеличиваю переменную-счетчик s_uNumAllocsInHeap, чтобы знать число выделенных блоков в куче. Наконец, оператор new возвращает адрес только что созданного объекта CSomeClass.

Вот так происходит создание нового объекта CSomeClass. Теперь рассмотрим, как этот объект разрушается, — если он больше не нужен программе. Эта задача возлагается на функцию, переопределяющую оператор delete:

void CSomeClass::operator delete (void* p) { if (HeapFree(s_hHeap, 0, p)) {

// объект удален успешно s_uNumAllocsInHeap--;

}

if (s_uNumAllocsInHeap == 0) {

// если в куче больше нет объектов, уничтожаем ее if (HeapDestroy(s_hHeap)) {

//описатель кучи приравниваем NULL, чтобы оператор new

//мог создать новую кучу при создании нового объекта CSomeClass s_hHeap = NULL;}

}

}

}

Оператор delete принимает только один параметр: адрес удаляемого объекта. Сначала он вызывает HeapFree и передает ей описатель кучи и адрес высвобождаемого объекта. Если объект освобожден успешно, s_uNumAltocsInHeap уменьшается, показывая, что одним объектом CSomeClass в куче стало меньше. Далее оператор проверяет: не равна ли эта переменная 0, и, если да, вызывает HeapDestroy, передавая ей описатель кучи. Если куча уничтожена, s_hHeap присваивается NULL. Это важно: ведь в будущем наша программа может попытаться создать другой объект CSomeClass. При этом будет вызван оператор new, который проверит значение s_hHeap, чтобы определить, нужно ли использовать существующую кучу или создать новую.

Данный пример иллюстрирует очень удобную схему работы с несколькими кучами. Этот код легко подстроить и включить в ваши классы. Но сначала, может быть, стоит поразмыслить над проблемой наследования. Если при создании нового класса вы используете класс CSomeClass как базовый, то производный класс унаследует операторы new и delete, принадлежащие классу CSomeClass. Новый класс унаследует и его кучу, а это значит, что применение оператора new к производному классу повлечет выделение памяти для объекта этого класса из той же кучи, которую использует и класс CSomeClass. Хорошо это или нет, зависит от конкретной ситуации. Если

612 Часть III. Управление памятью

объекты сильно различаются размерами, это может привести к фрагментации кучи, что затруднит выявление таких ошибок в коде, о которых я рассказывал в разделах «Защита компонентов» и «Более эффективное управление памятью».

Если вы хотите использовать отдельную кучу для производных классов, нужно продублировать все, что я сделал для класса CSomeClass. А конкретнее — включить еще один набор переменных s_hHeap и s_uNumAllocsInHeap и повторить еще раз код для операторов new и delete. Компилятор увидит, что вы переопределили в производном классе операторы new и delete, и сформирует обращение именно к ним, а не к тем, которые содержатся в базовом классе.

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

Другие функции управления кучами

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

ToolHelp-функции (упомянутые в конце главы 4) дают возможность перечислять кучи процесса, а также выделенные внутри них блоки памяти. За более подробной информацией я отсылаю вас к документации Platform SDK: ищите разде-

лы по функциям Heap32First, Heap32Next, Heap32ListFirst и Heap32ListNext.

В адресном пространстве процесса может быть несколько куч, и функция GetProcessHeaps позволяет получить их описатели «одним махом»:

DWORD GetProcessHeaps(

DWORD dwNumHeaps,

PHANDLE phHeaps);

Предварительно вы должны создать массив описателей, а затем вызвать функцию так, как показано ниже.

HANDLE hHeaps[25];

DWORD dwHeaps = GetProcessHeaps(25, hHeaps); if (dwHeaps > 25) {

//У процесса больше куч, чем мы ожидали

}else {

//элементы от hHeaps[0] до hHeaps[dwHeaps - 1]

//идентифицируют существующие кучи

}

Соседние файлы в предмете Программирование на C++