Встроенные средства контроля ресурсов используемой оперативной памяти в приложении



Описывается достаточно простое в реализации программное средство контроля используемых ресурсов оперативной памяти в процессе выполнения приложения. Основу реализации составляет перехват и регистрация запросов на выделение, освобождение и повторное использование ресурсов памяти, направляемых приложением операционной системе через вызовы malloc(), calloc(), realloc(), free(). Все запросы памяти регистрируются в специальном журнале и по завершении приложения накопленная информация выводится в форме отчета на консоль или записывается в текстовый файл. Анализ отчета позволяет выявлять случаи неэффективного использования оперативной памяти в приложении. К таковым относятся “утечки” (memory leaks), когда запрошенные ресурсы памяти не освобождаются и не востребуются приложением, фрагментация, когда размеры освобожденных и доступных для повторного использования непрерывных участков памяти оказываются недостаточными для удовлетворения новых запросов, что приводит к выделению дополнительных ресурсов.

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

Введение


Значительная доля современных программных комплексов разрабатывается c использованием языков программирования C#, Java и им подобных, где управление ресурсами оперативной памяти выполняется на системном уровне с минимизацией ответственности разработчиков программного обеспечения. В качестве примера такого рода программных комплексов можно назвать систему сквозного автоматизированного проектирования электронной аппаратуры Delta Design, реализованную на C# в среде .net. С другой стороны, программные средства систем автоматизации обладают достаточно длительным “сроком жизни” (до нескольких десятков лет), что приводит к необходимости поддержания “устаревающих” технологий управления ресурсами оперативной памяти, в частности, при интеграции двух упомянутых классов систем в общую схему управления.

По этой причине до настоящего времени актуальной проблемой при создании, развитии и со-провождении функционально сложных программных приложений остается необходимость оптимизации управления в них ресурсами оперативной памяти. Наряду с эффективными методами (умные указатели, сборщики “мусора”) по-прежнему продолжают использоваться программные средства, выполняющие запросы ресурсов памяти через вызовы malloc, calloc, realloc, free.
Такой подход предоставляет разработчикам полный контроль по управлению оперативной памятью в приложении, одновременно возлагая на него высокий уровень ответственности за своевременное освобождение этих ресурсов во избежание “утечек”.

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

  • обнаруживается значительный разрыв между реальными размерами потребляемой оперативной памяти и оценками разработчиков;
  • наблюдается линейный рост используемой динамической памяти в процессе выполнения приложения.

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

  • несоответствие запросов на размещение и освобождение оперативной памяти (“утечки”);
  • низкий уровень повторного использования освобожденных участков оперативной памяти, что ведет к запросам новых участков (фрагментация).

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

Ниже представляется пример одной из возможных реализаций такого подхода.

Программные средства встроенного контроля


Основу представляемого решения составляет перехват средствами встроенного контроля всех запросов к ресурсам оперативной памяти, направляемым приложением операционной системе в процессе его исполнения. Все многообразие таких запросов осуществляется через вызовы malloc(), realloc() или free(). Пример последовательности событий при обработке запроса на вы-деление оперативной памяти malloc() при отключенном и включенном состояниях средств встроенного контроля представлен на диаграмме ниже (аналогичные диаграммы могут быть построены для realloc() или free() вызовов). Полоса “A” включает последовательность событий при выделении памяти, происходящих в отключенном состоянии режима встроенного контроля. Приложение направляет операционной системе запрос на выделение требуемого размера памяти и получает указатель на адрес начала выделенного фрагмента. В том случае, когда запрошенный размер памяти недоступен возвращается указатель с нулевым адресом, что соответствующим образом должно обрабатываться запрашивающим приложением.



Полоса “B” включает последовательность событий при выделении памяти, происходящих при включенном режиме встроенного контроля. Эта последовательность полностью совпадает с описанной ранее лишь с тем исключением, что после выделения запрошенной памяти управ-ление передается в функцию register_request(), которая выполняет сохранение информации об адресе и размере выделенной памяти по сделанному запросу.

Собственно перехват и регистрация запросов к ресурсам оперативной памяти выполняется объектом memSupervisor, который создается в единственном экземпляре посредством вызова init_memSupervisor() и который затем может переключаться во включенное или выключенное состояния через вызовы enable_memSupervisor() или disable_ memSupervisor() соответственно.
Техника перехвата запросов к ресурсам оперативной памяти основывается на использовании статических переменных GNU библиотеки как это показано в следующих фрагментах кода.





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



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

Генератор текстовых отчетов (memSupervisor.genReport(имя__файла)) формирует отчет по данным таблицы и записью информации в указанный текстовый файл (или выводом на консоль приложения). Загрузка отчетных данных в таблицу MS Excel и соответствующая настройка последнего позволит получить графическое представление данных отчета по использованию ресурсов оперативной памяти.

Интерфейс к средствам встроенного контроля


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



Ниже представлен фрагмент программного кода, демонстрирующего применение средств встроенного контроля.







Ниже представлена иллюстрация применения описанных средств встроенного контроля ресурсов оперативной памяти, запрашиваемых ‘foo’ приложением, выполняющим автоматическую трассировку соединений на печатных платах. Для анализа выбрана достаточно наглядная и удобная модель меандра, отображающего размеры запрошенной приложением динамической оперативной памяти в процессе выполнения им прокладки соединений на печатной плате.



Эта модель помогает выявить следующие проблемы управления ресурсами оперативной памя-ти:

  • в точках “A” и “D” должны быть примерно одинаковые размеры используемой оперативной памяти
  • рост размеров запрашиваемой памяти на этапе выполнения прокладки соединений дол-жен быть плавным. При необходимости, основные шаги этого этапа должны контролироваться путем установки контрольных точек в соответствующих фрагментах программного кода. Принудительное удаление всех данных о проложенных соединениях должно приводить к одинаковым размерам потребляемой памяти в точках “B” и “C”

