07. Управление памятью

Управление памятью

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

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

Отображение виртуальной памяти на физическую

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

Трансляция адресов производится процессором при помощи встроенного модуля управления памятью (memory management unit, MMU).

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

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

Существует два основных метода реализации виртуальной памяти:

  • Сегментация или сегментная адресация (Memory Segmentation)
  • Подкачка страниц (Memory Paging)

При этом, эти методы могут комбинироваться.

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

При реализации виртуальной памяти при помощи сегментной адресации, операции производятся над цельными сегментами памяти.

При реализации методом подкачки страниц, каждому сегменту (или всему адресному пространству, в отсутствие сегментации) присваивается “таблица страниц”, которая содержит в себе отображения виртуальных адресов на реальные. При обращении к адресу, процессор находит страницу, к которой относится это обращение и соответсвтующим образом транслирует адреса. Страницы обычно значительно меньше, чем сегменты, (часто 4096 байт) что значительно ускоряет их подкачку (выгрузку и загрузку). Многие современные процессоры так же поддерживают “огромные страницы” – от 8 КиБ до 16 ГиБ, в зависимости от архитектуры. x86 поддерживает страницы размером 4КиБ и “огромные страницы” размером 4МиБ (или 2 МиБ в режиме расширения физических адресов, позволяющем адресовать более 4 ГиБ оперативной памяти). x86-64 поддерживает “огромные страницы” размером 2 МиБ и, в некоторых реализациях, 1 ГиБ. Использование “огромных страниц” позволяет уменьшать объем таблицы страниц и ускорять поиск в ней.

Существуют методы определения размера страницы в популярных ОС. В частности, в POSIX-совместимых ОС (Unix-подобных):

#include <stdio.h>
#include <unistd.h> /* sysconf(3) */

int main(void) {
	printf("The page size for this system is %ld bytes.\n",
	       sysconf(_SC_PAGESIZE)); /* _SC_PAGE_SIZE is OK too. */

	return 0;
}

Или в Windows:

#include <stdio.h>
#include <windows.h>

int main(void) {
	SYSTEM_INFO si;
	GetSystemInfo(&si);

	printf("The page size for this system is %u bytes.\n", si.dwPageSize);

	return 0;
}

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

Страница может находится в трех состояниях:

  • Свободна
  • Чистая
  • Грязная

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

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

Грязные страницы изменены после загрузки, поэтому должны быть выгружены в своп перед использованием.

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

  • Малый. В случаях, когда соответствующая страница загружена в физическую, но отображение не установлено. Это возможно в случаях разделяемой памяти, либо если ОС не успела выгрузить страницу в своп.
  • Большой. Если соответствующая страница выгруженна в своп, либо не выделена ввиду отложенного выделения. В таком случае ОС должна загрузить эту страницу в физическую память.
  • Неверный. Если не существует страницы, соответствующей запрошенному адресу, либо процесс не имеет права доступа к этой странице. В таком случае, ОС сообщает программе об ошибке.

Существуют разные методы сообщения о неверном PF. В частности, Windows использует механизм Structured Exception Handling, генерируя асинхронное исключение с кодом 0xC0000005. Unix-подобные ОС посылают сигнал SIGSEGV (код 11), так же известный как segmentation fault.

Примеры:

int main(void) {
  __try
  {
      *(int*) 0 = 0;
  }
  __except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ?
           EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
  {
      return 1;
  }
}
#include <cstdio>
#include <cstdlib>
#include <signal.h>
#include <execinfo.h>

void handler(int signal) {
    void *array[10];
    size_t size;
    size = backtrace(array, 10);
    fprintf(stderr, "Error: signal %d:\n", signal);
    backtrace_symbols_fd(array, size, 2);
    exit(EXIT_FAILURE);
}

int main()
{
    signal(SIGSEGV,handler);
    *(int *) 0 = 0;
}

Если исключение не обрабатывается программой, то ОС сообщает об ошибке пользователю и завершается.

Обработка Page Fault, как и прочих прерываний, требует переключения контекста, что является затратной по времени операцией. Поэтому ОС используют различные стратегии управления виртуальной памятью, чтобы избежать Page Fault. Например:

  • По запросу. Самый простой алгоритм, который загружает страницы только после того, как они были запрошены. В том числе, страницы исполняемого кода загружаются с диска только когда PC достигает их.
  • Предугадывание. ОС загружает несколько страниц, находящихся рядом со страницей, вызвавшей Page Fault.
  • Периодическая очистка. При простое процессора, страницы периодически синхронизируются со свопом, что позволяет их использовать сразу по необходимости.

Существуют и другие стратегии.