среда, 26 ноября 2008 г.

Ловушка для меморилика

Не знаю как люди ловаят мемориликов. Пробовал google-perftools, что-то ничего не говорит даже на примитивный пример с единственным malloc. valgrind можно было бы попробовать прикрутить, но это связано с определенными трудностями, поскольку меморилик затаился в сервере c замкнутой программной средой на базе FreeBSD.

К ловле меморилика надо подходить творчески.

Заменить все вызовы malloc на debug_malloc, а потом вглядываться в исписанный экран в поисках утечки - это не наш метод. Хотя при должном уровне автоматизации он тоже должен дать плоды, но такой метод обычно требует модификации кода, и поэтому не является предпочтительным.

Что касается должного уровня автоматизации, я считаю что мало вывести на экран информацию. Гораздо лучше, если эту информацию предварительно обработать. Что же мы можем сделать в случае выделения/освобождения памяти?

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

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

Некоторая проблема есть в том, что работать придется практически на системном уровне. Для реализации мы пишем свои версии malloc, free, realloc, strdup, ::operator new и других, оперирующие с памятью функций. в начале каждого обработчика определяем точку вызова. Но возможности вызвать функции из стандартных библиотек у нас уже нету.

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

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

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

11 коммент.:

Yuri Volkov комментирует...

заинтриговали, признаюсь =)

Андрей Валяев комментирует...

Можно и на ты. :)

Как я не люблю фрю... кроме того времени не хватает, пока еще инпытываю проблемы с многопоточностью, на сервере пока так и не работает. Но есть мысли как их преодолеть...

Зато на однопоточных приложениях уже вполне успешно заработало. :) Точно показывает на каком malloc'е утечка.

Yuri Volkov комментирует...

мне как-то довелось использовать valgrind, но то была однопоточная программа и утечка нашлась достаточно быстро

Omega комментирует...

Интересно! А вот это не задолбает заменять?
>>свои версии malloc, free, realloc, strdup, ::operator new и других, оперирующие с памятью функций.

Или речь о том чтобы их #define'ами перекрыть? (или как либо еще подменить их _все_ в коде)

Андрей Валяев комментирует...

Любые define предполагают необходимость добавления в каждый исходник дополнительного include. Что как-бы не очень хорошо.

я просто определяю свои malloc/free/и тд, линкую их раньше библиотек и используются мои а не стандартные версии.

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

Функций работающих с памятью сравнительно не много.

Omega комментирует...

>>Любые define предполагают необходимость добавления в каждый исходник дополнительного include. Что как-бы не очень хорошо.

Это скорее вопрос как код организован, но не суть - главное, что предоплагается замена _всех_ вызовов в коде.

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

Андрей Валяев комментирует...

Нельзя ли сразу после завершения программы показать какие блоки имеют не нулевой счетчик и из какой точки вызова их выделили?

2omega: это возможно, если программа прорабатывает и выключается. А это приложение (их несколько в контексте проекта) - это сервер, который по идее работает непрерывно. А если выключается, то вся система отправляется в перезагрузку и можно уже не успеть проанализировать результаты.

Уже дописываю вторую статью... вероятно их будет всего 4.. тема оказалась достаточно глубокой. :)

Omega комментирует...

Логи не предлагать? ;-) шучу.

смысл ясен, теперь меня тоже заинтриговали :) (да-да, до жирафов доходит медленнее :)

Yuri Volkov комментирует...

Любые define предполагают необходимость добавления в каждый исходник дополнительного include. Что как-бы не очень хорошо

=) макросы можно и в командной строке определять при компиляции

Андрей Валяев комментирует...

=) макросы можно и в командной строке определять при компиляции

Это конечно поможет в случае си, но мало поможет в случае C++ new.
Всеравно придется переопределять, зачем же тогда с макросами возиться дополнительно? Кроме того у макроса покрытие меньше. :)

Анонимный комментирует...

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

+1

Заменить все вызовы malloc на debug_malloc, а потом вглядываться в исписанный экран в поисках утечки - это не наш метод.
Т.е. помимо debug output на экране еще выводится как-либо инфа? В другую консоль нельзя выводить? Или, если есть gui, в top-level window? Тогда можно использовать гистограмму или еще что...

я просто определяю свои malloc/free/и тд, линкую их раньше библиотек и используются мои а не стандартные версии.
Это поможет и при использованных скомпилированных библиотек.