Взаимодействие kswapd и lmkd

В этой статье рассказывается о взаимодействии kswapd и lmkd на Android и о том, как они вместе управляют нагрузкой на память.

Фон

В Android есть два основных механизма освобождения памяти, когда ее не хватает:

  1. kswapd – стандартный демон ядра Linux, который освобождает страницы, удаляя чистые страницы, связанные с файлами, или заменяя анонимные страницы на ZRAM.
  2. lmkd – фоновый процесс Low Memory Killer в пространстве пользователя Android. Когда kswapd не справляется с нагрузкой на память и нагрузка на систему возрастает (о чем сигнализирует PSI), lmkd завершает процессы, чтобы освободить большие объемы памяти.

kswapd и lmkd – это две дополняющие друг друга системы освобождения памяти. Они работают над достижением одной и той же цели (создание свободной памяти), но разными средствами. Для сравнения и сопоставления:

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

Сегменты оценок OOM

Android назначает процессам корректировку оценки нехватки памяти (oom_score_adj) в зависимости от их важности для пользователя. Процессы с более высокими оценками завершаются первыми.

Вот примеры распространенных сегментов:

  • Кешированные (оценка >= 900). Процессы, которые сейчас не нужны, но хранятся в памяти для более быстрого перезапуска.
  • Сервис (500 баллов). Процессы, в которых размещены запущенные сервисы.
  • Активная служба (оценка 200). Процессы, в которых размещены активные службы.
  • Активное приложение (оценка 0). Приложение, с которым пользователь взаимодействует в данный момент.

Подробный обзор: запуск PSI и LMKD

Что такое PSI?

Pressure Stall Information (PSI) – это функция ядра Linux, которая позволяет измерять нехватку ресурсов. Она отслеживает время задержки задач из-за нехватки ресурсов ЦП, памяти и I/O.

PSI памяти: "some" и "full"

PSI памяти отслеживает задержки, вызванные нехваткой памяти (например, ожидание освобождения страниц или повторных ошибок). Он содержит два показателя:

  • some – процент времени, в течение которого хотя бы одна задача была приостановлена из-за нехватки памяти. Это означает, что нехватка памяти влияет на производительность, но некоторые процессы все ещё выполняются.
  • full – процент времени, в течение которого все задачи, кроме бездействующих, были приостановлены одновременно. Это указывает на критическое состояние, когда система полностью заблокирована в ожидании памяти.

Как PSI запускает LMKD

Демон lmkd регистрируется в ядре, чтобы получать уведомления о превышении пороговых значений PSI (путем мониторинга /proc/pressure/memory). Когда система испытывает высокую нагрузку на память (например, задержка full превышает заданную продолжительность), ядро активирует lmkd. Затем lmkd оценивает состояние системы и решает, нужно ли завершать процессы и какие именно, исходя из их размера и оценки OOM.

Настройка LMKD

Поведение LMKD можно настроить с помощью системных свойств.

Как найти значения настройки на устройстве

Чтобы найти текущие значения настройки LMKD на подключенном устройстве, выполните следующую команду:

adb shell getprop | grep lmk

Общие свойства LMKD

Ниже приведены некоторые ключевые свойства, используемые для настройки LMKD (полный список см. в документации Android).

  • ro.lmk.use_psi: задайте значение true, чтобы использовать мониторы PSI для обнаружения нехватки памяти (по умолчанию в Android 10 и более поздних версиях).
  • ro.lmk.psi_partial_stall_ms – пороговое значение длительности задержки some (частичной) в миллисекундах, при которой запускается принудительное завершение (по умолчанию 70 мс на обычных устройствах и 200 мс на устройствах с малым объемом ОЗУ).
  • ro.lmk.psi_complete_stall_ms – пороговое значение длительности задержки full в миллисекундах, при котором происходит принудительное завершение (по умолчанию 700 мс).
  • ro.lmk.kill_heaviest_task – если true, LMKD завершает процесс, который использует больше всего памяти среди кандидатов в том же сегменте OOM.

Значения параметров на тестовом устройстве (Google Pixel 8 Pro)

(Примечание. Стандартные настройки по умолчанию для устройств высокого класса с Android 15.)

  • ro.lmk.use_psi: true
  • ro.lmk.psi_partial_stall_ms: 70 (типичное значение)
  • ro.lmk.psi_complete_stall_ms: 700 (типичное значение)
  • ro.lmk.kill_heaviest_task: true

Проверка ZRAM и файла подкачки

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

# Check ZRAM sizing and usage
adb shell free -m

# Check swap partition details
adb shell cat /proc/swaps

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

В этом упражнении вы будете использовать пример приложения KswapdLmkdLab, чтобы создать процессы в разных сегментах OOM, а затем задействовать память в основном процессе, чтобы активировать kswapd и lmkd.

