OS에 메모리 페이지 반환

앱 코드에서 객체를 해제하거나 임시 스트림을 닫는다고 해서 항상 실제 RAM이 운영체제에 반환되는 것은 아닙니다. 커스텀 네이티브 할당자, 가비지 컬렉션 런타임, 개방 주소 해시 테이블은 내부의 논리적 데이터가 사라진 후에도 오랫동안 실제 페이지에서 지원되는 가상 메모리 영역을 유지하는 경우가 많습니다. 마찬가지로 대규모 애셋 보관 파일을 읽거나 메모리 매핑하면 커널 페이지 캐시가 채워지고 이는 앱의 메모리 제어 그룹 (memcg)에 계속 청구됩니다.

이 페이지에서는 madvise 및 posix_fadvise 시스템 호출을 사용하여 가상 주소 예약 및 파일 설명자를 그대로 유지하면서 사용되지 않는 익명 및 파일 지원 페이지를 OS에 반환하는 방법과 adb shell 및 Perfetto에서 영향을 확인하는 방법을 설명합니다.

논리적 할당 해제가 물리적 RAM을 유지하는 이유

Linux 커널은 하드웨어 페이지의 세분화 수준에서 실제 메모리를 추적합니다. Android 기기에서는 4KB 또는 16KB입니다. 프로세스가 mmap를 사용하여 가상 주소 공간의 영역을 예약하면 커널은 실제 RAM을 할당하지 않고 가상 메모리 영역 (VMA)을 만듭니다. 커널은 프로세스가 해당 페이지를 처음 터치할 때만 물리적 페이지를 할당하고 청구합니다. 익명 메모리의 경우 첫 번째 읽기는 공유 제로 페이지를 매핑하므로 쓰기만 RAM을 할당합니다.

페이지가 RAM에 오류가 발생하면 커널은 해당 페이지 내의 바이트가 여전히 앱 런타임의 라이브 객체를 나타내는지 알 수 없습니다. 앱에서 사용을 완료한 후에도 실제 페이지를 상주 상태로 유지하는 두 가지 일반적인 패턴은 다음과 같습니다.

  • 익명 아레나 및 풀: 커스텀 네이티브 할당자 (예: 슬랩, 풀 또는 프레임 아레나 할당자), 이동하지 않는 힙 공간이 있는 관리형 런타임, 대형 해시 테이블은 MAP_PRIVATE | MAP_ANONYMOUS를 사용하여 mmap로 연속 가상 주소 범위를 예약합니다. 앱이 아레나를 재설정하거나, 블록을 커스텀 풀로 다시 해제하거나, 메타데이터를 0으로 설정하여 해시 테이블을 지우면 기본 페이지가 더티 상태로 유지됩니다. 커널에 명시적인 힌트가 없으면 이러한 페이지는 프로세스의 익명 상주 집합 크기 (RSS) 및 비례 집합 크기 (PSS)와 memory.current cgroup 요금에 계속 반영됩니다. 메모리 부족 시 커널은 이를 ZRAM으로 압축하며, 이 경우 여전히 memory.swap.current에 청구되는 스왑 공간을 차지합니다.
  • 애셋 스트리밍의 파일 지원 페이지 캐시: 앱이 read, pread 또는 파일 지원 mmap를 사용하여 대규모 애셋 번들, 레벨 보관 파일, 미디어 컨테이너 또는 머신러닝 모델을 읽을 때 커널은 읽은 모든 페이지를 페이지 캐시에 캐시하고 이러한 페이지를 프로세스 cgroup의 memory.stat 파일에 있는 file 카운터에 청구합니다. 앱이 애셋을 압축 해제하거나 익명 런타임 버퍼에 복사하거나 GPU에 업로드하는 경우 원시 파일 페이지를 페이지 캐시에 보관하면 RAM에서 애셋이 중복됩니다.
madvise로 해제된 더티 아레나 페이지와 posix_fadvise로 제거된 캐시된 애셋 페이지(가상 범위와 파일 설명자는 그대로 유지됨)
그림 1. 가상 아레나 및 페이지 캐시에서 실제 페이지를 회수합니다.

익명 페이지 출시

madvise 시스템 호출은 프로세스가 가상 주소 범위를 사용하는 방법에 관한 커널 조언을 제공합니다. 비공개 익명 매핑 (MAP_PRIVATE | MAP_ANONYMOUS)의 경우 MADV_DONTNEED를 전달하면 커널이 지정된 페이지 정렬 범위에 해당하는 실제 페이지와 스왑된 ZRAM 항목을 즉시 삭제합니다.

