среда, 3 декабря 2008 г.

Ловушка для меморилика (слайды)

Струя воды толщиной в спичку
дает утечку двести литров в сутки...
(c) Афоня



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

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

Но обо всем попорядку. Как же это работает?

Сразу отмечу, что данный код работает только под FreeBSD 32bit, возможно он сможет работать и под Linux - не проверял, но точно не заработает на 64-х битной платформе. Кроме того весь код приводить тоже не буду, может быть выложу куда нибудь.

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

extern "C" void *malloc (size_t size);
extern "C" void *calloc (size_t number, size_t size);
extern "C" void *realloc (void *ptr, size_t size);
extern "C" void *reallocf (void *ptr, size_t size);
extern "C" void free (void *ptr);
extern "C" char *strdup(const char *str);
void *operator new(size_t size);
void operator delete (void *ptr) throw();
void *operator new[] (unsigned int size);
void operator delete[] (void *ptr) throw();

Каждая из этих функций представляет из себя обертку к медленному и простому хип менеджеру.
Например:

extern "C"
void *malloc (size_t size)
{
return salloc (size);
}

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

static
uint32_t getCallPoint(const void *stack)
{
const uint32_t *sptr = reinterpret_cast(stack);
return sptr[-1];

}

extern "C"
void *malloc (size_t size)
{
const uint32_t cp = getCallPoint(&size);
return salloc (size, cp);
}

Вот так будет достаточно. Что же касается менеджера памяти, то переопределив malloc во FreeBSD мы потеряли всякую возможность воспользоваться стандартным, и вынуждены писать свой. Но так даже лучше, в четвертой статье расскажу почему.

Релизация у него будет предельно проста. Заводится статический буффер большого размера. Определяется структура заголовка блока, которая состоит из двух полей: cp - точка выделения блока, и size - размер блока. Если cp имеет нулевое значение - это обозначает, что блок пуст.

И для учета блоков по точкам вызова мы заводим массив структур:

struct callpoint {
uint32_t cp;
uint32_t current_blocks;
uint32_t max_blocks;
uint32_t size;
};

static struct callpoint scps[cnum];

Размеры хипа и таблицы точек вызова я выбрал вроде бы с запасом, 16 мегабайт и 4096 точек. При выделении каждого блока я нахожу соответствующую точке запись и увеличиваю счетчик блоков. Если current_blocks превышает max_blocks, это может быть утечка. Для понижения уровня шума max_blocks можно проинициализировать некоторым значением - у меня 100. То есть до 100 блоков на точку - ругани не будет, дальше будет. И при каждом повышении количества блоков ругань будет продолжаться.

При освобождении блока я извлекаю точку вызова из информации блока, ищу эту точку в таблице и понижаю счетчик current_blocks.

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

6 коммент.:

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

Что же касается менеджера памяти, то переопределив malloc во FreeBSD мы потеряли всякую возможность воспользоваться стандартным, и вынуждены писать свой.
Всегда есть возможность написать что-нибудь типа
#ifdef _DEBUG
# define malloc __dbg_malloc
#endif
Когда нужен стандартный менеджер -
#undef malloc

Читаю я все это и понимаю, как много надо программерам делать, когда нет некоторых элементарных вещей... и как хорошо, что в MSVC есть crtdbg.h :-) Сами его используем.

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

Конечно, через define мы можем переопределить malloc, хотя и это не универсальное решение...

Но как быть с new/delete? Их не так то просто заменить на имена своих функций...

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

Почему не универсальное? Нормальная имхо...

А с new/delete - просто определяешь в глобальном пространстве имен
void* operator new( size_t n )
{
return malloc( n );
}
void operator delete( void *p )
{
free( p );
}
// то же и для operator new[], ...
Или я что-то недопонял?

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

2coff: Это конечно сработает. Но есть одно но...

Для этого еще требуется перекомпиляция проекта. И всех прилегающих к нему библиотек.

И __dbg_malloc всеравно надо реализовывать или взять готовый у кого нибудь. Там зачем изобретать велосипед с #define, если всеравно необходимо линковаться с отладочным менеджером хипа?

Можно конечно обложить весь код дефайнами, ради теста (sic!), только смысл?

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

Да, на самом деле, если надо писать свой менеджер кучи, то в принципе это может сработать. Нормальный вариант.

А strdup наверняка тоже использует malloc...

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

LD_PRELOAD рулит пожизни