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

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

Фон

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

  1. kswapd – стандартный демон ядра Linux, который освобождает страницы, удаляя чистые страницы с файловой поддержкой или заменяя анонимные страницы на ZRAM.
  2. lmkd – фоновый процесс 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 (ZRAM swap space is extremely low and thrashing occurred)
    • 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 и завершение работы приложений при нехватке памяти.
Рисунок 3. График нагрузки на память, показывающий свободную память, kswapd, PSI и lmkd kills.

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

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

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

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