Введение

Методология управления памятью компьютера

Управление памятью в виртуальном адресном пространстве

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

Ручное управление памятью

Задача выполнения запроса на выделение памяти состоит в поиске блока неиспользуемой памяти достаточного размера. Запросы памяти удовлетворяются путем выделения порций из большого пула памяти, называемого кучей (heap). Важно не путать кучу с одноименной структурой данных. В некоторых операционных системах, например OS/360, свободное хранилище может быть разделено различными способами, например, подпулы в OS/360, ниже линии, выше линии и выше планки в z/OS. В любой момент времени часть кучи занята, а часть – "свободна" (не используется) и, следовательно, доступна для будущих выделений. В языке C функция для выделения памяти из кучи называется, а функция для освобождения ранее выделенной памяти и пометки ее как "свободной" (для использования в будущих выделениях) называется. Упрощенная реализация этих двух функций приведена в статье "Внутреннее устройство управления памятью". Реализация осложняется рядом проблем, таких как внешняя фрагментация, возникающая при наличии множества небольших промежутков между выделенными блоками памяти, что делает их непригодными для удовлетворения запроса на выделение. Метаданные распределителя также могут увеличивать размер (отдельных) небольших выделений. Обычно это решается с помощью разбиения на чанки. Система управления памятью должна отслеживать текущие выделения, чтобы гарантировать отсутствие перекрытий и предотвратить "потерю" памяти (то есть "утечки памяти").

Эффективность

Специфический алгоритм динамического выделения памяти может существенно влиять на производительность. Исследование, проведенное в 1994 году компанией Digital Equipment Corporation, демонстрирует накладные расходы, связанные с использованием различных аллокаторов. Минимальная средняя длина траектории выполнения инструкций, необходимая для выделения одного блока памяти, составила 52 (измерено с помощью профилировщика на уровне инструкций на различных программных продуктах).

Реализация

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

Распределение блоков с фиксированным размером

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

Блоки друзей

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

Размещение в рамках Slab

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

Распределение стека

Многие Unix-подобные системы, а также Microsoft Windows реализуют функцию `alloca` для динамического выделения памяти в стеке, аналогично выделению памяти на куче. Компилятор обычно преобразует её в встроенные инструкции, манипулирующие указателем стека. Хотя нет необходимости вручную освобождать память, выделенную таким образом, поскольку она автоматически освобождается при возврате функции, вызвавшей `alloca`, существует риск переполнения стека. И поскольку `alloca` является расширением, реализованным во многих системах, но не включенным в POSIX или стандарт C, её поведение в случае переполнения стека не определено. Более безопасная версия `alloca`, называемая `_alloca`, сообщающая об ошибках, существует в Microsoft Windows. Для её использования требуется gnulib, которая предоставляет эквивалентный интерфейс, но вместо генерации исключения SEH при переполнении делегирует выделение памяти функции `malloc`, когда обнаруживается слишком большой размер. Аналогичную функциональность можно эмулировать с помощью ручного учёта и проверки размера, как, например, при использовании `alloca` в glibc.

Автоматическое управление памятью

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

Автоматическое управление переменными стека вызовов

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

Сбор мусора

Сборка мусора — это стратегия автоматического определения памяти, выделенной объектам, которые больше не используются в программе, и возврата этой выделенной памяти в пул свободных участков памяти. Этот метод противопоставляется "ручному" управлению памятью, при котором программист явно программирует запросы на выделение памяти и освобождение памяти в программе. Хотя автоматическая сборка мусора имеет преимущества в снижении рабочей нагрузки программиста и предотвращении определенных видов ошибок, связанных с выделением памяти, сборка мусора сама по себе требует ресурсов памяти и может конкурировать с прикладной программой за процессорное время.

Справочное подсчет

Счет ссылок — это стратегия определения, что память больше не используется программой, путем ведения счетчика количества независимых указателей, ссылающихся на эту память. Каждый раз, когда новый указатель начинает ссылаться на участок памяти, программист должен увеличивать счетчик. Когда указатель меняет цель, на которую он указывает, или когда он перестает указывать на что-либо либо сам освобождается, счетчик должен уменьшаться. Когда счетчик достигает нуля, память следует считать неиспользуемой и освобождать. Некоторые системы подсчета ссылок требуют участия программиста, а другие реализуются автоматически компилятором. Недостатком подсчета ссылок является возможность возникновения циклических ссылок, приводящих к утечке памяти. Это можно смягчить, добавив понятие «слабой ссылки» (ссылки, которая не участвует в подсчете ссылок, но получает уведомление, когда объект, на который она указывает, становится недействительным), или объединив подсчет ссылок и сборку мусора.

Пулы памяти

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

Системы с виртуальной памятью

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

Управление памятью в OS/360 и последующих версиях

IBM System/360 не поддерживает виртуальную память. За исключением модели 67, изоляция памяти заданий опционально достигается с использованием защитных ключей, при этом каждому заданию назначается различный ключ хранения, 0 для супервизора или 1–15. Управление памятью в OS/360 является функцией супервизора. Хранение запрашивается с помощью макроса GETMAIN и освобождается с помощью макроса FREEMAIN, что приводит к вызову супервизора (SVC) для выполнения операции. В OS/360 детали различаются в зависимости от способа генерации системы, например, для PCP, MFT, MVT. В OS/360 MVT субалокация в пределах региона задания или общей зоны системной очереди (SQA) основана на подпулах – областях, размер которых кратен 2 КБ, что соответствует размеру области, защищенной защитным ключом. Подпулы нумеруются от 0 до 255. В пределах региона подпулам назначается либо защита хранения задания, либо ключ супервизора, ключ 0. Подпулы 0–127 получают ключ задания. Изначально создается только подпул 0, и все пользовательские запросы на хранение удовлетворяются из подпула 0, если в запросе памяти не указан другой подпул. Подпулы 250–255 создаются запросами памяти от супервизора от имени задания. Большинству из них присваивается ключ 0, хотя некоторые получают ключ задания. Номера подпулов также имеют значение в MFT, хотя детали реализации гораздо проще. MFT использует фиксированные разделы, которые оператор может переопределять, вместо динамических регионов, а PCP имеет только один раздел. Каждый подпул отображается списком блоков управления, идентифицирующих выделенные и свободные блоки памяти в пределах подпула. Память выделяется путем поиска свободной области достаточного размера или путем выделения дополнительных блоков в подпуле до размера региона задания. Можно освободить всю или часть выделенной области памяти. Детали для OS/VS1 аналогичны деталям для MFT и MVT; детали для OS/VS2 аналогичны деталям для MVT, за исключением того, что размер страницы составляет 4 КБ. Для OS/VS1 и OS/VS2 общая зона системной очереди (SQA) не подлежит разбиению на страницы. В MVS адресное пространство включает в себя дополнительную совместно используемую область, подлежащую разбиению на страницы, – Общую зону хранения (CSA), и дополнительную частную область – область работы системы (SWA). Кроме того, ключи хранения 0–7 зарезервированы для использования привилегированным кодом.