Введение

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

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

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

Основная часть

Адресное пространство процесса

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

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

Адресное пространство процесса не является непрерывным, а состоит из отдельных блоков памяти, называемых страницами. Размер страницы обычно равен 4 КБ или 2 МБ в зависимости от архитектуры процессора. Страницы памяти могут быть размещены в любом месте физической памяти, а не по порядку. Для того, чтобы операционная система могла отслеживать, какие страницы принадлежат какому процессу и где они находятся в физической памяти, она использует специальные таблицы, называемые таблицами страниц.

Таблицы страниц - это структуры данных, которые хранят соответствие между виртуальными адресами и физическими адресами страниц памяти. Каждый процесс имеет свою собственную таблицу страниц, которая находится в ядре операционной системы. Таблица страниц состоит из нескольких уровней иерархии, чтобы уменьшить объем занимаемой памяти и ускорить поиск нужной страницы. Например, на x86_64 архитектуре таблица страниц состоит из четырех уровней: PML4, PDPT, PD и PT.

Когда процесс обращается к памяти по виртуальному адресу, процессор должен перевести его в физический адрес. Для этого он использует специальный регистр CR3, который содержит базовый адрес таблицы страниц процесса. Затем он разбивает виртуальный адрес на четыре части: индекс PML4, индекс PDPT, индекс PD и индекс PT. Каждая часть указывает на соответствующий элемент таблицы страниц на каждом уровне. Таким образом, процессор может найти физический адрес нужной страницы памяти.

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

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

В этом случае операционная система должна найти способ освободить память для процесса или завершить его. Для этого она использует специальный механизм, называемый OOM Killer (out of memory killer), который отвечает за выбор и убийство процессов в случае нехватки памяти. О OOM Killer мы поговорим подробнее в конце статьи.

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

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

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

Для того, чтобы оптимизировать работу с подкачкой памяти, операционная система использует несколько алгоритмов и параметров. Один из таких параметров - это SWAPINESS, который определяет, как часто операционная система будет использовать подкачку памяти. SWAPINESS может принимать значения от 0 до 100, где 0 означает, что подкачка будет использоваться только в крайнем случае, когда нет свободной физической памяти, а 100 означает, что подкачка будет использоваться активно, даже если есть достаточно свободной физической памяти. Значение SWAPINESS по умолчанию равно 60 и может быть изменено с помощью команды sysctl или файла /proc/sys/vm/swappiness.

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

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

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

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

Страничный кэш занимает часть физической памяти, которая может быть необходима для других целей. Для того, чтобы оптимально распределять память между страничным кэшем и процессами, операционная система использует алгоритмы вытеснения страниц из кэша. Один из таких алгоритмов - это LRU (least recently used), который вытесняет из кэша те страницы, которые были использованы давнее всего. Другой алгоритм - это CLOCK (clock with adaptive replacement), который вытесняет из кэша те страницы, которые имеют меньший приоритет по различным критериям, таким как частота использования, время последнего использования, статус изменения и т.д.

Для того, чтобы управлять страничным кэшем, операционная система использует несколько параметров, которые можно настроить с помощью команды sysctl или файла /proc/sys/vm. Например, параметр dirty_ratio определяет максимальный процент физической памяти, который может быть занят грязными (измененными) страницами кэша, прежде чем операционная система начнет синхронизировать их с диском. Параметр min_free_kbytes определяет минимальное количество свободной физической памяти, которое должно быть в системе для нормальной работы. Параметр vfs_cache_pressure определяет, насколько агрессивно операционная система будет вытеснять страницы кэша файловых систем по сравнению с другими страницами кэша.

Заключение

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

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

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