지금까지는 커널에 “무엇을 넣을지”(드라이버, 컴파일 옵션, initramfs)를 다뤘다. 이번 편은 각도를 바꿔 커널이 메모리 자체를 어떻게 다루는지 — Page Cache, Swap, Memory Reclaim, Watermark, MGLRU, 그리고 끌 수 있는 디버깅 옵션까지 — 를 검토한다. 이 중 상당수는 이미지 크기보다 런타임 동작에 더 큰 영향을 준다.
이 시리즈의 다른 글
- 메모리가 작은 시스템을 위한 커널 경량화 (1) — 측정 방법론과 베이스라인
- 메모리가 작은 시스템을 위한 커널 경량화 (2) — initrd 사용에 따른 영향
- 메모리가 작은 시스템을 위한 커널 경량화 (3) — Kconfig 옵션으로 커널 이미지 줄이기
- 메모리가 작은 시스템을 위한 커널 경량화 (4) — 빌드 최적화와 initramfs 축소
- 메모리가 작은 시스템을 위한 커널 경량화 (6) — 종합 비교와 메모리 한계 실증
Page Cache
파일을 읽으면 커널이 그 내용을 남는 RAM에 캐싱해두는 게 Page Cache다. free -m의 buff/cache 컬럼이 이 값인데, 지금까지 이 시리즈 전체에서 부팅 직후엔 2MB 안팎으로 거의 무시할 수준이었다 — busybox 하나만 올라온 최소 rootfs라 캐싱할 파일 자체가 별로 없기 때문이다. 캐시 회수 강도는 vm.vfs_cache_pressure(기본 100)로 조절하는데, 실제 애플리케이션이 파일을 많이 읽는 시스템이라면 이 값을 낮춰 캐시를 더 오래 유지하거나, 반대로 메모리가 정말 빠듯하면 높여서 더 적극적으로 회수하게 만들 수 있다.
Swap: zram으로 대체
이 QEMU 타깃엔 물리 스왑 디바이스가 없다. 그렇다고 CONFIG_SWAP을 통째로 빼는 게 능사는 아니다 — 압축 RAM 블록 디바이스인 zram을 스왑으로 붙이면 물리 메모리보다 조금 더 버틸 수 있는 여유가 생긴다. CONFIG_SWAP은 그대로 두고 CONFIG_ZRAM/CONFIG_ZSMALLOC만 추가했다.
./scripts/config --enable CONFIG_ZRAM
./scripts/config --enable CONFIG_ZSMALLOC
make olddefconfig
make -j6 bzImage부팅 후 /dev/zram0이 devtmpfs로 자동 생성되므로, 크기를 정하고 mkswap/swapon만 해주면 된다.
echo 128M > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0
cat /proc/swapsSetting up swapspace version 1, size = 134213632 bytes
Filename Type Size Used Priority
/dev/zram0 partition 131068 0 -1
스왑이 실제로 동작하는지 확인하려고 228MB 환경(QEMU -m 256M)에서 tmpfs에 busybox 바이너리를 반복해서 채워 물리 메모리보다 많은 양(약 213MB)을 강제로 올렸다.
mount -t tmpfs -o size=300m tmpfs /mnt
i=0
while [ $i -lt 105 ]; do
cat /bin/busybox >> /mnt/junk
i=$((i+1))
donefree -m (채우기 전)
total used free shared buff/cache available
Mem: 228 11 215 0 3 213
Swap: 128 0 128
free -m (채운 후, /mnt/junk = 213MB)
total used free shared buff/cache available
Mem: 228 15 3 207 210 1
Swap: 128 6 122
zram mm_stat: orig_data_size=9084928 compr_data_size=6935867 mem_used_total=8163328
스왑이 6MB 쓰였고(Swap: 128 6 122), zram은 그 중 9.08MB어치 데이터를 6.94MB로 압축해 보관했다 — 약 1.31배 압축률이다. all-zero 같은 극단적 사례가 아니라 실제 ELF 바이너리를 반복 저장한 값이라 이 정도가 현실적인 하한선에 가깝다.
제거 가능한 디버깅 옵션
defconfig는 CONFIG_SLUB_DEBUG=y, CONFIG_DEBUG_MEMORY_INIT=y도 기본으로 켜둔다. 문서에 “Disabling these can result in significant savings in code size”라고 명시돼 있는데, scripts/config --disable로 꺼봐도 olddefconfig가 다시 y로 되돌렸다.
grep -n "config SLUB_DEBUG" -A3 mm/Kconfig.debugconfig SLUB_DEBUG
default y
bool "Enable SLUB debugging support" if EXPERT
depends on SYSFS && !SLUB_TINY
원인은 default y + if EXPERT였다. CONFIG_EXPERT가 꺼져 있으면 이 옵션은 아예 프롬프트로 노출되지 않고 조용히 y로 고정된다 — CONFIG_DEBUG_MEMORY_INIT도 정확히 같은 패턴이다. 먼저 CONFIG_EXPERT를 켜야 진짜로 끌 수 있다.
./scripts/config --enable CONFIG_EXPERT
./scripts/config --disable CONFIG_SLUB_DEBUG
./scripts/config --disable CONFIG_DEBUG_MEMORY_INIT
./scripts/config --enable CONFIG_LRU_GEN
./scripts/config --enable CONFIG_LRU_GEN_ENABLED
make olddefconfig참고로 CONFIG_SWAP 자체를 끄면 CONFIG_HIBERNATION도 연쇄로 함께 꺼진다 — 하이버네이션은 스왑 영역에 이미지를 쓰는 방식이라 스왑 없이는 애초에 성립하지 않는다. 이 글에서는 zram으로 스왑을 유지하기로 했으니 해당 사항 없다.
Memory Reclaim과 Watermark
여유 메모리가 low watermark 밑으로 떨어지면 kswapd가 백그라운드로, min 밑으로 떨어지면 할당 요청 자체가 동기적으로 회수(direct reclaim)를 수행한다. 실측한 watermark 값이다.
min_free_kbytes: 4005
Node 0, zone DMA32
min 985 (≈3.9MB)
low 1231 (≈4.8MB)
high 1477 (≈5.8MB)
managed 247879 (≈968MB)
1GB 환경에선 4MB 남짓한 min_free_kbytes가 전체의 0.4%에 불과해 무시할 만하다. 하지만 이 값은 total_ram의 제곱근에 비례해 계산되므로, 128MB 시스템이라면 절대값은 줄어도 비중은 커진다 — 극단적으로 작은 타깃에서는 vm.min_free_kbytes를 직접 낮춰 볼 가치가 있다는 뜻이다. 다만 이 글의 부팅-직후-종료 테스트는 유휴 상태라 pgscan_kswapd/pgsteal_kswapd 등 회수 카운터가 전부 0이었다 — 실제 회수 동작을 보려면 의도적으로 메모리를 채워 압박을 줘야 하는데, 이 글에서는 거기까지 하지 않았다.
MGLRU (Multi-Gen LRU)
CONFIG_LRU_GEN은 defconfig에서 기본값이 꺼짐이다 — 존재는 하지만 옵트인이다. 세대별로 페이지를 더 세밀하게 나눠 추적해 회수 정확도를 높이는 게 목적이라, 우리처럼 이미지 크기를 줄이는 방향과는 반대로 코드가 늘어난다. 켠 뒤 실제로 활성화됐는지는 sysfs로 확인했다.
cat /sys/kernel/mm/lru_gen/enabled0x0007
비트마스크 0x7로 활성화는 확인했지만, 회수 효율이 실제로 얼마나 개선되는지는 인위적으로 메모리 압박을 걸어 재현해야 하는 별도 벤치마크 영역이라 이 글에서는 검증하지 않았다. 메모리가 아주 작은 시스템에서 코드 크기보다 회수 효율이 더 중요하다면 켜볼 만하고, 반대로 이미지 크기가 최우선이면 꺼두는 게 맞다 — 서로 반대 방향의 목표다.
종합 크기 비교
| 항목 | (4)편 결과 | +VM 옵션 정리(SWAP 유지+zram) | 변화 |
|---|---|---|---|
| bzImage | 8.947 MB | 8.934 MB | -0.15% |
| vmlinux (text+data+bss) | 25.50 MB | 25.45 MB | -0.20% |
| SwapTotal | 0 kB (스왑 디바이스 없음) | 131068 kB (zram0 압축 스왑) | +128MB 여유 |
SLUB_DEBUG/DEBUG_MEMORY_INIT을 끈 절감분을 zram+zsmalloc+MGLRU 추가분이 거의 다 상쇄해서 이미지 크기 자체는 큰 변화가 없다. 대신 스왑이라는 실제 기능을 128MB어치 확보했으니, 이번 옵션 정리의 목적은 “크기 절감”이 아니라 “같은 크기 예산 안에서 스왑이라는 실용적 여유를 사는 것”에 가깝다.
주의사항
scripts/config --disable가 안 먹히는 옵션이 있다면 Kconfig 정의를 확인해봐야 한다 —default y+if EXPERT같은 패턴이면CONFIG_EXPERT부터 켜야 실제로 끌 수 있다. (3)(4)편에서 쓴olddefconfig만으로는 이런 옵션은 계속 원래 값으로 되돌아간다.- zram이 있다고 메모리가 무한정 늘어나는 게 아니다 — 압축 스토리지도 결국 같은 물리 RAM을 쓴다. 처음엔 tmpfs에 240MB(105회 대신 120회 반복)를 채워봤는데, zram이 있는데도
Out of memory: Killed process ... (cat)에 이어Kernel panic - not syncing: System is deadlocked on memory로 커널이 죽었다 — OOM killer가 진짜 메모리를 많이 쓰는 프로세스가 아니라 사실상 무고한cat을 죽였는데도 회수가 안 됐고, PID 1(init)마저 메모리를 못 받아 데드락으로 판단된 것이다. 실제로 안전하게 흡수 가능한 여유는 “zram 크기 × 압축률”에서 크게 벗어나지 않는다 — zram 크기만 키운다고 압박이 무제한으로 사라지지 않는다. - 이 글의 watermark/reclaim 수치는 전부 유휴 상태에서 잰 값이다. 실제 메모리 압박 상황에서 kswapd/direct reclaim이 어떻게 도는지, MGLRU가 회수 효율을 실제로 개선하는지는 별도로 부하를 걸어 검증해야 하며 이 글에서는 하지 않았다.
마무리
VM 서브시스템 옵션은 이미지 크기보다 런타임 동작(캐시 회수 강도, 스왑 유무, watermark 여유분, 회수 알고리즘)에 더 크게 관여한다는 걸 확인했다. 다음 편에서는 (1)~(5)편의 모든 결과를 한 표로 모아 defconfig 베이스라인 대비 최종 절감폭을 정리하고 시리즈를 마무리한다.