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

Большинство статей, посвященных проблемам утечек ресурсов, ориентировано главным образом на программистов, имеющих в своем распоряжении исходные коды и обширный набор различных диагностических утилит: от штатных отладчиков, входящих в комплект поставки компилятора, до специализированных анализаторов типа IBM Rational Purify, BoundsChecker или Valgrind.

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

Мы же люди простые. Администраторы мелкокорпоративных, офисных или даже домашних серверов, работающих, как правило, на основе NT-based систем. Исходных текстов у нас нет, да и времени/средств на исправление чужих ошибок - тоже. Тем не менее, бороться с утечками все же приходится. Кому не случалось перегружать зависший сервер, не реагирующий даже на <Ctrl-Alt-Del>, и давить на Reset с угрозой разрушения дискового тома и потери кучи оперативных данных?

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

 

Классификация утечек и причины их возникновения

Прежде чем бороться с утечками, необходимо разобраться, что это вообще такое и почему это происходит. Куда утекает память? Риторический вопрос! Никуда она не утекает, просто неудачный термин. Правильнее говорить об «отложении» или «пластовании» ресурсов, по аналогии с осадочными слоями. Рассмотрим следующий (вполне классический) пример:

foo(char *x)
{
// выделяем буфер из динамической памяти (также называемой кучей)
char *p = malloc(MAX_SIZE);

// если строка не влезает в буфер, возвращаемся из функции
if (strlen(x) >= MAX_SIZE)
return ERR_STR_TOO_LONG;

// копируем строку в буфер
strcpy(p, x);

// делаем с ней что-нибудь полезное

// освобождаем выделенную память
free(p);
return OK;
}

Программист выделяет буфер под копируемую строку и, прежде чем начать копирование, заботливо проверяет ее длину. Если строка не помещается в буфер, происходит немедленный возврат из функции с сообщением об ошибке, но! Выделенная память не освобождается! И не освободится никогда! Лишь при завершении процесса система автоматически освободит все, что к тому времени он успел понавыделять. Принимая в рассмотрение то, что серверные приложения не перезапускаются месяцами (и даже годами), становится ясно: утечки представляют собой едва ли не основную проблему, и даже потеря одного байта в долговременной перспективе выливается в сотни мегабайт «осадочной» памяти.

При этом от разработчиков серверных приложений автору постоянно приходится слышать, что, мол, проблема утечек фундаментальна и что если сервер теряет не более 1 Кб памяти в секунду, это вполне нормально. Количество установленной физической памяти не играет никакой роли, и падение производительности за счет утечек практически полностью нивелируется тем фактом, что операционная система вытесняет неиспользованные страницы на диск в файл подкачки. Однако адресное пространство процесса небезгранично и на 32-битных платформах по умолчанию составляет чуть менее 2 Гб (остальные 2 Гб занимают ядро ОС, ядерные структуры данных, драйвера и т.д.).

Легко рассчитать, что если память утекает со скоростью 1 Кб в секунду, то адресное пространство будет полностью исчерпано за 25 дней, а на самом деле намного раньше, поскольку, помимо динамической памяти, в обозначенные 2 Гб входят стек, образы исполняемых файлов и библиотек, структуры данных операционной системы прикладного режима и т.д. Для рабочей станции функционировать в течение месяца без перезагрузок — слегка противоестественно, а вот для серверов это вполне нормальное состояние, но, чтобы они не грохнулись раньше времени, необходимо преодолеть утечки.

Утечки делятся на две категории: жесткие (hard) и мягкие (soft). Мягкие утечки (также называемые локальными) действуют только в течение определенного периода времени, а затем возвращают «награбленные» ресурсы в общий пул. Вот, например, некоторый сервер обрабатывает запросы пользователей в отдельном потоке и под каждый запрос выделяет определенное количество памяти, но не освобождает ее после завершения обработки запроса, однако при отключении клиента вся память освобождается одним махом. Вот это и называется локальной утечкой.

