Odzyskiwanie pamięci: usuwanie i przestrzeń wymiany

W poprzednich sekcjach omówiliśmy, jak jądro zarządza pamięcią w całym systemie. W tym rozdziale przyjrzymy się bliżej grupom kontroli pamięci (memcg), czyli mechanizmowi, którego Android używa do dzielenia i kontrolowania wykorzystania pamięci przez poszczególne aplikacje.

Zrozumienie memcg jest kluczowe, ponieważ to na tym poziomie system może kierować się do poszczególnych aplikacji w celu odzyskania pamięci lub ustawienia limitów pamięci, co umożliwia proaktywne zarządzanie wykorzystaniem pamięci przez aplikacje.

Grupy kontroli pamięci (memcg)

Grupy kontroli (cgroups) to funkcja jądra systemu Linux, która umożliwia organizowanie procesów w hierarchiczne grupy i rozdzielanie między nie zasobów systemowych (takich jak procesor, pamięć i wejście/wyjście). memcg to kontroler cgroup przeznaczony specjalnie do zarządzania pamięcią.

Kluczowe pojęcia dotyczące memcg

  • Obciążenie memcg: gdy proces w memcg przydziela stronę pamięci (anonimową lub wspieraną przez plik), ta strona jest "obciążana" na rzecz memcg. Całkowite obciążenie memcg to suma wszystkich stron używanych przez wszystkie procesy w tej grupie.
  • Hierarchiczne rozliczanie: wykorzystanie pamięci jest rozliczane w górę drzewa. Obciążenie w podrzędnej grupie cgroup jest też uwzględniane w wykorzystaniu jej grupy nadrzędnej.
  • Limity pamięci: każda grupa memcg może mieć limity (np. memory.max lub memory.high), które w przypadku przekroczenia mogą spowodować odzyskanie pamięci lub nawet wyłączenie procesu przez OOM, niezależnie od globalnej pamięci systemowej.
  • Odzyskiwanie pamięci w memcg: gdy memcg przekroczy limit lub gdy system potrzebuje pamięci, jądro może skierować się do konkretnej grupy memcg w celu odzyskania pamięci. Oznacza to usunięcie stron plików lub przeniesienie stron anonimowych do ZRAM.

Hierarchia memcg w Androidzie

Android używa określonej hierarchii do zarządzania procesami aplikacji. Ta struktura umożliwia systemowi stosowanie różnych zasad do różnych typów aplikacji (np. działających na pierwszym planie i w tle).

Hierarchia memcg Androida

  • /sys/fs/cgroup/apps/: katalog główny wszystkich aplikacji na Androida.
  • uid_<UID>/: katalog dla każdego identyfikatora użytkownika aplikacji. Wszystkie procesy należące do tego samego pakietu aplikacji współdzielą tę grupę.
  • pid_<PID>/: katalog dla każdego procesu i jego procesów potomnych. Umożliwia to szczegółową kontrolę i rozliczanie aplikacji z wieloma procesami.

Ćwiczenie praktyczne: poznawanie memcg

W tym ćwiczeniu znajdziesz katalog memcg dla MemoryLab i zaobserwujesz jego obciążenie pamięci w czasie rzeczywistym.

1. Uruchom MemoryLab

Upewnij się, że na urządzeniu działa MemoryLab.

2. Znajdź memcg MemoryLab

Najpierw pobierz PID działającej aplikacji MemoryLab:

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

Teraz znajdź jej katalog cgroup. Znajdziesz go w /proc/<PID>/cgroup:

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

W cgroup v2 ścieżka po 0:: reprezentuje hierarchię memcg względem /sys/fs/cgroup. Pełna ścieżka to /sys/fs/cgroup/apps/uid_10274/pid_11672/.

3. Odczytaj statystyki memcg

Otwórz ten katalog i sprawdź kluczowe pliki:

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

memory.current pokazuje łączną ilość pamięci (w bajtach) aktualnie obciążonej na rzecz tej grupy cgroup.

# 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 zawiera podział: * anon: ilość pamięci anonimowej (sterty, stosy). * file: ilość pamięci wspieranej przez plik (pamięć podręczna stron). * swap: ilość pamięci przeniesionej do ZRAM.

