Освобождение памяти: вытеснение и подкачка

В предыдущих разделах мы рассмотрели, как ядро управляет памятью на уровне системы. В этой главе мы подробно рассмотрим группы управления памятью (memcg) – механизм, который Android использует для разделения и контроля использования памяти отдельными приложениями.

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

Контрольные группы памяти (memcg)

Группы управления (cgroups) – это функция ядра Linux, которая позволяет организовывать процессы в иерархические группы и распределять между ними системные ресурсы (например, ЦП, память и ввод-вывод). memcg – это контроллер cgroup, предназначенный для памяти.

Основные понятия memcg

  • Memcg Charge. Когда процесс в memcg выделяет страницу памяти (анонимную или с резервированием на диске), эта страница "записывается" в memcg. Общий объем памяти, используемый memcg, – это сумма всех страниц, используемых всеми процессами в нем.
  • Иерархический учет. Использование памяти учитывается по дереву. Загрузка в дочерней группе cgroup также учитывается при использовании родительской группы.
  • Ограничения памяти. Каждая группа memcg может иметь ограничения (например, memory.max или memory.high), которые запускают освобождение памяти или даже OOM killer при превышении, независимо от глобальной системной памяти.
  • Освобождение памяти для отдельной группы memcg. Когда группа memcg превышает лимит или системе требуется память, ядро может освободить память для определенной группы memcg. Это означает, что его файловые страницы вытесняются, а анонимные страницы переносятся в ZRAM.

Иерархия memcg в Android

В Android используется определенная иерархия для управления процессами приложений. Такая структура позволяет системе применять разные правила к разным типам приложений (например, к активным и фоновым).

Иерархия memcg в Android

  • /sys/fs/cgroup/apps/ – корневой элемент для всех приложений для Android.
  • uid_<UID>/ – каталог для идентификатора пользователя каждого приложения. Все процессы, принадлежащие одному пакету приложения, используют эту группу совместно.
  • pid_<PID>/ – каталог для каждого отдельного процесса и всех дочерних процессов, созданных на его основе. Это позволяет точно контролировать и учитывать приложения с несколькими процессами.

Практическое упражнение: изучение memcg

В этом упражнении вы найдете каталог memcg для MemoryLab и посмотрите, как меняется объем используемой памяти в реальном времени.

1. Запустите MemoryLab.

Убедитесь, что на устройстве запущено приложение MemoryLab.

2. Как найти memcg MemoryLab

Сначала получите PID запущенного приложения MemoryLab:

adb shell pidof com.android.memorylab
# Example output: 11672

Теперь найдите его каталог cgroup. Вы можете найти эту информацию в /proc/<PID>/cgroup:

adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672

В cgroup v2 путь после 0:: представляет иерархию memcg относительно /sys/fs/cgroup. Полный путь выглядит так: /sys/fs/cgroup/apps/uid_10274/pid_11672/.

3. Чтение статистики memcg

Перейдите в этот каталог и посмотрите на файлы ключей:

# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672

memory.current – общий объем памяти (в байтах), выделенный для этой группы.

# Detailed statistics
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.stat | head -n 10
# Example output:
# anon 1585098752
# file 741376
# kernel 5484544
# kernel_stack 393216
# pagetables 4583424
# sec_pagetables 0
# percpu 216
# sock 0
# vmalloc 4096
# shmem 20480

memory.stat предоставляет следующую информацию: * anon – объем анонимной памяти (кучи, стеки). * file – объем памяти, поддерживаемой файлами (кеш страниц). * swap – объем памяти, выгруженной в ZRAM.

4. Отслеживание изменений

  1. Откройте MemoryLab.
  2. Запишите значение memory.current.
  3. Несколько раз нажмите Allocate Java Memory (10MB) (Выделить память Java (10 МБ)).
  4. Прочитайте memory.current ещё раз. При каждом нажатии оно должно увеличиваться примерно на 10 МБ.
  5. Нажмите Выделить битовые карты. Проверьте memory.stat, чтобы увидеть увеличение anon.

Проактивный возврат с помощью memory.reclaim