Приложение настроено на автоматическое создание подпроцессов и выделение памяти, чтобы сделать их привлекательными целями для LMKD.

1. Создание подпроцессов

При запуске приложение автоматически порождает следующие подпроцессы:

  • Кешированный процесс (oom_score_adj >= 900)
  • Процесс сервиса (oom_score_adj 500)
  • Процесс активной службы (oom_score_adj 200)

Проверьте процессы и их оценки OOM с помощью ADB:

adb shell dumpsys activity processes | grep com.android.kswapdlmkdlab

Пример фрагмента выходных данных:

    Proc #  0: fg       T/A/TOP  LCMNFUATI  t: 0 18959:com.android.kswapdlmkdlab/u0a358 (top-activity)
    Proc #  1: prcp     b/ /FGS  -------TI  t: 0 19050:com.android.kswapdlmkdlab:foreground/u0a358 (fg-service)
    Proc #  2: svc      b/ /SVC  -------TI  t: 0 19077:com.android.kswapdlmkdlab:service/u0a358 (started-services)
    Proc #  3: cch      b/ /CACC -------TI  t: 0 19014:com.android.kswapdlmkdlab:cached/u0a358 (cch-client-act)

Сокращения состояний процесса:

  • fg: активное приложение (оценка OOM – 0)
  • prcp: Perceptible (активная служба, оценка OOM 200)
  • svc: сервис запущен (оценка OOM – 500)
  • cch: кешированный процесс (оценка OOM – 900)

2. Как вызвать нехватку памяти

Приложение автоматически начнет выделять память в основном процессе. Чтобы тест был эффективным на устройствах с большим объемом ОЗУ, приложение также заставляет подпроцессы выделять память (например, по 500 МБ каждый), чтобы сделать их более подходящими целями для LMKD.

Отслеживать завершение подпроцессов

Приложение отслеживает завершение этих подпроцессов с помощью механизма IBinder.linkToDeath:

  1. Основной процесс привязывается к сервису в целевом процессе с помощью bindService.
  2. После подключения он получает объект IBinder, представляющий сервис.
  3. Он регистрирует реализацию IBinder.DeathRecipient, вызывая linkToDeath в связывателе.
  4. Когда целевой процесс завершается (например, из-за LMKD), система вызывает метод binderDied(), позволяя приложению асинхронно записать событие завершения.

Чтобы эта привязка не повышала приоритет целевых процессов (что противоречило бы цели упражнения), приложение использует флаг Context.BIND_WAIVE_PRIORITY.

Пример приложения в действии

Ниже приведен скриншот приложения после завершения тестового запуска на Google Pixel 8 Pro (общий объем ОЗУ составляет около 12 ГБ).

Скриншот примера приложения после завершения тестирования.
Рисунок 1. Пример приложения после завершения тестирования на Pixel 8 Pro.

Анализ трассировки Perfetto

Чтобы зафиксировать взаимодействие, используйте следующую конфигурацию Perfetto для записи планирования, счетчиков памяти, завершения процессов LMK и информации о задержках из-за нехватки ресурсов (PSI).

Конфигурация Perfetto

Запишите взаимодействие, запустив Perfetto с планированием, счетчиками памяти, завершением процессов LMK и информацией о задержках из-за нехватки ресурсов (PSI):

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/kswapd_lmkd.perfetto-trace <<EOF
buffers: { size_kb: 131072 }
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "lowmemorykiller/lowmemory_kill"
            ftrace_events: "vmscan/mm_vmscan_kswapd_wake"
            ftrace_events: "vmscan/mm_vmscan_kswapd_sleep"
            ftrace_events: "psi/psi_event"
        }
    }
}
duration_ms: 180000
EOF

Просмотр трассировки

На скриншоте ниже показан период времени, который начинается незадолго до завершения процесса :cached (наиболее уязвимого) и заканчивается завершением процесса :foreground (наименее уязвимого).

Скриншот интерфейса Perfetto, на котором показаны действия kswapd и lmkd.
Рисунок 2. Трассировка Perfetto, показывающая восстановление kswapd и завершение lmkd.

Как видите, MemFree уже довольно низкий и постоянно снижается. kswapd постоянно работает и снижает нагрузку, удаляя mem.rss.file страниц и перемещая mem.rss.anon в mem.swap из всех процессов kswapdlmkdlab (основного и дочерних), а также, вероятно, из других процессов, не показанных на этом скриншоте. В то же время приложение kswapdlmkdlab продолжает выделять память, увеличивая нагрузку быстрее, чем kswapd может ее освободить.

