C++ Stack, идущий на Windows
Я создаю менеджер памяти для C++, используя очень стиль.NET. При этом мне нужно знать, какие объекты считаются достижимыми; и объект считается достижимым, если достижимый объект имеет дескриптор рассматриваемого объекта. Таким образом, это ставит вопрос о том, какой объект (ы) является корнем нашего поиска? Ответ мог бы состоять в том, что эти объекты "eve" находятся в стеке, будь то в форме дескриптора управляемого объекта или экземпляра локального объекта области действия, который сам имеет дескриптор управляемого объекта.
Я прочитал несколько статей на эту тему, а также ознакомился с подробностями реализации MSDN о методе StackWalk в Win32 API.
Как всегда, любая помощь очень ценится. И, пожалуйста, не советуйте создавать менеджер памяти и не предлагайте альтернативы, такие как умные указатели. Я полностью понимаю, что я делаю. Спасибо!
1 ответ
Ваши требования вроде бы похожи на небольшой проект, над которым я сейчас работаю, но моя цель не в том, чтобы создать менеджер памяти, а в том, чтобы инструментально использовать dmalloc (и долгосрочное приложение в режиме отладки, в котором оно работает. выполняется с возможностью периодически останавливать выполнение и сканировать память в поисках выделений кучи, на которые нет ссылок. Вроде как "тупой" сборщик мусора, но не с целью освобождения памяти; вместо этого, с целью регистрации утечек выделенных ресурсов для последующего анализа (наряду со следами стека, захваченными во время выделения, которые я уже добавил в dmalloc). Обратите внимание, что, как сборщик мусора диспетчера памяти общего назначения, этот процесс будет довольно неэффективным и займет "много времени" (я еще не закончил, но я не удивлюсь, если он будет запускаться каждый раз) приостанавливает нормальное выполнение программы более чем на 10 секунд), но для моих собственных целей меня не сильно заботит производительность, потому что я буду включать ее только один раз в несколько месяцев для проверки новых утечек памяти в продукте моей компании.
В любом случае, я предполагаю, что ваш менеджер памяти будет единственным источником кучи памяти в вашем приложении? И что потоки в вашей системе работают в среде с полностью разделяемой памятью, где ни у какого потока нет памяти, включая пространство стека и пространство локального хранилища, которое не видно из других потоков? Если так...
Я полагаю, что есть только четыре категории памяти, в которых вы можете найти указатели для распределения кучи:
- На стеках вызовов каждого потока
- В пределах кучи выделений сами
- В статически распределенной доступной для записи памяти (.bss & .data/.sdata, но не.rdata/.rodata)
- В локальном потоке памяти для каждого потока
Вы уже знаете, что в стеке могут возникать указатели на выделение кучи. Указатели на выделения также могут (вместо этого) храниться в самих объектах кучи и даже не храниться в стеке. Ваш вопрос предполагает, что вы можете надеяться использовать стек в качестве "корня" поиска вашего сборщика мусора; Я полагаю, что это означает, что вы надеетесь, что сможете следовать указателям в стеке за пределы в другие выделения, искать от одного объекта к другому по памяти, пока вы не пройдете все объекты в памяти и не найдете все указатели на все распределения. "Корневые" указатели могут также существовать в статически размещенных объектах, на которые можно ссылаться напрямую, даже если указатель на такой объект в стеке отсутствует, поэтому нельзя просто предполагать, что все выделения достижимы из "указателей", которые вы найдете в стек. Кроме того, к сожалению, в C++, если вы не в состоянии узнать структуру каждого размещения (что не получится без помощи компилятора), вам придется предполагать, что любое местоположение, возможно, является указателем. Поэтому вам придется сканировать каждую из этих четырех категорий памяти в поисках потенциальных указателей на все существующие распределения, помечая каждую из них флагом "возможно, все еще используется", если вы найдете в памяти значение, соответствующее адресу выделения, действительно ли это указатель. Когда вы сканируете память, в каждом байтовом местоположении (или в каждом байтовом местоположении, равномерно делимом на sizeof(void*), если вы знаете, что ваша платформа не может иметь указатели на смещенные адреса), вам придется искать свой список распределений чтобы увидеть, есть ли это значение в вашем списке распределений.
Поскольку вы уверены, что знаете, что делаете, ваш диспетчер памяти, вероятно, отслеживает эти распределения в сбалансированной древовидной структуре (возможно, в красно-черном дереве или дереве Андерссона), которая дает вам O(log n) вставку и поиск на эти распределения, но константа пропорциональности для навигации по этим деревьям действительно убьет производительность вашего сборщика мусора. Перед выполнением сканирования сборки мусора вы захотите скопировать указатели размещения дерева в плоский непрерывный буфер (т. Е. "Массив") по порядку (т. Е. По возрастанию или по убыванию с использованием обхода inorder). Я предлагаю массив void*
адреса каждого выделения и отдельного битового массива (не bool
массив) с одним битом на выделение, инициализированный для всех нулей, где соответствующий бит выделения установлен в 1, если вы обнаружите потенциальную ссылку на него. Это все равно даст вам O(log n) поиска (используя бинарный поиск), пока вы сканируете сборщик мусора, но с гораздо более управляемой константой пропорциональности для ваших поисков; Кроме того, эта более компактная структура данных будет иметь лучшую производительность по кэшу, чем сбалансированное дерево.
Теперь я расскажу о каждой из трех категорий памяти, которые вам придется сканировать:
- CallStacks каждого потока
Для этого вам нужно будет запросить у вашего менеджера потоков верх и низ стеков каждого потока. Если вы можете получить только текущий указатель стека для каждого потока, то вы можете использовать API "backtrace" для получения списка адресов возврата функций в этом стеке. Исходя из этого, вы можете отсканировать назад к базе каждого стека (чего вы не знаете), отмечая каждый обратный адрес галочкой, пока не дойдете до последнего обратного адреса, где вы затем нашли базу стека (или достаточно близко), Что касается "текущего потока", убедитесь, что он не содержит никаких стековых кадров, связанных с вашим менеджером памяти; то есть, сделайте резервную копию нескольких стековых фреймов и проигнорируйте те, которые связаны с вашим сборщиком мусора, иначе вы можете найти адреса утечек распределений в локальных переменных вашего сборщика мусора и принять их за
- В пределах кучи выделений сами
Объекты кучи могут ссылаться друг на друга, и у вас может быть сеть пропущенных объектов, которые все ссылаются друг на друга еще как группа, они пропущены. Вы не хотите видеть их указатели друг на друга и относиться к ним как к "используемым", поэтому вы должны обращаться с ними осторожно... и в заключение. Как только все другие категории будут закончены, вы можете свернуть / разделить ваш плоский массив void*
адреса распределения, составление отдельного списка "рассмотренных в использовании" распределений и "еще не проверенных" распределений. Просматривайте "рассмотренные в использовании" распределения в поисках потенциальных указателей на распределения, которые еще находятся в "еще не проверенном" списке. По мере их обнаружения переместите их из списка "еще не проверено" в конец списка "рассматриваемых в использовании", чтобы в конечном итоге вы их также сканировали.
- В статически распределенной доступной для записи памяти (.bss & .data/.sdata, но не.rdata/.rodata)
Для этого вам нужно получить символы от вашего компоновщика до начала и конца (или длины) каждого из этих разделов. Если такие символы еще не существуют или вы не можете получить эту информацию из API платформы, вам необходимо получить командный сценарий компоновщика (скрипт компоновщика) и изменить его, чтобы добавить и инициализировать глобальные символы для начального адреса и конца. адрес (или длина) каждого из этих разделов. Раздел.bss содержит неинициализированные глобальные, файловые области и статические члены класса. Разделы.data /.sdata содержат неконстантные предварительно инициализированные глобальные, файловые области и статические члены класса. Вам не нужно беспокоиться о разделах.rdata /.rodata, потому что ваша программа не будет записывать адреса распределения кучи в статические данные const.
- В локальном потоке памяти для каждого потока
Для этого вам нужно будет иметь возможность запросить у вашего менеджера потоков локальное хранилище для каждого потока, иначе часть запуска каждого потока должна добавить его локальное хранилище в список потоков. локальное пространство для приложения и удалите его при выходе из потока.
Если вы все еще на борту и хотите сделать это, к настоящему времени вы, вероятно, поняли, что это более крупный проект, чем вы могли подумать. Дайте мне знать, как это происходит!