Мощная функция memcg (v2) – это файл memory.reclaim. Запись значения в этот файл указывает ядру немедленно попытаться освободить этот объем памяти из memcg и любых memcg под ним.

Как Android использует memory.reclaim

CachedAppOptimizer в Android использует эту функцию, чтобы максимально освободить память, занятую приложениями, после их заморозки. Функция заморозки приложений в Android позволяет кешированным приложениям потреблять как можно меньше ОЗУ, когда они не запущены. Когда приложение переходит в фоновый режим и замораживается, система записывает текущее использование памяти в файл memory.reclaim. Это заставляет ядро вытеснить все возможные страницы файлов и перенести все анонимные страницы в ZRAM, минимизируя резидентный след приложения.

Вы можете добиться того же "максимального возврата" вручную, прочитав memory.current и записав его обратно в memory.reclaim:

# Force reclaim of everything
adb shell "cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"

Упражнение: принудительное освобождение памяти и трассировка

Теперь мы заставим ядро освободить память, выделенную для MemoryLab, и запишем это действие в трассировку Perfetto.

  1. Подготовка. Убедитесь, что в MemoryLab есть выделения (Java и битовые карты).
  2. Начать трассировку. Используйте эту встроенную конфигурацию трассировки, чтобы регистрировать события планирования, освобождения памяти и кеширования страниц.

    # Use a config that captures scheduling, reclaim and page cache events
    adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/reclaim.perfetto-trace <<EOF
    buffers: {
        size_kb: 131072
        fill_policy: RING_BUFFER
    }
    data_sources: {
        config {
            name: "linux.ftrace"
            ftrace_config {
                ftrace_events: "sched/sched_switch"
                ftrace_events: "sched/sched_wakeup"
                ftrace_events: "kmem/rss_stat"
                ftrace_events: "mm_filemap_add_to_page_cache"
                ftrace_events: "mm_filemap_delete_from_page_cache"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_end"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_end"
                symbolize_ksyms: true
            }
        }
    }
    data_sources: {
        config {
            name: "linux.process_stats"
            process_stats_config {
                scan_all_processes_on_start: true
            }
        }
    }
    EOF
    
  3. Давление и возврат

    • В MemoryLab нажмите Allocate Native Memory (1GB) (Выделить нативную память (1 ГБ)). Это вызовет давление на всю систему.
    • В отдельном терминале освободите 200 МБ:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Fault Back In (Вернуться к ошибке). Вернуться к MemoryLab. Нажмите Thrash Pagecache (Refault test) (Очистить кэш страниц (тест на повторную ошибку)).

  5. Остановить трассировку. Нажмите Ctrl + C в терминале трассировки.

Анализ возврата памяти в Perfetto

Открыв трассировку, вы увидите, как работает освобождение памяти на уровне системы и отдельных приложений:

Perfetto показывает прямое освобождение памяти и освобождение памяти memcg

Проверьте следующее:

  • kswapd. Найдите kswapd0 в разделе Kernel threads (Потоки ядра). На скриншоте выше этот поток был вручную закреплен вверху. Вы увидите, как он просыпается и работает (зеленые фрагменты), когда система пытается найти свободные страницы.
  • Прямое освобождение. Посмотрите на потоки процесса com.android.memorylab. Вы увидите фиолетовые фрагменты событий ftrace (например, mm_vmscan_direct_reclaim_begin), которые появляются непосредственно под дорожкой планирования потока. Это означает, что поток приложения заблокирован в ожидании, пока ядро освободит страницы.
  • Счетчики RSS и Swap
    • mem.rss.anon: увеличивается, когда вы нажимаете кнопки распределения.
    • mem.swap: стабильно увеличивается, поскольку kswapd и собственные потоки приложения (подлежащие прямому возврату) сжимают эти анонимные страницы в ZRAM.
  • memcg Reclaim. Если вы увеличите масштаб в момент, когда вы запустили освобождение памяти вручную, вы увидите резкое падение как rss.anon, так и rss.file, сопровождаемое событиями mm_vmscan_memcg_reclaim.

← На уровне системы | ↑ Вверх | Взаимодействие kswapd и lmkd →