이 페이지에서는 Android에서 kswapd와 lmkd 간의 상호작용을 다루며, 이 두 가지가 함께 작동하여 메모리 부족을 관리하는 방법을 보여줍니다.
배경
Android는 시스템 메모리가 부족할 때 메모리를 회수하기 위해 두 가지 기본 메커니즘을 사용합니다.
kswapd: 클린 파일 지원 페이지를 제거하거나 익명 페이지를 ZRAM으로 스와핑하여 페이지를 회수하는 표준 Linux 커널 데몬입니다.lmkd: Android의 사용자 공간 로우 메모리 킬러 데몬입니다.kswapd가 메모리 수요를 따라가지 못하고 시스템 압박이 증가하면 (PSI로 표시됨)lmkd가 개입하여 프로세스를 종료하고 더 큰 메모리 청크를 확보합니다.
kswapd 및 lmkd는 두 가지 상호 보완적인 메모리 회수 시스템입니다. 두 프로세스는 동일한 목표 (여유 메모리 생성)를 달성하기 위해 작동하지만 수단이 다릅니다. 비교 및 대조:
- 복구 가능 vs 파괴적:
kswapd는 스토리지에서 다시 읽을 수 있는 메모리 페이지를 삭제하거나, 메모리 페이지를 다른 메모리 파티션 또는 필요에 따라 다시 읽을 수 있는 스토리지로 스왑합니다.lmkd는 프로세스를 종료하여 비휘발성 상태를 소멸시킵니다. - 대역폭 회수:
kswapd는lmkd보다 더 많은 CPU를 사용하여 지정된 양의 여유 메모리를 회수하며, 해당 메모리를 회수하는 데 더 오래 걸립니다.lmkd는 비교적 적은 노력으로 짧은 시간 내에 대량의 메모리를 회수할 수 있습니다. 휴대기기에서 CPU 작업은 지연 시간뿐만 아니라 배터리 전력으로도 변환된다는 점을 고려하세요.
OOM 점수 버킷
Android는 사용자에게 중요한 정도에 따라 프로세스에 메모리 부족 (OOM) 점수 조정 (oom_score_adj)을 할당합니다. 점수가 높은 프로세스가 먼저 종료됩니다.
일반적인 버킷은 다음과 같습니다.
- 캐시됨 (점수 >= 900): 현재 필요하지 않은 프로세스로, 더 빠른 다시 시작을 위해 유지됩니다.
- 서비스 (점수 500): 시작된 서비스를 호스팅하는 프로세스입니다.
- 포그라운드 서비스 (점수 200): 포그라운드 서비스를 호스팅하는 프로세스입니다.
- 포그라운드 (점수 0): 사용자가 현재 상호작용하고 있는 앱입니다.
심층 분석: PSI 및 LMKD 트리거
PSI란 무엇인가요?
압력 스톨 정보 (PSI)는 리소스 부족을 측정하는 표준 방법을 제공하는 Linux 커널 기능입니다. CPU, 메모리, I/O 부족으로 인해 태스크가 지연되는 시간을 추적합니다.
메모리 PSI: '일부'와 '전체'
메모리 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: 메모리 부족 감지에 PSI 모니터를 사용하려면true로 설정합니다 (Android 10 이상에서 기본값).ro.lmk.psi_partial_stall_ms: 종료를 트리거하는some(부분) 중단 기간의 기준점(밀리초)(일반 기기의 경우 기본값 70ms, RAM이 부족한 기기의 경우 200ms)ro.lmk.psi_complete_stall_ms: 종료를 트리거하는full중단 기간의 기준점 (밀리초, 기본값 700ms)ro.lmk.kill_heaviest_task:true인 경우 LMKD는 동일한 OOM 버킷의 후보 중 가장 많은 메모리를 사용하는 프로세스를 종료합니다.
테스트 기기 (Google Pixel 8 Pro)의 관련 조정 값
(참고: Android 15를 실행하는 고급형 기기의 일반적인 기본값)
ro.lmk.use_psi:truero.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)
ADB를 사용하여 프로세스와 OOM 점수를 확인합니다.
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. 메모리 부족 트리거
앱이 기본 프로세스에서 메모리를 자동으로 할당하기 시작합니다. RAM이 많은 기기에서 테스트를 효과적으로 수행하기 위해 앱은 하위 프로세스에 메모리 (예: 각각 500MB)를 할당하여 LMKD의 더 나은 타겟으로 만들도록 명령합니다.
하위 프로세스 종료 모니터링
앱은 IBinder.linkToDeath 메커니즘을 사용하여 이러한 하위 프로세스의 종료를 모니터링합니다.
- 기본 프로세스는
bindService를 사용하여 타겟 프로세스의 서비스에 바인딩합니다. - 연결되면 서비스를 나타내는
IBinder객체를 가져옵니다. - 바인더에서
linkToDeath를 호출하여IBinder.DeathRecipient구현을 등록합니다. - 대상 프로세스가 종료되면 (예: LMKD에 의해 종료됨) 시스템은
binderDied()메서드를 호출하여 앱이 종료 이벤트를 비동기식으로 기록할 수 있도록 합니다.
이 바인딩이 타겟 프로세스의 우선순위를 높이지 않도록(연습의 목적에 어긋남) 앱은 Context.BIND_WAIVE_PRIORITY 플래그를 사용합니다.
샘플 앱 실행
다음은 Google Pixel 8 Pro (총 RAM 약 12GB)에서 테스트 실행이 완료된 후의 앱 스크린샷입니다.

Perfetto 트레이스 분석
상호작용을 캡처하려면 다음 Perfetto 구성을 사용하여 예약, 메모리 카운터, LMK 종료, 압력 정지 정보 (PSI)를 캡처합니다.
Perfetto 구성
예약, 메모리 카운터, LMK 종료, 압력 스톨 정보 (PSI)를 사용하여 Perfetto를 실행하여 상호작용을 캡처합니다.
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
trace 검토
다음 스크린샷은 :cached (가장 종료 가능한) 프로세스가 종료되기 직전부터 :foreground (가장 종료 불가능한) 프로세스가 종료될 때까지의 기간을 보여주는 트레이스에서 가져온 것입니다.
MemFree는 이미 상당히 낮으며 지속적인 압박을 받고 있습니다.
kswapd는 지속적으로 작동하며 모든 kswapdlmkdlab 프로세스 (기본 프로세스 및 하위 프로세스)에서 mem.rss.file 페이지를 삭제하고 mem.rss.anon를 mem.swap로 이동하여 압력을 완화합니다. 이 스크린샷에 표시되지 않은 다른 프로세스에서도 마찬가지입니다. 동시에 kswapdlmkdlab 앱은 계속 메모리를 할당하여 kswapd가 회수할 수 있는 것보다 더 빠르게 부담을 늘립니다.
psi.mem가 충분한 시간 동안 높게 유지되는 경우와 같이 압력이 너무 높아지면 lmkd가 절전 모드에서 해제되어 일부 프로세스를 종료합니다. lmkd가 OOM 점수를 준수하여 kswapdlmkdlab:cached를 먼저 종료한 다음 :service, :foreground를 종료하는 것을 확인할 수 있습니다.
이벤트 타임라인을 검토하고 다음 사항을 생각해 보세요.
kswapd와lmkd가 서로를 보완하는 방식:kswapd는 메모리 부족에 가장 먼저 대응하지만kswapd의 회수 대역폭이 할당 요구를 충족하지 못하는 경우lmkd가 도움이 됩니다.- 메모리 해제 속도:
kswapd에서 소요된 CPU 시간과mem.rss.file를 삭제하거나mem.rss.anon를mem.swap로 스왑하여 회수되는 메모리를 관찰합니다. 이를lmkd의 짧은 활동 급증과 비교해 보세요. 이로 인해 대량의 프로세스 비공개 메모리가 해제됩니다.
쿼리
Perfetto UI (쿼리 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_KILL1:NOT_RESPONDING2:LOW_SWAP_AND_THRASHING(ZRAM 스왑 공간이 매우 부족하고 스래싱이 발생함)3:LOW_MEM_AND_SWAP(시스템의 여유 메모리와 ZRAM 스왑이 모두 부족함)4:LOW_MEM_AND_THRASHING5:DIRECT_RECL_AND_THRASHING6:LOW_MEM_AND_SWAP_UTIL7:LOW_FILECACHE_AFTER_THRASHING8:LOW_MEM9: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 활성화: 사용 가능한 메모리가 특정 워터마크 아래로 떨어지면
kswapd가 절전 모드에서 해제되어 (파란색 임펄스) ZRAM으로 스왑하여 메모리를 회수합니다. 이는 간헐적인 급증으로 표시됩니다. - PSI 급증:
kswapd가 높은 할당 속도를 따라잡지 못하면 스레드가 메모리 할당에서 지연되기 시작하여 PSI(Pressure Stall Information) (녹색 선)가 급증합니다. - LMKD 종료: PSI가 임계 수준에 도달하고 (예: 개념적으로
ro.lmk.psi_complete_stall_ms임계값 초과) 사용 가능한 메모리가 매우 부족하면lmkd가 우선순위가 낮은 프로세스 (시간 30 및 45의 주황색 임펄스)를 종료하여 대량의 메모리를 확보합니다. 종료 직후 사용 가능한 메모리가 증가하고 PSI가 감소하여 압박을 완화하는 데 종료가 효과적임을 알 수 있습니다. ________________________________________________________________________________
← 메모리 회수: 제거 및 스왑 | ↑ 위로 | 메모리 페이지를 OS에 반환 →