MADV_DONTNEED를 사용한 madvise 후 다음을 반환합니다.

  • 가상 주소 범위는 프로세스의 주소 공간에서 예약되고 유효한 상태로 유지되므로 (VmSize는 변경되지 않음) 포인터와 아레나 경계는 munmap 또는 mmap를 호출하지 않고도 그대로 유지됩니다.
  • 물리적 페이지는 즉시 매핑 해제되고 해제되어 RssAnon, Pss, memory.stat의 anon 카운터, ZRAM에 있던 페이지의 memory.swap.current를 줄입니다.
  • 해당 범위의 주소에 대한 후속 읽기 또는 쓰기는 요청 시 새로운 0으로 채워진 페이지에서 오류가 발생합니다.
조언 플래그 대상 매핑 Android의 커널 동작
MADV_DONTNEED 비공개 익명 (MAP_PRIVATE | MAP_ANONYMOUS) 실제 페이지와 ZRAM 스왑 슬롯을 즉시 삭제합니다. 이후 액세스는 0으로 채워진 페이지에서 오류가 발생합니다. RssAnon 및 memory.current의 결정론적 감소
MADV_DONTNEED 파일 지원 (MAP_PRIVATE 또는 MAP_SHARED) 프로세스 페이지 테이블에서 페이지를 매핑 해제합니다 (RssFile 삭제). MAP_PRIVATE 매핑의 비공개 쓰기 시 복사 페이지가 삭제되므로 수정사항이 손실되고 다음 액세스 시 파일을 다시 읽습니다. 더티 MAP_SHARED 페이지는 나중에 저장소에 다시 기록됩니다. 클린 페이지는 제거될 때까지 페이지 캐시에 남아 있습니다.
MADV_FREE 비공개 익명 (MAP_PRIVATE | MAP_ANONYMOUS) 페이지를 지연 재사용 가능으로 표시합니다. 커널은 메모리 부족이 없는 경우 RssAnon에 페이지를 유지하고 전역 또는 memcg 회수가 실행될 때만 페이지를 회수합니다.
MADV_WILLNEED 파일 지원 또는 스왑된 익명 파일 지원 매핑의 비동기 미리 읽기를 시작하거나 스왑된 익명 페이지를 ZRAM에서 스왑 캐시로 미리 읽습니다.

런타임 페이지 크기에 내부 범위 정렬

madvise는 전체 페이지에서 작동합니다. 페이지 정렬 시작 주소가 필요하며 길이를 다음 페이지 경계로 반올림합니다. 인접한 바이트가 여전히 라이브 객체를 보유하는 공유 아레나 내에서 범위를 해제하는 경우 범위를 바깥쪽으로 반올림하면 경계 페이지를 공유하는 라이브 할당이 0이 될 수 있습니다.

항상 sysconf(_SC_PAGESIZE)를 사용하여 런타임 페이지 크기를 쿼리하여 4KB 및 16KB 페이지 기기를 모두 지원하고 내부 시작 주소를 다음 페이지 경계로 반올림하고 내부 끝 주소를 이전 페이지 경계로 반내림합니다. 페이지 크기에 구애받지 않는 할당자에 관한 자세한 내용은 커스텀 메모리 할당자 최적화를 참고하세요.

#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>

// Reclaims physical pages strictly inside [region_start, region_start + length)
// without disturbing live objects on partially occupied boundary pages.
void ReleaseUnusedArenaSpan(void* region_start, size_t length) {
  const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
  const uintptr_t page_mask = ~(page_size - 1);

  const uintptr_t raw_start = reinterpret_cast<uintptr_t>(region_start);
  const uintptr_t raw_end = raw_start + length;

  // Round start UP and end DOWN so only fully unused pages are discarded.
  const uintptr_t aligned_start = (raw_start + page_size - 1) & page_mask;
  const uintptr_t aligned_end = raw_end & page_mask;

  if (aligned_end > aligned_start) {
    madvise(reinterpret_cast<void*>(aligned_start),
            aligned_end - aligned_start,
            MADV_DONTNEED);
  }
}

