Linux и оперативная память

В этой статье будет рассмотрена общая информация о том, как в Linux используется оперативная память. Разберём основные моменты и термины.

Общая информация

Оперативная память, с которой работает система Linux, организована в виде страниц. Страница — это минимальная единица памяти, с которой происходит работа. По размеру страницы делятся на:

  • стандартная - 4KB;
  • HugePages - обычно 2 MB на архитектуре x86-64.

Помимо страниц память делится на зоны:

  • DMA — содержит первые 16 МБ физической памяти.
  • DMA32 — содержит первые 4 GB и существует только на 64-разрядных системах. Но это не означает, что первые 4 ГБ принадлежат только DMA32.
  • Normal — вся остальная память.

Зоны DMA и DMA32 содержат страницы, которые совместимы с режимом DMAРежим DMA ( direct memory access ) — это прямой доступ к памяти со стороны периферийного устройства без участия процессора. Если оборудование работает с памятью в режиме DMA, то оно занимает память из этой зоны и не может брать страницы из зоны Normal.

Когда драйвер говорит «Мне нужна память, доступная для DMA», ядро выделяет страницу из зоны DMA или DMA32. Когда обычное приложение вызывает malloc(), ядро выделяет страницу из зоны Normal.

В оперативной памяти хранятся:

  • данные ядра;
  • данные процессов;
  • файлы, которые были прочитаны с жесткого диска или записаны на него.

За выделение оперативной памяти отвечает ядро Linux.

Выделяемая память процессу может быть либо резидентная, либо виртуальная. В примере ниже видно у процессов резидентную (rss) и виртуальную память (vsz). Эта память отображается в KB.

$ ps -C apache2 -o pid,user,rss,vsz,comm
    PID USER       RSS    VSZ COMMAND
    403 root      7316  11188 apache2
    405 www-data  7032 1216200 apache2
    406 www-data 11128 1216200 apache2
  • Виртуальная память (VSZ) — это память которую выделили процессу, но не факт что он успел в эту память что-то записать. Точнее - это всё виртуальное адресное пространство процесса.
  • Резидентная память (RSS) — это память которую процесс занял, то есть что-то сохранил в виртуальную память. Именно резидентная память показывает сколько процесс потребляет физической памяти. Но RSS содержит и shared memory. То есть если два процесса используют одну библиотеку, RSS обоих включает эти страницы. Поэтому суммировать RSS всех процессов нельзя.

Приложение может запросить много памяти, а использовать малую её часть. Поэтому почти всегда rss меньше чем vsz.

Раздел или файл подкачки

Раздел подкачки (SWAP) — это раздел на жестком диске, куда помещаются:

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

Если какие-то данные из rss сбрасываются в swap то rss уменьшается, а vsz нет. Отсюда следует что данные процесса, которые лежат в swap, входят в виртуальную память этого процесса.

Linux умеет работать не только с разделом подкачки, но и с файлом подкачки. То есть данные из резидентной памяти могут сбрасываться в специальный файл, который лежит на жёстком диске.

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

Память процессов

Посмотреть более подробно на используемую память процесса поможет файл /proc/<pid>/status. Из предыдущего листинга видно что процесс с номером pid=406 занимает 11128 KB памяти.

$ grep Rss /proc/406/status
RssAnon:            8328 kB
RssFile:            2736 kB
RssShmem:             64 kB
  • RssAnon — rss не сопоставляемая с каким-нибудь файлом на диске;
  • RssFile — rss сопоставляемая с каким-нибудь файлом на диске;
  • RssShmem — rss разделяемая память, которая может использоваться другими процессами (Shared Memory).

В этом же файле можно посмотреть на виртуальную память:

$ grep Vm /proc/406/status
VmPeak:  1281736 kB
VmSize:  1216200 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:     11128 kB
VmRSS:     11128 kB
VmData:   225044 kB
VmStk:       132 kB
VmExe:       316 kB
VmLib:      5024 kB
VmPTE:       240 kB
VmSwap:        0 kB
  • VmPeak — пиковый размер использования виртуальной памяти;
  • VmSize — размер виртуальной памяти в данный момент;
  • VmHWM — пиковый размер использования резидентной памяти;
  • VmRSS — размер резидентной памяти в данный момент;
  • VmExe — код приложения;
  • VmLib — используемые библиотеки;
  • VmSwap — часть данных сброшенная на раздел подкачки.

Когда память выделяется процессу то обычно выделяется не одна страница памяти, а какой-то блок. Такой блок страниц памяти называется virtual memory area (VMA). Один процесс обычно содержит десятки или сотни VMA. Такой группе сразу назначаются права:

  • r — можно читать данные из памяти;
  • w — можно записывать данные в памяти;
  • e — можно выполнять исполняемые файлы.

Также группе назначаются и другие параметры, например:

  • p — приватная память для данного процесса;
  • s — общая память (shared memory).

Страничный кеш

В большинстве систем значительную часть свободной памяти занимает Page Cache. Вся работа с файлами на диске (запись или чтение) идет через Page Cache. Обычно запись кажется быстрее, потому что данные сначала попадают в Page Cache, а затем сбрасывается на диск. А при чтении ядро ищет файл в Page Cache, и если не находит читает файл с диска. Узнать сколько сейчас система тратит памяти на Page Cache можно выполнив команду free:

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           976Mi        74Mi       764Mi       0,0Ki       137Mi       765Mi
Swap:          974Mi          0B       974Mi

Страничный кеш показан в колонке buff/cache. Как мы видим у нас занято 137MB страничным кешем. Хотя тут не только Page Cache, тут также находится Buffer, который тоже связан с файлами на диске.

Посмотреть информацию по Page Cache и Buffer отдельно можно в файле /proc/meminfo:

$ egrep "^Cach|^Buff" /proc/meminfo
Buffers:           16012 kB
Cached:           101220 kB

При создании нового файла, запись идет в cache, а страницы памяти для этого файла помечаются как грязные (dirty). Пока страницы находятся в состоянии dirty, изменения существуют только в памяти. Постепенно грязные страницы сбрасываются на диск, управлять этим можно через параметры sysctl ($ sudo nano /etc/sysctl.conf):

  • vm.dirty_expire_centisecs — интервал сброса грязных страниц на диск в сотых долях секунд (100 = 1с);
  • vm.dirty_ratio — объем оперативной памяти в процентах который может быть выделен под Page Cache.
$ sudo sysctl vm.dirty_expire_centisecs
vm.dirty_expire_centisecs = 3000

$ sudo sysctl vm.dirty_ratio
vm.dirty_ratio = 20

Существует утилита — vmtouch, она может показать какой процент указанного файла находится в страничном кеше. Но её нужно скачивать из git и устанавливать:

$ sudo apt update
$ sudo apt install git make gcc
$ git clone https://github.com/hoytech/vmtouch.git
$ cd vmtouch
$ make
$ sudo make install

$ vmtouch /etc/passwd
           Files: 1
     Directories: 0
  Resident Pages: 1/1  4K/4K  100%
         Elapsed: 6.3e-05 seconds
  • Весь файл /etc/passwd сейчас находится в Page Cache (Resident Pages).

Узнать объем грязных страниц можно из файла /proc/meminfo. А команда sync записывает грязные страницы на диск:

$ grep Dirty /proc/meminfo
Dirty:                24 kB

$ sudo sync

$ grep Dirty /proc/meminfo
Dirty:                 0 kB

HugePages

Поговорим немного про большие страницы HugePages. Особенности таких страниц:

  • размер страниц обычно 2 MB на x86-64.
  • приложение должно уметь работать с такими страницами;
  • они не могут быть выгружены в swap.

Выделить под HugePages страницы можно параметром sysctl:

  • vm.nr_hugepages = <число страниц> (так если указать 1024 то выделится 2048MB).
  • vm.hugetlb_shm_group = <gid> — только члены этой группы могут использовать HugePages.

После исправления /etc/sysctl.conf нужно перезагрузиться и посмотреть на результат в файле /proc/meminfo:

$ egrep "HugePages_T|HugePages_F" /proc/meminfo
HugePages_Total:    1024
HugePages_Free:     1024
  • Выделено 1024 страниц и все они свободны. При этом у нас 2GB памяти не сможет использоваться обычными приложениями, которые не умеют работать с HugePages. Поэтому не всегда нужно выделять HugePages.

Стратегия освобождения памяти в Linux

Мы уже узнали что каждому процессу выделяется блок памяти (виртуальная память) и он эту память использует (физическая память). Суммарно физическая память состоит из оперативной памяти и подкачки (swap). Но подкачка может лишь хранить какие-то данные, которые не очень востребованы. Процессам же, чтобы избежать сильных тормозов, требуется работать с оперативной памятью. При всём этом, суммарно, виртуальной памяти может быть выделено больше чем есть оперативной памяти. И может случится такое, что процессу нужна память, а взять её не откуда. В этом случае ядро может высвободить занятую память тремя способами:

  • сбросить, по мнению ядра, самые ненужные данные из резидентной памяти в swap;
  • сбросить некоторые файлы, которые хранятся в Page Cache, на диск;
  • запустить специальный инструмент OOM Killer, который завершит самый ненужный и занимающий больше всего памяти процесс. Точнее - завершит процесс, который получил наибольшую оценку oom_score (об этом ниже).

Есть несколько стратегий распределения памяти. Управлять этими стратегиями можно с помощью параметра sysctl — vm.overcommit_memory. Этот параметр может принимать значения 0, 1 или 2:

  • vm.overcommit_memory = 2 — ядро строго ограничивает выделение виртуальной памяти. Суммарный объём памяти, который может быть выделен всем процессам, не может превышать CommitLimit. По умолчанию CommitLimit рассчитывается как: Swap + RAM × vm.overcommit_ratio / 100.
  • vm.overcommit_memory = 1 — процессам может быть выделено больше виртуальной памяти, чем позволяет объём физической памяти и swap. При этом, если приложение действительно попытается использовать эту память а физической памяти не хватит, то произойдет событие out of memory. По умолчанию, при событии out of memory, запустится OOM Killer и завершит какой-то процесс. При такой стратегии можно настроить еще некоторые параметры sysctl:
    • vm.panic_on_oom = 1 — при out of memory будет происходить kernel.panic вместо запуска OOM Killer;
    • kernel.panic = 10 — при kernel.panic через 10 секунд сервер перезагрузится.
  • vm.overcommit_memory = 0 — такая настройка используется по умолчанию. При этом ядро, с помощью специальных алгоритмов, само решает когда и сколько выделять памяти процессам.
$ grep -E "CommitLimit|Committed_AS" /proc/meminfo
CommitLimit:     2007972 kB
Committed_AS:     751276 kB
  • CommitLimit — максимально допустимый объём выделенной виртуальной памяти.
  • Committed_AS — сколько памяти уже выделено процессам.

OOM Killer

Любой запущенный процесс в системе потребляет оперативную память. И если память заканчивается, то система может, либо зависнуть, либо завершить один из её процессов. И чтобы система не зависла, в ней существует механизм, который отвечает за завершение процессов в случае нехватки памяти - OOM Killer (Out-Of-Memory Killer).

OOM Killer выбирает процесс для завершения смотря на баллы oom_score. А именно, при нехватке памяти, будет завершён процесс с наибольшим количеством баллов oom_score.

Факторы увеличивающие oom_score:

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

Факторы уменьшающие oom_score:

  • Системные процессы. Системные процессы обычно имеют меньшую вероятность завершения.

Мы, как IT-специалисты, можем также повлиять на некоторые факторы:

  • Корректировка оценки OOM (oom_score_adj). Это самый важный управляемый параметр. Задаётся от -1000 до 1000.
    • -1000: Процесс никогда не будет убит. Используется для критически важных системных процессов (например, systemd).
    • 0: Базовая оценка (по умолчанию для большинства процессов).
    • 1000: Процесс будет убит первым, если использует хоть сколько-нибудь памяти.
  • Приоритет процесса (nice). Процессы, запущенные с низким приоритетом (высокое nice-значение), получают больше баллов и шансов быть завершёнными.

Исходя из этой логики, больше всего шансов быть завершённым у процесса, который:

  1. Использует очень много физической памяти и swap.
  2. Имеет высокое значение oom_score_adj.
  3. Имеет низкий приоритет (высокое nice-значение).

Управление OOM Killer

Полностью выключить OOM Killer в системе нельзя. Но можно перенастроить поведение системы во время нехватки памяти.

echo "vm.panic_on_oom = 1" | sudo tee -a /etc/sysctl.conf # вместо OOM Killer -> kernel panic
echo "vm.panic_on_oom = 0" | sudo tee -a /etc/sysctl.conf  # по умолчанию -> OOM Killer

Для применения изменений выполните:

sysctl -p

Параметр oom_score_adj корректирует итоговое значение oom_score, прибавляя или отнимая баллы у процесса. Можно напрямую поменять значение параметра oom_score_adj используя файловую систему /proc:

echo -500 > /proc/<pid>/oom_score_adj
  • Также в системе есть файл /proc/PID/oom_adj - это устаревший механизм корректировки, от -17 до +15. В современных системах используем - oom_score_adj.

Либо можно управлять oom_score через настройки службы SystemD:

[Service]
# Защита от OOM Killer
OOMScoreAdjust=-500
# При завершении - перезапустить
OOMPolicy=restart

Незначительно на баллы oom_score влияет приоритет процесса. Можно вручную запустить процесс с указанным nice:

# Запустить команду с низким приоритетом
nice -n 19 <команда>

# Запустить команду с высоким приоритетом
sudo nice -n -10 <команда>
  • Установка отрицательных значений (более высокого приоритета) требует прав root (sudo).

Изменение nice уже работающего процесса (renice):

# Понизить приоритет
renice -n 19 -p <pid>

# Повысить приоритет (требует root)
sudo renice -n -5 -p <pid>

Также можно задать приоритет для процессов определённой службы:

[Service]
Nice=19

Смотрим текущие баллы oom_score у процесса

Скрипт для просмотра oom_score у топ 20 процессов:

for pid in /proc/[0-9]*; do
    pid_num=${pid##*/}
    if [ -r "$pid/oom_score" ]; then
        read score < "$pid/oom_score" 2>/dev/null
        printf "%5d %7d\n" "$score" "$pid_num"
    fi
done | sort -rn | head -20

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

cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj

Если понравилась статья, подпишись на мой канал в  VK  или  Telegram .