4. Obserwuj zmiany

  1. Otwórz MemoryLab.
  2. Zanotuj wartość memory.current.
  3. Kilka razy kliknij Allocate Java Memory (10MB) (Przydziel pamięć Java (10 MB)).
  4. Ponownie odczytaj memory.current. Powinna się zwiększyć o około 10 MB po każdym kliknięciu.
  5. Kliknij Allocate Bitmaps (Przydziel bitmapy). Sprawdź memory.stat, aby zobaczyć wzrost anon.

Proaktywne odzyskiwanie pamięci za pomocą memory.reclaim

Przydatną funkcją memcg (v2) jest plik memory.reclaim. Zapisanie wartości w tym pliku powoduje, że jądro natychmiast próbuje odzyskać tę ilość pamięci z tej grupy memcg i wszystkich grup memcg pod nią.

Jak Android używa memory.reclaim

CachedAppOptimizer w Androidzie używa tej funkcji, aby maksymalnie odzyskać pamięć z aplikacji po ich zamrożeniu. App Freezer w Androidzie dba o to, aby aplikacje w pamięci podręcznej zużywały jak najmniej pamięci RAM, gdy nie są uruchomione. Gdy aplikacja przechodzi w tle i zostaje zamrożona, system zapisuje jej aktualne wykorzystanie pamięci w pliku memory.reclaim. Zmusza to jądro do usunięcia wszystkich możliwych stron plików i przeniesienia wszystkich stron anonimowych do ZRAM, co minimalizuje ilość pamięci zajmowanej przez aplikację.

To samo „maksymalne odzyskiwanie” możesz osiągnąć ręcznie, odczytując memory.current i zapisując ją z powrotem w 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"

Ćwiczenie: wymuś odzyskanie pamięci i śledzenie

Teraz zmusimy jądro do odzyskania pamięci z MemoryLab i zarejestrujemy tę aktywność w śladzie Perfetto.

  1. Przygotowanie: upewnij się, że MemoryLab ma przydzieloną pamięć (Java i bitmapy).
  2. Rozpoczęcie śledzenia: użyj tej konfiguracji śledzenia w tekście, aby rejestrować zdarzenia związane z planowaniem, odzyskiwaniem pamięci i pamięcią podręczną stron.

    # 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. Wywołanie presji i odzyskiwania pamięci:

    • W MemoryLab kliknij Allocate Native Memory (1GB) (Przydziel pamięć natywną (1 GB)). Spowoduje to wywołanie presji w całym systemie.
    • W osobnym terminalu odzyskaj 200 MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Powrót do aplikacji: wróć do MemoryLab. Kliknij Thrash Pagecache (Refault test) (Wyczyść pamięć podręczną stron (test ponownego błędu)).

  5. Zatrzymanie śledzenia: naciśnij Ctrl+C w terminalu śledzenia.

Analizowanie odzyskiwania pamięci w Perfetto

Po otwarciu śladu możesz zobaczyć, jak działa odzyskiwanie pamięci w całym systemie i w poszczególnych aplikacjach:

Perfetto pokazujące bezpośrednie odzyskiwanie pamięci i odzyskiwanie pamięci w grupie pamięci

W śladzie poszukaj tych elementów:

  • kswapd: wyszukaj kswapd0 w sekcji Kernel threads (na zrzucie ekranu powyżej został ręcznie przypięty u góry). Gdy system będzie miał trudności ze znalezieniem wolnych stron, zobaczysz, jak się budzi i działa (zielone fragmenty).
  • Bezpośrednie odzyskiwanie pamięci: sprawdź wątki procesu com.android.memorylab. Bezpośrednio pod ścieżką planowania wątku zobaczysz fioletowe fragmenty zdarzeń ftrace (np. mm_vmscan_direct_reclaim_begin). Wskazuje to, że wątek aplikacji jest zablokowany i czeka, aż jądro zwolni strony.
  • Liczniki RSS i wymiany:
    • mem.rss.anon: zwiększa się, gdy klikasz przyciski przydzielania pamięci.
    • mem.swap: stale się zwiększa, gdy kswapd i własne wątki aplikacji (podlegające bezpośredniemu odzyskiwaniu pamięci) kompresują te strony anonimowe do ZRAM.
  • Odzyskiwanie pamięci w memcg: jeśli powiększysz moment, w którym wywołałeś ręczne odzyskiwanie pamięci, zobaczysz gwałtowny spadek wartości rss.anon i rss.file, któremu towarzyszą zdarzenia mm_vmscan_memcg_reclaim.

← Cały system | ↑ W górę | Interakcja kswapd i lmkd →