mmap로 커스텀 익명 매핑을 만들 때 PR_SET_VMA_ANON_NAME를 사용하여 VMA에 prctl 라벨을 지정하면 showmap 및 /proc/<pid>/smaps에서 이름으로 상주를 추적할 수 있습니다. dumpsys meminfo는 Unknown 아래의 커스텀 이름이 지정된 익명 리전을 계산합니다. 일부 이전 커널은 이름을 복사하는 대신 이름에 대한 포인터를 유지하므로 문자열 리터럴과 같이 매핑의 전체 기간 동안 유효한 문자열을 전달합니다.

#include <sys/mman.h>
#include <sys/prctl.h>

#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif

void* ReserveNamedArena(size_t capacity_bytes, const char* arena_name) {
  void* ptr = mmap(nullptr, capacity_bytes, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (ptr != MAP_FAILED) {
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,
          reinterpret_cast<unsigned long>(ptr), capacity_bytes,
          reinterpret_cast<unsigned long>(arena_name));
  }
  return ptr;
}

파일 캐시 페이지 제거

앱이 파일 설명자에서 데이터를 읽을 때 커널은 향후 읽기를 예상하여 페이지 캐시에 파일 페이지를 캐시합니다. 이렇게 하면 작은 구성 파일이나 공유 라이브러리에 대한 반복 액세스 속도가 빨라지지만, 한 번 파싱되어 다른 곳에 디코딩된 형식으로 저장되는 대규모 애셋 보관 파일, 텍스처 패키지 또는 오디오 및 동영상 청크를 스트리밍할 때는 불리하게 작용합니다.

posix_fadvise 시스템 호출을 사용하면 앱이 열린 파일 설명자에서 예상되는 파일 액세스 패턴을 커널에 알릴 수 있습니다.

  • POSIX_FADV_DONTNEED: 지정된 바이트 범위가 가까운 미래에 액세스되지 않을 것임을 커널에 알립니다. 커널은 페이지 정렬 범위에서 매핑되지 않은 깨끗한 캐시된 페이지를 페이지 캐시에서 즉시 제거하여 memory.stat의 file 카운터와 cgroup의 memory.current 요금을 줄입니다.
  • POSIX_FADV_SEQUENTIAL: 파일 설명자의 커널 미리 읽기 기간을 두 배로 늘려 큰 보관 파일을 처음부터 끝까지 스트리밍할 때 처리량을 개선합니다. 이를 후행 청크의 POSIX_FADV_DONTNEED와 페어링하여 사용하면 페이지 캐시 공간이 작은 슬라이딩 윈도우로 제한됩니다.
  • POSIX_FADV_RANDOM: 파일 설명자에서 추측성 커널 미리 읽기를 사용 중지합니다. 각각의 작은 읽기 작업이 인접한 미사용 페이지를 페이지 캐시로 가져오지 않도록 큰 보관 파일 또는 데이터베이스 파일에 바이트 오프셋으로 희소하고 연속되지 않은 조회를 수행할 때 사용합니다. 메모리 매핑 파일의 경우 매핑에서 MADV_RANDOM를 사용하여 madvise를 호출합니다.

다음 예시에서는 대규모 애셋 보관 파일을 청크로 스트리밍하여 순차적 미리 읽기를 미리 사용 설정하고 처리된 각 페이지 정렬 범위를 페이지 캐시에서 제거합니다.

#include <fcntl.h>
#include <unistd.h>
#include <cstdint>
#include <vector>

void StreamAssetArchive(int fd, size_t total_size) {
  const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE));
  constexpr size_t kChunkSize = 4 * 1024 * 1024;  // 4 MB streaming window
  std::vector<uint8_t> buffer(kChunkSize);

  // Hint sequential access so the kernel prefetches upcoming chunks.
  posix_fadvise(fd, 0, total_size, POSIX_FADV_SEQUENTIAL);

  off_t offset = 0;
  while (offset < static_cast<off_t>(total_size)) {
    ssize_t bytes_read = pread(fd, buffer.data(), buffer.size(), offset);
    if (bytes_read <= 0) break;

    // Decode or upload buffer[0 .. bytes_read) into runtime structures here.

    // Evict full pages that have been completely consumed.
    const size_t aligned_len =
        (static_cast<size_t>(bytes_read) / page_size) * page_size;
    if (aligned_len > 0) {
      posix_fadvise(fd, offset, static_cast<off_t>(aligned_len),
                    POSIX_FADV_DONTNEED);
    }
    offset += bytes_read;
  }
}

파일 지원 mmap 영역 회수

