Какой API-интерфейс Alloc может вызывать VirtualAlloc / зарезервировать память внутри?

Я отлаживаю потенциальную проблему утечки памяти в отладочной DLL.

Дело в том, что процесс запускает субтест, который динамически загружает / выгружает DLL, во время теста зарезервировано и зафиксировано много памяти (1,3 ГБ). После завершения теста и выгрузки DLL остается зарезервировано огромное количество памяти (1,2 ГБ).

Причина, по которой я сказал, что эта зарезервированная память выделяется DLL, заключается в том, что если я использую DLL выпуска (ничего не изменилось, тот же тест), зарезервированная память составляет ~ 300 МБ, поэтому вся дополнительная зарезервированная память должна быть выделена в DLL отладки.

Похоже, что во время теста выделяется много памяти, но после теста выводится из режима работы (не освобождает статус). Итак, я хочу отслеживать, кто резервирует / распаковывает эту большую память. Но в исходном коде VirtualAlloc не вызывается, поэтому возникают следующие вопросы:

  1. VirtualAlloc - единственный способ зарезервировать память?
  2. Если нет, то какой другой API может это сделать? Если да, то какой другой API будет внутренне вызывать VirtualAlloc? Некоторые люди в сети говорят, что HeapAlloc будет внутренне вызывать VirtualAlloc? Как это работает?

person MichaelYi    schedule 30.11.2016    source источник


Ответы (1)


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

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

Следовательно, HeapAlloc () построен поверх VirtualAlloc (), как и GlobalAlloc () и LocalAlloc () (хотя последние два устарели в 32-битной Windows и в основном никогда не должны использоваться приложениями - предпочитайте явный вызов HeapAlloc () ).

Конечно, HeapAlloc () - это не просто оболочка вокруг VirtualAlloc (). Это добавляет некоторую собственную логику. VirtualAlloc () всегда выделяет память большими фрагментами, определяемыми степенью детализации распределения системы, которая зависит от оборудования (может быть получена путем вызова GetSystemInfo () и чтения значения SYSTEM_INFO.dwAllocationGranularity). HeapAlloc () позволяет выделять меньшие фрагменты памяти с любой необходимой степенью детализации, что гораздо больше подходит для типичного программирования приложений. Внутренне HeapAlloc () обрабатывает вызов VirtualAlloc () для получения большого фрагмента и затем разделяет его по мере необходимости. Это не только упрощает API, но и повышает его эффективность.

Обратите внимание, что функции распределения памяти, предоставляемые библиотекой времени выполнения C (CRT) - а именно, malloc () в C и новый оператор C ++ - пока находятся на более высоком уровне. Они построены на основе HeapAlloc () (по крайней мере, в реализации CRT от Microsoft). Внутри они выделяют значительный кусок памяти, который в основном служит «главным» блоком памяти для вашего приложения, а затем по запросу делят его на более мелкие блоки. Когда вы освобождаете / удаляете эти отдельные блоки, они возвращаются в пул. Опять же, этот дополнительный уровень обеспечивает упрощенный интерфейс (и, в частности, возможность писать независимый от платформы код), а также повышенную эффективность в общем случае.

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

Так что да, по сути, процедура выделения памяти самого низкого уровня для обычного приложения Windows - это VirtualAlloc (). Но это не значит, что это функция «рабочая лошадка», которую обычно следует использовать для выделения памяти. Вызывайте VirtualAlloc () только в том случае, если вам действительно нужны его дополнительные функции. В противном случае либо используйте процедуры выделения памяти из стандартной библиотеки, либо, если у вас есть веские причины избегать их (например, не связываться с CRT или создавать собственный пул памяти), вызовите HeapAlloc ().

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


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

Фактически, вся эта зарезервированная память вовсе не может быть утечкой. Довольно распространенная стратегия, используемая при отладке, состоит в том, чтобы зарезервировать определенный диапазон адресов памяти без их фиксации, чтобы перехватить попытки доступа к памяти в этом диапазоне с исключением «нарушения прав доступа». Тот факт, что ваша DLL не делает этих больших резервов при компиляции в режиме Release, предполагает, что, действительно, это может быть стратегия отладки. И это также предлагает лучший способ определения источника: вместо того, чтобы сканировать ваш код в поисках всех процедур распределения памяти, просканируйте свой код в поисках условного кода, который зависит от конфигурации сборки. Если вы делаете что-то другое, когда определены DEBUG или _DEBUG, то, вероятно, именно здесь происходит волшебство.

Другое возможное объяснение - это реализация malloc () или new в CRT. Когда вы выделяете небольшой кусок памяти (скажем, несколько КБ), CRT фактически резервирует гораздо больший блок, но фиксирует только кусок запрошенного размера. Когда вы впоследствии освободите / удалите этот небольшой кусок памяти, он будет списан, но больший блок не будет возвращен обратно в ОС. Причина в том, чтобы позволить будущим вызовам malloc / new повторно использовать этот зарезервированный блок памяти. Если последующий запрос предназначен для блока большего размера, чем может быть удовлетворено текущим зарезервированным адресным пространством, он резервирует дополнительное адресное пространство. Если при отладке сборок вы постоянно выделяете и освобождаете все большие блоки памяти, то, что вы видите, может быть результатом фрагментации памяти. Но на самом деле это не проблема, если не считать незначительного снижения производительности, о котором действительно не стоит беспокоиться при отладке сборок.

person Cody Gray    schedule 30.11.2016
comment
Невероятно четкое объяснение, большое спасибо. У меня последний вопрос: можно ли сказать, что большой объем зарезервированной памяти не является утечкой памяти? Или в основном это не должно быть утечкой памяти. А есть ли понятие утечки зарезервированной памяти? - person MichaelYi; 30.11.2016
comment
Утечка памяти - это когда вы теряете возможность освобождать выделенную память. Независимо от того, выделена ли эта память или зарезервирована, если вы потеряете возможность разблокировать или удалить ее, у вас будет утечка. Теперь операционная система будет очищать ваш процесс после того, как он существует, поэтому на практике утечка имеет значение только на время жизни приложения. Но это все равно утечка памяти, если вы зарезервируете кучу памяти, а затем никогда не сможете ее освободить. Я не уверен, почему бы этого не было. @Майкл - person Cody Gray; 01.12.2016
comment
То, что я говорю выше в последних абзацах, заключается в том, что есть причины, по которым у вас может быть много зарезервированной памяти, но при этом не будет утечки памяти в вашем коде. Одна из наиболее вероятных причин - это реализация процедур распределения памяти библиотеки времени выполнения C. На самом деле они не пропускают эту память, потому что они все еще сохраняют способность высвободить ее, если захотят, они просто не запрограммированы на это. - person Cody Gray; 01.12.2016