Диаграмма ниже показывает проблемы управления ресурсами оперативной памяти в упомяну-том ‘foo’ приложении.



Краткие выводы


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

  • Средства встроенного контроля регистрируют и накапливают информацию о ресурсах запрошенной памяти с любой степенью точности и детализации;
  • Возможности переключения средств встроенного контроля между активным и неактивным режимами позволяют настраивать их на извлечение и анализ данных в выбранных фрагментах программного кода приложения;
  • Встроенные средства контроля допускают полное их исключение из приложения при сборке последнего без установки соответствующего параметра компиляции;
  • Реализация программного анализа содержания получаемых при исполнении приложе-ний отчетов об использованных ресурсах оперативной памяти и сверка последних с эталонными копиями позволяет разработать процедуры регрессионного тестирования приложений на предмет обнаружения в них деградации в использовании оперативной памяти;
  • Программный код такого рода средств встроенного контроля оперативной памяти от-крыт для расширения его возможностей и адаптации к особенностям условий его при-менения;
  • Основным недостатком описанных средств является необходимость включения кода встроенного контроля оперативной памяти в код контролируемого приложения.

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

Комментарии (7)


  1. staticmain
    25.08.2019 22:36
    +2

    В коде malloc увидел следующее:
    RestoreHooks
    Malloc
    Sethooks


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


    Звучит так себе, особенно когда оно будет регистрировать таким образом free, который пролетел мимо хука.


    Мы используем у себя два метода:


    1. Своя стандартная библиотека ( https://github.com/codemeow/bixi/tree/master/code/libbixi ), которая имеет возможность установить кастомный аллокатор, через который идут все функции (stdlib не используется).
    2. Ld_preload с библиотекой, реализующей кастомные аллокаторы, позволяет перехватывать даже stdlib.

    Описанный в статье метод нееффективен и непотокобезопасен.


    1. EREMEX
      26.08.2019 18:17

      Действительно представленное описание было ориентировано на использование средств встроенного контроля в контексте однопоточного исполнения. Но ничто не ограничивает в указанных функциях установить блокировки вида:

      #include
      #include
      #include
      #include
      ….
      std::mutex g_lock;
      void ProtectedFunction()
      {
      g_lock.lock ();
      //делаем в защищенном режиме.
      g_lock.unlock();
      }
      … и обрабатывать запросы к памяти в защищенном режиме.
      В отношении кастомного аллокатора памяти – нет вопросов, если выполняется разработка нового программного обеспечения. В тех же случаях, когда сопровождается и развивается приложение с глубокой “историей” и большим контингентом пользователей на такой риск вряд ли кто решится.
      В отношении высказанного (правда ничем не подтвержденного) утверждения о неэффективности описанного подхода можно лишь заметить, что последняя картинка в публикации относится к профилированию динамической памяти в очень сложном приложении, в котором на пятые сутки непрерывного исполнения обнаруживалась ситуация исчерпания ресурсов памяти с последующим аварийным завершением. Это простенькое средство контроля позволило выявить причины.
      Так что, не претендуя на универсальность это средство приносит пользу.


      1. staticmain
        26.08.2019 18:20

        Но ничто не ограничивает в указанных функциях установить блокировки вида:

        Вот только скорость работы такой штуки будет низкой. Выделение памяти — очень небыстрая операция, а вы это еще и только в одном потоке собираетесь делать.

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

        В таких случаях используется LD_PRELOAD и код «монстра» никак не модифицируется. Простейший пример можно посмотреть например тут: github.com/codemeow/c-heetah#call-as-library


        1. EREMEX
          26.08.2019 18:50

          Возможно я не все понял, но два вопроса все-таки возникают.
          1) при выявлении причин нехватки памяти время исполнения не столь критично в этом режиме (собственно алгоритмы берут на 2-3 порядка больше времени). Важны результаты, а именно: какая память в приложении не освобождается и накапливается.
          2) как я понимаю предлагаемый аллокатор контролирует и включает в отчет утечки памяти и пр.? Если это так и он внешний, то какой размер отчета я получаю по всему приложению? А это не требуется. В подавляющем большинстве случаев известен один (или несколько) исполняемых потоков операций (not threading), где необходимо искать проблему. Поэтому локальные вставки макросов и дают урезанный лог. Конечно, вопрос спорный. Но вроде как такая практика приносит свои положительные результаты наряду с высказанными вами негативными сторонами: замедление, инструментовка исходников для извлечения данных о памяти


  1. Antervis
    26.08.2019 00:40

    а можно же просто переопределить глобальные operator new/delete...?

    По идее, в с++ коде не должно быть выделений памяти через malloc


    1. OldFisher
      26.08.2019 06:32

      В C++ коде не должно, а в статических библиотеках new/delete уже не переопределишь. Ну и если часть кода — древнее legacy, то ему может быть не так уж и важно, кто кому чего должен.


    1. EREMEX
      27.08.2019 10:23

      Если вернуться к показанным в публикации фрагментам кода из исходника memorysupervisor.cpp можно увидеть, что показанные там new операторы перехватываются. Как я понимаю это результат того, что их реализация выполнена через malloc/calloc. К сожалению не представил исполнение delete оператора. Но имею подозрения, что его реализация базируется на использовании вызова free(). Поэтому установление крючков — перехватчиков к malloc/calloc/realloc/free обеспечивает перехват и запросов по new/delete.