앱이 pread로 읽는 대신 mmap로 대규모 애셋 파일을 메모리 매핑하는 경우 POSIX_FADV_DONTNEED만으로 posix_fadvise를 호출하면 프로세스의 페이지 테이블에 여전히 매핑된 페이지가 제거되지 않습니다. 전체 VMA를 매핑 해제하지 않고 읽기 전용 파일 매핑의 사용된 스팬을 삭제하려면 다음 안내를 따르세요.

  1. 매핑된 가상 주소 범위에서 madvise(addr, length, MADV_DONTNEED)을 호출하여 프로세스의 페이지 테이블에서 해당 페이지를 매핑 해제합니다 (RssFile 삭제).
  2. 해당 파일 오프셋에서 posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED)를 호출하여 커널이 페이지 캐시에서 매핑되지 않은 클린 페이지도 제거하도록 합니다 (memory.stat 드롭의 file).

페이지 조언 가이드라인 따르기

madvise 및 posix_fadvise를 할당자, 애셋 로더, 수명 주기 핸들러에 통합할 때 다음 권장사항을 적용하세요.

  • 대략적인 영역으로 일괄 호출: 각 madvise 호출은 프로세스의 메모리 맵 잠금 (mmap_lock)을 가져오고 페이지 테이블을 탐색하며 다른 CPU 코어에서 일치하는 변환 색인 버퍼 (TLB) 항목을 무효화합니다(Arm의 브로드캐스트 TLB 유지보수 안내 및 x86의 프로세서 간 인터럽트 사용). 작은 할당 해제마다 madvise를 호출하지 않습니다. 여유 범위를 통합하고 커널에 64KB~1MB 이상의 청크로 알리거나 프레임 경계 및 백그라운드 전환 (onStop 및 TRIM_MEMORY_UI_HIDDEN) 중에 여유 범위를 스윕합니다.
  • 해시 테이블 및 풀의 빈 용량 회수: 장면 전환 또는 데이터 동기화 후 대규모 해시 테이블, 객체 풀 또는 스테이징 버퍼가 삭제되면 백업 할당을 축소하거나 삭제된 백업 배열 전체에 MADV_DONTNEED를 실행합니다. tombstone으로 할당된 32MB 개방 주소 지정 테이블을 유지하면 32MB의 물리적 페이지가 모두 프로세스에 청구됩니다.
  • 더티 파일 데이터를 제거하기 전에 플러시: POSIX_FADV_DONTNEED는 클린 페이지 캐시 페이지만 제거합니다. 앱이 즉시 다시 읽지 않을 대용량 임시 파일이나 캐시 파일을 작성하는 경우 posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) 전에 fdatasync를 호출하여 더티 페이지가 저장소에 기록된 후 RAM에서 삭제되도록 합니다.
  • 자주 사용되는 공유 코드 또는 애셋을 제거하지 않음: 반복적으로 액세스되는 DEX, ODEX, VDEX, 공유 라이브러리 (.so) 또는 UI 애셋에서 POSIX_FADV_DONTNEED를 호출하지 않습니다. 활성 코드 또는 애셋 페이지를 제거하면 압력 정지 정보 (PSI)에서 보고한 대로 스레드를 정지하고 I/O 압력을 높이는 동기식 저장소 리폴트가 강제로 발생합니다.

셸에서 페이지 회수 확인

showmap 및 앱의 cgroup v2 계정 파일을 사용하여 madvise 및 posix_fadvise가 실제 RAM을 회수하는지 확인합니다.

showmap으로 가상 크기와 실제 크기 검사

PR_SET_VMA_ANON_NAME를 사용하여 익명 아레나의 이름을 지정하면 showmap는 가상 예약 (virtual size)과 상주하는 실제 페이지(RSS 및 PSS)를 모두 표시합니다. 다음 샘플 출력은 AOSP의 MemoryLab 샘플 앱에서 가져온 것으로, 아레나의 이름을 memorylab-arena로 지정합니다. I can help you with that. Here is the updated text: com.example.app 및 memorylab-arena를 패키지 이름과 아레나 이름으로 바꿉니다. 터치된 64MB 아레나에서 MADV_DONTNEED를 사용하여 madvise를 호출하기 전에 다음을 실행합니다.

adb shell "showmap \$(pidof com.example.app) | grep -E '^ +virtual|^ +size|memorylab-arena' | head -3"
 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536    65536    65536        0        0        0    65536        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

madvise(arena_ptr, 64 * 1024 * 1024, MADV_DONTNEED)를 호출한 후 showmap를 다시 실행합니다.

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536        0        0        0        0        0        0        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

virtual size는 65536KB (64MB)로 유지되어 가상 주소 범위가 여전히 예약되어 있음을 확인하는 반면 RSS, PSS, private dirty는 0KB로 떨어집니다.

cgroup memory.current 및 memory.stat 검사

익명 페이지 회수와 매핑되지 않은 페이지 캐시 제거(pread로 읽는 경우 프로세스 페이지 테이블에 표시되지 않음)를 모두 확인하려면 프로세스의 cgroup v2 디렉터리를 검사합니다. Android에서 각 앱 프로세스에는 자체 cgroup이 있습니다(기기에 따라 /sys/fs/cgroup/apps/uid_<UID>/pid_<PID>/ 등). 다음 명령어는 /proc/<PID>/cgroup의 0:: 줄에서 디렉터리를 찾고 카운터를 출력합니다.

adb shell '
  PID=$(pidof com.example.app)
  CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
  echo "memory.current: $(cat $CG/memory.current)"
  echo "memory.swap.current: $(cat $CG/memory.swap.current)"
  grep -E "^(anon|file) " $CG/memory.stat
'

64MB 익명 아레나를 해제하고 페이지 캐시에서 128MB 메모리 매핑 애셋 파일을 제거하기 전:

memory.current: 242962432
memory.swap.current: 16633856
anon 100671488
file 140009472

아레나 및 애셋 파일 매핑에서 MADV_DONTNEED를 사용하여 madvise를 호출한 직후, 애셋 파일 설명자에서 POSIX_FADV_DONTNEED를 사용하여 posix_fadvise를 호출합니다.

memory.current: 41766912
memory.swap.current: 16502784
anon 33722368
file 5914624

anon는 64MB, file는 128MB 감소하여 파일 설명자를 닫거나 아레나의 가상 주소 공간을 매핑 해제하지 않고도 memory.current가 약 192MB 감소합니다. memory.swap.current는 거의 변경되지 않습니다. 아레나 페이지가 ZRAM으로 스왑되지 않았기 때문입니다. 값은 기기 및 커널이 이미 자체적으로 제거한 파일의 양에 따라 달라집니다. 커널이 cgroup당 메모리를 충전하고 회수하는 방법에 관한 자세한 내용은 메모리 회수: 제거 및 스왑을 참고하세요.

Perfetto에서 페이지 회수 분석

Perfetto를 사용하면 통합 타임라인에서 madvise 및 posix_fadvise 호출을 커널 페이지 테이블 및 페이지 캐시 카운터와 연결할 수 있습니다.

트레이스 기록

주기적인 프로세스 카운터의 경우 linux.process_stats를 사용하고 실시간 RSS 및 페이지 캐시 이벤트의 경우 linux.ftrace를 사용하여 트레이스를 캡처한 다음 컴퓨터로 가져옵니다.

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/page_advice.perfetto-trace <<EOF
buffers: {
    size_kb: 65536
    fill_policy: RING_BUFFER
}
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config {
            scan_all_processes_on_start: true
            proc_stats_poll_ms: 100
        }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "kmem/rss_stat"
            ftrace_events: "filemap/mm_filemap_add_to_page_cache"
            ftrace_events: "filemap/mm_filemap_delete_from_page_cache"
        }
    }
}
duration_ms: 20000
EOF
adb pull /data/misc/perfetto-traces/page_advice.perfetto-trace

Perfetto UI에서 트랙 검사

Perfetto UI에서 트레이스를 열고 앱의 프로세스 그룹을 펼친 후 다음 카운터 트랙을 고정하여 타임라인 상단에 유지합니다.

  • mem.virt: MADV_DONTNEED 호출에서 일정하게 유지되어 가상 주소 예약 (mmap)이 변경되지 않았음을 확인합니다.
  • mem.rss.anon: 앱이 더티 익명 아레나 또는 힙 영역에서 MADV_DONTNEED를 사용하여 madvise를 호출하는 정확한 타임스탬프에서 급격히 감소합니다.
  • mem.rss.file: 앱이 파일 지원 mmap 리전에서 MADV_DONTNEED를 사용하여 madvise를 호출할 때 감소하며, POSIX_FADV_DONTNEED를 사용하는 posix_fadvise가 페이지 캐시에서 클린 페이지를 제거할 때 mm_filemap_delete_from_page_cache ftrace 이벤트가 급증합니다.
  • mem.swap: MADV_DONTNEED가 이전에 ZRAM으로 압축된 익명 페이지를 삭제하면 감소합니다.
한 프로세스의 Perfetto 카운터 트랙. mem.rss.anon 및 mem.rss.file은 아레나 및 애셋 파일이 채워지면 증가하고 madvise 및 posix_fadvise 후에는 감소하는 반면 mem.swap 및 mem.virt는 일정하게 유지됩니다.
그림 2. 페이지 조언 통화 후 RSS가 감소합니다.

그림 2는 MemoryLab 샘플 앱의 트랙을 보여줍니다. 동일한 뷰를 탐색하려면 Perfetto UI에서 샘플 트레이스를 엽니다.

PerfettoSQL로 최대 메모리 및 최종 메모리 쿼리

대상 프로세스의 mem.virt, mem.rss.anon, mem.rss.file, mem.swap의 최대값과 최종값을 비교하려면 Perfetto UI (쿼리 (SQL) 탭)에서 다음 PerfettoSQL 쿼리를 실행하십시오.

WITH counters AS (
  SELECT t.name AS counter_name, c.ts, c.value
  FROM counter c
  JOIN process_counter_track t ON c.track_id = t.id
  JOIN process p USING (upid)
  WHERE p.name = 'com.example.app'
    AND t.name IN ('mem.virt', 'mem.rss.anon', 'mem.rss.file', 'mem.swap')
)
SELECT
  counter_name,
  CAST(MAX(value) / 1048576 AS INT) AS peak_mb,
  CAST((
    SELECT value FROM counters AS last
    WHERE last.counter_name = counters.counter_name
    ORDER BY ts DESC LIMIT 1
  ) / 1048576 AS INT) AS final_mb
FROM counters
GROUP BY counter_name
ORDER BY counter_name;

그림 2의 트레이스에 대한 샘플 출력:

counter_name  peak_mb  final_mb
mem.rss.anon      111        48
mem.rss.file      212        84
mem.swap           36        36
mem.virt        13952     13952

mem.rss.anon는 최고점에서 63MB, mem.rss.file는 최고점에서 128MB 감소하는 반면 mem.virt는 가상 예약이 변경되지 않으므로 최고점에서 끝납니다.

제거된 페이지를 파일로 확인

mm_filemap_add_to_page_cache 및 mm_filemap_delete_from_page_cache 이벤트는 경로가 아닌 inode 번호 (i_ino)로 파일을 식별합니다. 제외된 페이지가 권장한 파일에 속하는지 확인하려면 inode당 제외 수를 계산합니다.

SELECT
  EXTRACT_ARG(arg_set_id, 'i_ino') AS inode,
  COUNT(*) AS evicted_pages
FROM ftrace_event
WHERE name = 'mm_filemap_delete_from_page_cache'
GROUP BY inode
ORDER BY evicted_pages DESC
LIMIT 5;

그림 2의 트레이스에 대한 샘플 출력:

inode     evicted_pages
9761              35781
76792192            767
126                 668
0                   612
11587712            514

트레이스를 기록한 기기가 아직 연결되어 있는 동안 각 inode를 경로로 확인합니다. 아이노드 번호는 하나의 파일 시스템 내에서만 고유하므로 앱의 데이터가 포함된 파일 시스템으로 검색을 제한합니다.

INODE=<inode from the query>
adb shell "find /data -xdev -inum $INODE 2>/dev/null"

아이노드 9761의 샘플 출력:

/data/user/0/com.android.memorylab/files/asset_pack.bin
/data/data/com.android.memorylab/files/asset_pack.bin

/data/user/0와 /data/data는 동일한 앱 데이터 디렉터리의 두 이름이므로 두 경로는 동일한 파일을 참조합니다. 애셋 파일의 제거 횟수 (35,781페이지)가 파일 자체 (4KB에서 32,768페이지)보다 큽니다. 앱이 파일을 다시 읽고 posix_fadvise를 호출하기 전에 kswapd가 메모리 부족으로 인해 파일의 일부를 이미 제거했기 때문입니다.

한 번에 여러 아이노드를 해결하는 스크립트는 아이노드에 대한 오류 해결 후 파일에 대한 오류 해결을 참조하세요.


← kswapd 및 lmkd 상호작용 | ↑ 위로