Когда давление становится слишком высоким, о чем свидетельствует высокое значение psi.mem в течение достаточного периода времени, lmkd просыпается и завершает некоторые процессы. Как видите, lmkd учитывает оценки OOM и сначала завершает kswapdlmkdlab:cached, затем :service и только потом :foreground.

Просмотрите хронологию событий и подумайте над следующим:

  • Как kswapd и lmkd дополняют друг друга: kswapd первым реагирует на нехватку памяти, но lmkd помогает, когда пропускная способность kswapd не соответствует спросу на выделение памяти.
  • Скорость освобождения памяти. Посмотрите, сколько процессорного времени тратится на kswapd и сколько памяти освобождается при удалении mem.rss.file или переносе mem.rss.anon в mem.swap. Сравните это с короткими всплесками активности в lmkd, которые приводят к освобождению большого объема частной памяти процесса.

Запросы

Выполните эти запросы в интерфейсе Perfetto (Query SQL), чтобы извлечь данные из трассировки.

1. Активность kswapd

Чтобы узнать, когда управляющая программа подкачки ядра была активна и работала, выполните следующую команду:

SELECT ts, dur, state
FROM thread_state
JOIN thread USING (utid)
WHERE thread.name LIKE 'kswapd%'
  AND state = 'Running'
LIMIT 5;

Пример выходных данных:

ts dur штат
30317633052969 159180 Бег
30317633398347 3999186 Бег
30317638448111 1847941 Бег
30317641455354 1031006 Бег
30317643272615 888387 Бег

2. Остановки LMKD

LMKD записывает сведения о принудительных завершениях процессов из-за нехватки памяти в таблицу instant. Чтобы запросить все завершения LMKD, выполните следующую команду:

SELECT ts, name
FROM instant
WHERE name LIKE 'lmk,%'
ORDER BY ts ASC;

Формат названия события: lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing>.

  • kill_reason – целое число, указывающее, почему lmkd решил завершить процесс (определено в system/memory/lmkd/statslog.h):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • 2: LOW_SWAP_AND_THRASHING (в swap-пространстве ZRAM осталось очень мало места, и произошла пробуксовка)
    • 3: LOW_MEM_AND_SWAP (в системе мало свободной памяти и места в файле подкачки ZRAM)
    • 4: LOW_MEM_AND_THRASHING
    • 5: DIRECT_RECL_AND_THRASHING
    • 6: LOW_MEM_AND_SWAP_UTIL
    • 7: LOW_FILECACHE_AFTER_THRASHING
    • 8: LOW_MEM
    • 9: DIRECT_RECL_STUCK
  • max_thrashing – максимальный процент пробуксовки страниц в момент завершения процесса.

Пример выходных данных:

ts название
30326148679967 lmk,2308,3,900,201,0
30330439763710 lmk,2309,3,500,201,0
30334985999837 lmk,2310,2,200,0,307

Визуализация

Чтобы визуализировать данные, вы можете использовать следующий скрипт gnuplot для построения графика свободной памяти системы, активности kswapd, завершений lmkd и PSI в одном временном ряду. Предполагается, что скрипт запускается из верхнего каталога сборки Android.

Пример графика выходных данных

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

График, на котором показаны объем свободной памяти, активность kswapd, PSI и завершения процессов lmkd.
Рисунок 3. График нагрузки на память, показывающий свободную память, kswapd, PSI и завершенные lmkd.

Объяснение сюжета

На этом графике показано динамическое взаимодействие между различными компонентами управления памятью во время стресс-теста:

  1. Уменьшение объема свободной памяти. По мере того как основное приложение выделяет память, объем свободной системной памяти (фиолетовая линия) неуклонно уменьшается.
  2. Активация kswapd. Когда объем свободной памяти падает ниже определенного уровня, запускается kswapd (синие импульсы), чтобы освободить память путем подкачки в ZRAM. Это выглядит как периодические всплески.
  3. Скачки PSI. Поскольку kswapd не справляется с высокой скоростью выделения памяти, потоки начинают задерживаться на этом этапе, что приводит к скачкам PSI (зеленая линия).
  4. Завершение процессов демоном LMKD. Когда PSI достигает критического уровня (например, превышает пороговое значение ro.lmk.psi_complete_stall_ms) и объем свободной памяти становится крайне низким, lmkd завершает низкоприоритетные процессы (оранжевые импульсы на отметках 30 и 45), чтобы освободить большие объемы памяти. Обратите внимание, что сразу после завершения процесса объем свободной памяти увеличивается, а PSI снижается. Это говорит о том, что завершение процесса эффективно снижает нагрузку. ________________________________________________________________________________

← Освобождение памяти: вытеснение и подкачка | ↑ Наверх | Возврат страниц памяти в ОС →