Жесткие (или глобальные) утечки не освобождаются, пока администратор не отправит сервер в shutdown или не перезагрузит ОС. Последний момент очень важен! Если приложение выделяет блоки совместно используемой памяти (shared memory), то они не освобождаются вместе с завершением выделившего их процесса и продолжают болтаться в адресном пространстве вплоть до полной перезагрузки.

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

 

Утечка ресурсов как направленная атака

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

Все дело в том, что существует целый подкласс DoS-атак, вызывающих отказ в обслуживании путем генерации запросов, приводящих к утечкам памяти. Вернемся к фрагменту исходного кода, демонстрирующего утечку памяти. Допустим, что процедура foo() обрабатывает поля некоторого заголовка, причем длина строки MAX_SIZE выбрана программистом с большим запасом, так что нормальные запросы обрабатываются без каких-либо проблем. Но вот коварные хакеры находят ошибку в коде и начинают бомбардировать сервер строками невероятной длины. И хотя это не приводит к немедленному отказу, количество свободной памяти постепенно уменьшается вплоть до полного исчерпания кучи.

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

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

 

Схватка с утечками в рукопашную

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

Допустим, у нас есть k серверных приложений, и они порождают n процессов (их легко посчитать в диспетчере задач). Поскольку на 32-битных платформах каждый процесс владеет 4 Гб оперативной памяти, нам потребуется 4*(MAX(k, n) Гб памяти и еще пара гигабайт под системные нужды. Однако при изменении размера файла подкачки через графический интерфейс («Мой компьютер -> Свойства системы -> Дополнительно -> Параметры быстродействия -> Виртуальная память -> Изменить») мы ограничены четырехразрядным полем в мегабайтах, то есть не можем получить более 10 Гб виртуальной памяти. Для большинства нужд этого более чем достаточно, однако для серверов с многодневным аптаймом, на которых установлена куча
серверных приложений, возможно, потребуется и больший объем. Установить его поможет бесплатная утилита pagefileconfig.vbs.

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

Операционные системы Windows XP Professional, NT Server 4.0 Enterprise Edition, W2K Advanced Server, W2K Datacenter Server, Server 2003/Enterprise Edition/Datacenter Edition при загрузке поддерживают специальный ключ ‘/3G’, с помощью которого можно ужать систему до 1 Гб и выделить высвободившееся место в личное пользование каждого процесса, то есть размер кучи возрастает до 3 Гб (ну или чуть меньше за счет стека, образа исполняемого файла и динамических библиотек). Подробнее об этом можно прочитать на support.microsoft.com/kb/823440, а ниже приводится пример готового файла boot.ini,
приготовленного по этой технологии:

[Boot Loader]
Timeout=30
Default=multi(0)disk(0)rdisk(0)partition(2)\WINNT

[Operating Systems]
multi(0)disk(0)rdisk(0)partition(2)\WINNT="Windows Server 2003" /3GB

 

Перезагрузка приложений

Если планируется использовать сервер в полностью автономном режиме длительное время (например, ты уезжаешь в отпуск, оставляя домашний компьютер с ftp-архивом предоставленным самому себе), то тогда потребуются намного более радикальные меры борьбы с утечками. А именно - периодический перезапуск серверных приложений командой kill.exe (входит в бесплатно распространяемый набор Microsoft Debugging Tools, Support Tools, а также в Microsoft Platform SDK), закинутой в системный планировщик (смотри описание штатной команды at).

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

С серверами, реализованными как системные службы, дела в некотором смысле обстоят намного лучше, поскольку всякая служба обязана (по условиям спецификации) поддерживать мягкую перезагрузку без потерь оперативных данных. Однако далеко не всякая мягкая перезагрузка возвращает «осадочную» память, к тому же источником утечек вполне может оказаться и головной процесс SERVICES.EXE, которым «крышуются» все службы.

System Process (0)
System (8)
SMSS.EXE (232)
CSRSS.EXE (260)
WINLOGON.EXE (280) NetDDE Agent
SERVICES.EXE (308)
svchost.exe (480)
DLLHOST.EXE (1048)
Smc.exe (504) Sygate Personal Firewall
ups.exe (536)
svchost.exe (568) MCI command handling window
vmware-authd.ex (1240)

Попытка «убийства» SERVICES.EXE либо закончится сообщением о невозможности совершения такой операции, либо все-таки увенчается успехом, и тогда система тут же обрушится. Вот так ситуация!

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

Функция VirtualQueryEx принимает на грудь дескриптор процесса и возвращает следующие данные:

typedef struct _MEMORY_BASIC_INFORMATION {
// базовый адрес региона
PVOID BaseAddress;
// базовый адрес выделенного блока памяти
PVOID AllocationBase;
// «первородные» атрибуты защиты
DWORD AllocationProtect;
// размер региона в байтах
DWORD RegionSize;
// тип региона (выделен, закреплен, свободен)
DWORD State;
// текущие атрибуты защиты
DWORD Protect;
// тип страниц памяти
DWORD Type;
} MEMORY_BASIC_INFORMATION;

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

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

 

Принудительное освобождение памяти

А вот не хотим мы перезапускать ни серверное приложение, ни саму операционную систему. Не хотим и все! Что тогда? Вот тогда-то нам и пригодится весьма продвинутая методика, дающая неплохой результат, хотя и без всяких гарантий. Анализ большого количества программ, страдающих хроническими утечками памяти, показал, что указатели на блоки динамической памяти, как правило, помещаются в локальные стековые переменные, автоматически уничтожаемые компилятором при выходе из функции. Следовательно, если на данный блок динамической памяти не ссылаются ни другие блоки, ни локальные переменные, то его можно считать с высокой степенью вероятности «потерянным» и с некоторым риском освободить,
возвращая память обратно в кучу.

Подобный «сборщик мусора» представляет собой довольно сложную программу, вынужденную учитывать многие нюансы. У мыщъх'а пока что имеется pre-alpha версия, предназначенная для сугубо внутреннего использования.

Как она работает? Вместо того чтобы определять границы стека каждого из потоков, мыщъх просто сканирует адресное пространство процесса (естественно, исключая невыделенные блоки), выцеживая 32-битные значения, похожие на указатели. Похожие - это находящиеся в пределах динамических блоков памяти, полный перечень которых можно получить посредством следующих API-функций: CreateToolhelp32Snapshot\Heap32First\ Heap32ListFirst\Heap32ListNext\Heap32Next.

Занятые блоки динамической памяти, в границах которых нет ни одного указателя, считаются «осадочными» и освобождаются. А вот как они освобождаются — это уже вопрос. Можно, конечно, вызывать API-функцию VirtualFreeEx, но! Компиляторы работают с динамической памятью не напрямую, а посредством своих собственных библиотек времени исполнения (Runtime Library, или сокращенно RTL). Любая работа с динамической памятью в обход RTL-менеджера неминуемо приводит к краху приложения. Поэтому мы должны впрыснуть свой код в подопытный процесс и вызывать RTL-функцию освобождения памяти. Например, в языке Си это функция free().

Имеются, естественно, и другие трудности, но их обсуждение выходит за рамки этой статьи. Главное, что освобождение «потерянной» памяти все-таки возможно!

 

Заключение

Мыщъх предложил несколько достаточно эффективных методов борьбы с утечками памяти, опробованных как на домашнем сервере, так и на серверах ряда мелких предприятий. И хотя до «промышленного» внедрения этим методикам еще далеко, они работают. Мыщъх продолжает рыть землю в этом направлении, разрабатывая полностью автоматизированный «сборщик мусора», ориентированный на откомпилированные программы без исходных текстов. Желание принять участие в проекте всячески приветствуется. В общем, дорогу осилит идущий!


Полную версию статьи
читай в сентябрьском номере
Хакера!

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии