Interakcja kswapd i lmkd

Na tej stronie opisujemy interakcję między kswapd i lmkd na Androidzie, pokazując, jak te mechanizmy współpracują ze sobą, aby zarządzać obciążeniem pamięci.

Tło

Android używa 2 głównych mechanizmów do odzyskiwania pamięci, gdy systemowi zaczyna jej brakować:

  1. kswapd: standardowy demon jądra systemu Linux, który odzyskuje strony przez usuwanie czystych stron opartych na plikach lub zamianę stron anonimowych na ZRAM.
  2. lmkd: demon Low Memory Killer w przestrzeni użytkownika Androida. Gdy kswapd nie nadąża za zapotrzebowaniem na pamięć i rośnie obciążenie systemu (co sygnalizuje PSI), lmkd interweniuje, aby zakończyć procesy i zwolnić większe fragmenty pamięci.

kswapd i lmkd to 2 uzupełniające się systemy odzyskiwania pamięci. Oba dążą do tego samego celu (zwolnienia pamięci), ale na różne sposoby. Aby porównać i zestawić:

  • Możliwe do odzyskania i nieodwracalne: kswapd usuwa strony pamięci, które można odczytać z pamięci masowej, lub przenosi strony pamięci do innych partycji pamięci lub pamięci masowej, które można odczytać na żądanie. lmkd kończy procesy, co niszczy stan nietrwały.
  • Przepustowość odzyskiwania: kswapd zużywa więcej mocy procesora, aby odzyskać daną ilość wolnej pamięci, niż lmkd, i zajmuje mu to więcej czasu. lmkd może odzyskać duże ilości pamięci w krótkich seriach przy stosunkowo niewielkim wysiłku. Pamiętaj, że na urządzeniach mobilnych obciążenie procesora przekłada się nie tylko na opóźnienia, ale też na zużycie baterii.

Segmenty wyników OOM

Android przypisuje procesom korektę wyniku braku pamięci (oom_score_adj) na podstawie ich ważności dla użytkownika. Procesy z wyższymi wynikami są zamykane jako pierwsze.

Typowe zasobniki:

  • W pamięci podręcznej (wynik >= 900): procesy, które nie są obecnie potrzebne, ale są przechowywane, aby można było je szybciej uruchomić ponownie.
  • Service (wynik 500): procesy hostujące uruchomione usługi.
  • Usługa działająca na pierwszym planie (wynik 200): procesy hostujące usługi działające na pierwszym planie.
  • Pierwszy plan (wynik 0): aplikacja, z którą użytkownik obecnie wchodzi w interakcję.

Szczegółowe omówienie: aktywowanie PSI i LMKD

Czym jest PSI?

Pressure Stall Information (PSI) to funkcja jądra systemu Linux, która zapewnia kanoniczny sposób pomiaru niedoborów zasobów. Śledzi czas opóźnienia zadań z powodu braku procesora, pamięci i wejścia/wyjścia.

Wskaźnik PSI pamięci: „some” a „full”

PSI pamięci śledzi opóźnienia spowodowane brakiem pamięci (np. oczekiwanie na odzyskanie strony lub ponowne błędy). Zawiera 2 rodzaje danych:

  • some: odsetek czasu, przez który co najmniej 1 zadanie było zablokowane w pamięci. Oznacza to, że brak pamięci wpływa na wydajność, ale niektóre zadania są nadal wykonywane.
  • full: odsetek czasu, przez który wszystkie zadania inne niż bezczynne były jednocześnie wstrzymane. Wskazuje to stan krytyczny, w którym system jest całkowicie zablokowany i czeka na pamięć.

Jak PSI aktywuje LMKD

Demon lmkd przestrzeni użytkownika rejestruje się w jądrze, aby otrzymywać powiadomienia o przekroczeniu progów PSI (przez monitorowanie /proc/pressure/memory). Gdy system jest pod dużym obciążeniem pamięci (np. full przekracza skonfigurowany czas), jądro budzi lmkd. lmkd ocenia stan systemu i decyduje, czy zakończyć procesy i które z nich, na podstawie ich wyników OOM i rozmiaru.

Dostrajanie LMKD

Działanie LMKD można dostosować za pomocą właściwości systemowych.

Znajdowanie wartości dostrajania na urządzeniu

Aby znaleźć bieżące wartości dostrajania LMKD na połączonym urządzeniu, uruchom:

adb shell getprop | grep lmk

Typowe właściwości LMKD

Oto niektóre z kluczowych właściwości używanych do dostrajania LMKD (pełną listę znajdziesz w dokumentacji Androida):

  • ro.lmk.use_psi: ustaw wartość true, aby używać monitorów PSI do wykrywania obciążenia pamięci (domyślnie na Androidzie 10 lub nowszym).
  • ro.lmk.psi_partial_stall_ms: próg czasu trwania some (częściowego) zatrzymania w milisekundach, który powoduje zakończenie (domyślnie 70 ms na zwykłych urządzeniach, 200 ms na urządzeniach z małą ilością pamięci RAM).
  • ro.lmk.psi_complete_stall_ms: próg czasu trwania blokady full w milisekundach, który powoduje wymuszenie zakończenia (domyślnie 700 ms).
  • ro.lmk.kill_heaviest_task: jeśli true, LMKD kończy proces, który zużywa najwięcej pamięci spośród kandydatów w tym samym zasobniku OOM.

Odpowiednie wartości dostrajania na urządzeniu testowym (Google Pixel 8 Pro)

(Uwaga: typowe ustawienia domyślne dla urządzeń wysokiej klasy z Androidem 15)

  • ro.lmk.use_psi: true
  • ro.lmk.psi_partial_stall_ms: 70 (typowa)
  • ro.lmk.psi_complete_stall_ms: 700 (typowa)
  • ro.lmk.kill_heaviest_task: true

Weryfikacja ZRAM i pamięci wymiany

Pojemność ZRAM ma duży wpływ na to, jak szybko zapełnia się pamięć wymiany i jak szybko uruchamiane są procesy LMKD. Stan ZRAM i pamięci wymiany na urządzeniu możesz sprawdzić za pomocą tych poleceń:

# Check ZRAM sizing and usage
adb shell free -m

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

Ćwiczenie praktyczne: aktywowanie kswapd i lmkd

W tym ćwiczeniu użyjesz przykładowej aplikacji KswapdLmkdLab, aby utworzyć procesy w różnych zasobnikach OOM, a następnie wykorzystasz pamięć w głównym procesie, aby aktywować kswapd i lmkd.

Aplikacja jest skonfigurowana tak, aby automatycznie tworzyć podprocesy i przydzielać pamięć, dzięki czemu staje się atrakcyjnym celem dla LMKD.

1. Tworzenie podprocesów

Po uruchomieniu aplikacja automatycznie tworzy te procesy podrzędne:

  • Proces w pamięci podręcznej (oom_score_adj >= 900)
  • Proces usługi (oom_score_adj 500)
  • Proces usługi działającej na pierwszym planie (oom_score_adj 200)

Sprawdź procesy i ich wyniki OOM za pomocą ADB:

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

Przykładowy fragment kodu wyjściowego:

    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)

Skróty stanów procesów odpowiadają tym stanom:

  • fg: na pierwszym planie (wynik OOM: 0)
  • prcp: Perceptible (usługa na pierwszym planie, wynik OOM 200)
  • svc: uruchomiono usługę (wynik OOM: 500)
  • cch: proces w pamięci podręcznej (wynik OOM 900)

2. Wywołaj obciążenie pamięci

Aplikacja automatycznie zacznie przydzielać pamięć w głównym procesie. Aby test był skuteczny na urządzeniach z dużą ilością pamięci RAM, aplikacja nakazuje też podprocesom przydzielenie pamięci (np. po 500 MB), aby były lepszymi celami dla LMKD.

Monitorowanie zakończenia podprocesu

Aplikacja monitoruje zakończenie tych procesów podrzędnych za pomocą mechanizmu IBinder.linkToDeath:

  1. Główny proces wiąże się z usługą w procesie docelowym za pomocą bindService.
  2. Po nawiązaniu połączenia uzyskuje obiekt IBinder reprezentujący usługę.
  3. Rejestruje implementację IBinder.DeathRecipient, wywołując linkToDeath w binderze.
  4. Gdy proces docelowy zostanie zakończony (np. przez LMKD), system wywołuje metodę binderDied(), co pozwala aplikacji na asynchroniczne zarejestrowanie zdarzenia zakończenia.

Aby zapobiec podniesieniu priorytetu procesów docelowych przez to powiązanie (co uniemożliwiłoby wykonanie ćwiczenia), aplikacja używa flagi Context.BIND_WAIVE_PRIORITY.

Przykładowa aplikacja w akcji

Oto zrzut ekranu aplikacji po zakończeniu testu na telefonie Google Pixel 8 Pro (który ma około 12 GB pamięci RAM).

Zrzut ekranu przykładowej aplikacji po zakończeniu testu.
Rysunek 1. Przykładowa aplikacja po zakończeniu testu na Pixelu 8 Pro.

Analiza logu czasu Perfetto

Aby zarejestrować interakcję, użyj tej konfiguracji Perfetto, która umożliwia przechwytywanie harmonogramu, liczników pamięci, zabijania procesów przez LMK i informacji o obciążeniu (PSI).

Konfiguracja Perfetto

Zarejestruj interakcję, uruchamiając Perfetto z harmonogramem, licznikami pamięci, zabijaniem procesów przez LMK i informacjami o obciążeniu (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

Sprawdzanie logu czasu

Poniższy zrzut ekranu pochodzi ze śladu pokazującego okres od momentu tuż przed zakończeniem procesu :cached (najbardziej podatnego na zakończenie) do momentu zakończenia procesu :foreground (najmniej podatnego na zakończenie).

Zrzut ekranu interfejsu Perfetto pokazujący aktywność kswapd i lmkd.
Rysunek 2. Ślad Perfetto pokazujący odzyskiwanie kswapd i zabijanie lmkd.

Jak widać, MemFree jest już dość niski i stale spada. kswapd stale działa i zmniejsza obciążenie, usuwając strony mem.rss.file i przenosząc mem.rss.anon do mem.swap ze wszystkich procesów kswapdlmkdlab (procesu głównego i podprocesów), a prawdopodobnie także z innych procesów, które nie są widoczne na tym zrzucie ekranu. W tym samym czasie aplikacja kswapdlmkdlab nadal przydziela pamięć, zwiększając obciążenie szybciej, niż kswapd może je zmniejszyć.

Gdy obciążenie staje się zbyt duże, co widać po wysokiej wartości psi.mem przez wystarczająco długi czas, lmkd budzi się i zabija niektóre procesy. Jak widać, lmkd uwzględnia wyniki OOM, najpierw kończąc kswapdlmkdlab:cached, potem :service, a na końcu :foreground.

Przejrzyj oś czasu zdarzeń i zastanów się nad tymi kwestiami:

  • Jak kswapd i lmkd uzupełniają się nawzajem: kswapd jako pierwszy reaguje na obciążenie pamięci, ale lmkd pomaga, gdy przepustowość odzyskiwania kswapd nie spełnia wymagań dotyczących alokacji.
  • Szybkość zwalniania pamięci: obserwuj czas pracy procesora poświęcony na kswapd i pamięć odzyskiwaną przez usunięcie mem.rss.file lub zamianę mem.rss.anon na mem.swap. Porównaj to z krótkimi okresami aktywności w lmkd, które powodują zwolnienie dużych ilości pamięci prywatnej procesu.

Zapytania

Uruchom te zapytania w interfejsie Perfetto (Query SQL), aby wyodrębnić dane ze śladu.

1. Aktywność kswapd

Aby sprawdzić, kiedy demon wymiany jądra był aktywny i działał, wykonaj:

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

Przykładowe dane wyjściowe:

ts dur stan
30317633052969 159180 Uruchomiono
30317633398347 3999186 Uruchomiono
30317638448111 1847941 Uruchomiono
30317641455354 1031006 Uruchomiono
30317643272615 888387 Uruchomiono

2. Zabicia przez LMKD

LMKD zapisuje błędy LMK w tabeli instant. Aby wysłać zapytanie o wszystkie procesy zakończone przez LMKD:

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

Format nazwy zdarzenia: lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing>.

  • kill_reason: liczba całkowita wskazująca, dlaczego lmkd zdecydował się zakończyć proces (zdefiniowany w system/memory/lmkd/statslog.h):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • 2: LOW_SWAP_AND_THRASHING (bardzo mało przestrzeni wymiany ZRAM i wystąpiło szamotanie procesów)
    • 3: LOW_MEM_AND_SWAP (w systemie jest mało wolnej pamięci i pamięci wymiany 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: maksymalny odsetek przeładowania strony w momencie zamknięcia.

Przykładowe dane wyjściowe:

ts nazwa
30326148679967 lmk,2308,3,900,201,0
30330439763710 lmk,2309,3,500,201,0
30334985999837 lmk,2310,2,200,0,307

Wizualizacja

Aby zwizualizować dane, możesz użyć poniższego skryptu gnuplot, który pozwala wykreślić wolną pamięć systemową, aktywność kswapd, zabicia lmkd i PSI w jednej serii czasowej. Skrypt zakłada, że jest uruchamiany z głównego katalogu kompilacji Androida.

Przykładowy wykres danych wyjściowych

Poniższy wykres został wygenerowany za pomocą poprzedniego skryptu z przykładowymi danymi reprezentatywnymi dla testu.

Wykres wolnej pamięci, aktywności kswapd, PSI i zdarzeń lmkd.
Rysunek 3. Wykres obciążenia pamięci pokazujący wolną pamięć, kswapd, PSI i procesy zakończone przez lmkd.

Wyjaśnienie wykresu:

Ten wykres ilustruje dynamiczną interakcję między różnymi komponentami zarządzania pamięcią podczas testu obciążenia:

  1. Spadek ilości wolnej pamięci: gdy główna aplikacja przydziela pamięć, ilość wolnej pamięci systemowej (fioletowa linia) stale się zmniejsza.
  2. Aktywacja kswapd: gdy ilość wolnej pamięci spada poniżej określonych poziomów, kswapd budzi się (niebieskie impulsy), aby odzyskać pamięć przez przeniesienie do ZRAM. Jest to widoczne jako okresowe skoki.
  3. Skoki PSI: gdy kswapd nie nadąża za wysokim tempem alokacji, wątki zaczynają się blokować podczas alokacji pamięci, co powoduje skoki PSI (Pressure Stall Information) (zielona linia).
  4. Zakończenia procesów przez LMKD: gdy PSI osiąga poziomy krytyczne (np. przekracza próg ro.lmk.psi_complete_stall_ms), a ilość wolnej pamięci jest bardzo mała, lmkd interweniuje, kończąc procesy o niskim priorytecie (pomarańczowe impulsy w czasie 30 i 45), aby zwolnić duże fragmenty pamięci. Zauważ, że zaraz po zakończeniu procesu ilość wolnej pamięci wzrasta, a PSI spada, co pokazuje, że zakończenie procesu skutecznie zmniejsza obciążenie. ________________________________________________________________________________

← Odzyskiwanie pamięci: wykluczanie i wymiana | ↑ W górę | Zwracanie stron pamięci do systemu operacyjnego →