각 프로세스는 수십 TB 메모리를 독점한다고 착각한다 — 가상 메모리의 속임
서버에서 free를 치면 "free가 거의 없다"고 나온다. 메모리 부족인가, 패닉할 일인가? 보통 아니다. 또 누군가 malloc(1GB)를 호출하면 즉시 1GB RAM이 사라질까? 그것도 아니다. 두 사실 모두 리눅스 메모리 관리가 만들어내는 환상의 결과다.
이 글은 그 환상을 다룬다 — 각 프로세스가 "자기만의 거대한 주소 공간"을 독점하는 듯 보이게 만드는 가상 메모리, RAM이 부족할 때 디스크까지 끌어쓰는 swap, 그리고 모조리 고갈됐을 때 시스템을 살리기 위해 프로세스를 죽이는 OOM killer까지. 이 셋을 모르면 free/vmstat 숫자를 읽을 수 없고, "서버가 느린데 CPU는 한가하다" 같은 메모리 병목을 진단하지 못한다.
가상 메모리 — 각자 자기만의 주소 세계
각 프로세스는 물리 RAM과 무관한 고유의 가상 주소 공간을 본다. x86_64에서 약 256 TiB(4-level) 또는 128 PiB(5-level). 물리 RAM은 8GB여도, 프로세스는 자기가 256 TB를 쓸 수 있다고 믿는다.
flowchart LR
V["가상 주소<br/>프로세스 관점"] --> MMU["MMU + TLB"]
MMU --> PT["페이지 테이블<br/>(커널 관리)"]
PT --> P["물리 페이지 프레임"]
왜 이 속임수를 쓸까. 세 가지 문제를 한꺼번에 푼다:
- 격리/보안 — 프로세스 A의 "주소 100"과 프로세스 B의 "주소 100"은 물리적으로 다른 곳. 서로 침범 불가.
- RAM보다 큰 공간 — demand paging(필요할 때만 할당)으로 실제 RAM을 넘는 주소 공간을 다룬다.
- 효율적 공유 — 같은 물리 페이지를 여러 프로세스가 공유(COW, 공유 라이브러리).
호텔 방 번호와 비슷하다: 각 층에 101호가 있지만 물리적 위치는 다르다. 단, 가상 메모리는 페이지가 디스크(swap)로도 갈 수 있고 여러 프로세스가 같은 물리 페이지를 공유할 수 있다는 점에서 호텔 비유를 넘어선다.
페이지, 페이지 테이블, TLB — 변환의 비용
메모리를 다루는 최소 단위는 페이지(x86_64 기본 4 KiB)다. 가상 주소 → 물리 주소 변환 정보는 페이지 테이블(계층 구조: PGD → P4D → PUD → PMD → PTE)에 있고, 이 변환을 캐시하는 하드웨어가 TLB(CPU MMU 내)다.
왜 TLB가 중요한가. 수치로 보자.
| 지연 | 비고 | |
|---|---|---|
| L1 캐시 접근 | ~0.5 ns | |
| TLB 히트 | ~1 ns | 변환 캐시에서 즉시 |
| 메인 메모리 접근 | ~100 ns | |
| TLB 미스 → 페이지 워크 | ~60-100 ns | 페이지 테이블을 RAM에서 탐색 |
TLB 미스는 매 접근마다 메모리 탐색을 강제한다. 최악의 경우 CPU가 99% 시간을 주소 변환에 낭비할 수도 있다. 그래서 huge page(2 MiB/1 GiB)로 TLB 커버리지를 넓힌다 — 4 KiB 페이지 64개 엔트리는 256 KiB를 커버하지만, 2 MiB huge page 64개는 128 MiB를 커버. 대형 메모리 작업(DB)에 필수다.
malloc은 약속일 뿐이다 — demand paging
프로그램이 malloc(1GB)로 1GB를 요청하면, 즉시 1GB RAM이 할당될까? 아니다. malloc은 "이 주소 구간을 쓸 수 있다"는 약속만 할 뿐, 실제 물리 페이지는 그 주소에 처음 접근하는 순간에야 할당된다. 이걸 demand paging(요구 시 할당)이라 한다. 1GB를 malloc하고 1MB만 쓰면, 실제 RAM은 1MB만 점유한다. "오버 커밋"의 원리다.
접근 순간 페이지가 없으면 → 페이지 폴트 예외 → 커널이 그제야 물리 페이지를 확보하고 매핑 → 프로세스는 모르고 계속 실행.
flowchart TD
A[프로세스 메모리 접근] --> B{TLB hit?}
B -->|예| RUN[실행]
B -->|아니오| C[페이지 워크]
C --> D{PTE 존재?}
D -->|예| RUN
D -->|아니오| FAULT[페이지 폴트]
FAULT --> T{유형}
T -->|minor| M[zero/COW 페이지 할당<br/>디스크 I/O 없음]
T -->|major| MA[디스크 읽기<br/>파일/swap-in]
| 폴트 유형 | 의미 | 비용 |
|---|---|---|
| minor | COW, lazy alloc, 최초 zero 페이지 | 낮음(메모리만) |
| major | 파일 mmap 미스 또는 swap-in → 디스크 I/O | 높음( |
| SIGSEGV | 매핑 자체가 없거나 권한 위반 | 프로세스 종료 |
COW(Copy-On-Write) — fork가 빠른 이유
fork() 시 부모의 모든 페이지를 복사하면 느리다. 리눅스는 PTE를 읽기 전용으로 공유한다. 어느 한쪽이 쓰기하면 그때 minor 폴트로 그 페이지만 복사. fork 비용을 극적으로 낮춘다(03장 참조).
"free가 적은 건 정상이다" — 페이지 캐시
초보자가 free를 보고 "free가 거의 없다, 메모리 부족!"이라 패닉하는 게 가장 흔한 실수다. 틀렸다. 리눅스는 남는 RAM을 자동으로 디스크 캐시(페이지 캐시)로 쓴다. 빈 RAM은 낭비라는 철학 때문이다.
- 파일 읽기/쓰기는 페이지 캐시를 거친다(direct I/O가 아니면).
- RAM 여유가 많으면 캐시가 커진다 → 다음 읽기가 빨라진다.
- 메모리 압력이 오면 커널이 캐시를 회수해 프로세스에 준다.
그래서 판단은 free 컬럼이 아니라 available 컬럼으로 해야 한다. free -h의 buff/cache가 바로 이 영역. available이 충분하면 free가 낮아도 정상이다.
RAM이 모자랄 때 — swap과 zram
RAM이 꽉 찼다. 새 할당 요청이 오면? (1) 캐시 회수 (2) 그래도 모자라면 — 사용 빈도 낮은(cold) 페이지를 디스크로 쫓아낸다. 이게 swap이다.
- swap 파티션 또는 swap 파일.
vm.swappiness(0~200, 기본 60): 커널이 anonymous 페이지(프로세스 힙/스택)를 swap할 적극성. 높을수록 swap 선호.
zram은 RAM 일부를 압축하여 swap 장치로 사용하는 블록 장치(/dev/zramN)다. 디스크 I/O 없이 RAM 내에서 압축/해제 → 일반 swap보다 훨씬 빠르다. 메모리가 작은 시스템(라즈베리파이, 임베디드)에서 특히 효과적. Rocky 10 서버 설치 시 기본은 swap 파티션이며, zram은 별도 설정(zram-generator)으로 추가한다.
왜 swap이 느린가 — 수치로
| 지연 | 비고 | |
|---|---|---|
| 메모리 접근 | ~100 ns | |
| zram swap-in | ~수 μs | RAM 내 압축 해제 |
| SSD swap-in | ~150 μs | 디스크 읽기(메모리의 1500배) |
| HDD swap-in | ~10 ms | 디스크 탐색(메모리의 10만 배) |
swap이 잦으면 시스템이 이 수치 차이만큼 느려진다. 특히 HDD 서버에서 스와핑 폭주(thrashing)는 치명적이다. 잦은 스와핑은 RAM 증설 신호다.
swap을 0으로 두면? 고갈 시 즉시 OOM kill이 발생한다. 안정성을 위해 일정 swap을 권장한다. swappiness=0도 "안 하려 할 뿐"이지 절대 금지가 아니다 — 극한엔 여전히 swap한다.
최후의 방어 — OOM killer
swap까지 썼는데도 메모리가 완전 고갈되면, 시스템 전체가 멈추기 직전이 된다. 이때 시스템 보존을 위해 프로세스를 강제 종료하는 커널의 최후 방어가 OOM(Out-Of-Memory) killer다.
flowchart LR
LOW[메모리 고갈] --> RECL[페이지 회수<br/>캐시/swap]
RECL -->|부족| OOM[OOM killer 호출]
OOM --> SCORE[각 프로세스 oom_score 계산]
SCORE --> KILL[최고 점수 프로세스에 SIGKILL]
- oom_score ≈ RSS + 0.5 × swap 사용량에 가중치.
/proc/<pid>/oom_score_adj(-1000 ~ 1000)로 점수 조정:-1000은 OOM kill 대상에서 사실상 제외(systemd가 journald/dbus에 적용),1000은 최우선 kill 대상.
OOM은 무작위가 아니다. RSS가 큰(메모리 많이 쓰는) 프로세스가 희생된다. DB처럼 큰 메모리 프로세스가 잡히기 쉬우므로 oom_score_adj=-1000으로 보호하는 게 정상이다.
Rocky 10에서 직접 확인하기
미검증(출처 인용). Rocky 10 VM에서 실행 권장.
# Rocky 10 — 진짜 부족한가
free -h
판단: free가 낮아도 available이 충분하면 정상(캐시가 차지한 것). 진짜 부족 신호는 available 낮음 + 지속적 swap I/O.
total used free shared buff/cache available
Mem: 3.8Gi 1.2Gi 512Mi 120Mi 2.1Gi 2.2Gi
Swap: 2.0Gi 0.0Gi 2.0Gi
vmstat 1 5
읽는 법: si(swap-in)/so(swap-out)가 계속 0보다 크면 스와핑 발생 → 메모리 부족 신호.
cat /proc/meminfo | head -20
# MemTotal/MemFree/MemAvailable/Cached/SwapTotal/SwapFree
# Rocky 10 — swap 관리
swapon --show
sudo swapoff /dev/sdXN
cat /proc/sys/vm/swappiness # 보통 60
sudo sysctl vm.swappiness=10 # 임시 변경
# 영구: /etc/sysctl.d/99-swappiness.conf
# Rocky 10 — zram 설정(옵션)
sudo modprobe zram num_devices=1
sudo zramctl /dev/zram0 --algorithm zstd --size 2G
sudo mkswap /dev/zram0
sudo swapon -p 100 /dev/zram0 # 우선순위 높게(디스크 swap보다 먼저)
zramctl
# Rocky 10 — OOM 점수: 누가 희생될까
for p in $(pgrep -f postgres); do
echo "$p: $(cat /proc/$p/oom_score)"
done
# 중요 프로세스 보호 (예: PID 1234)
echo -1000 | sudo tee /proc/1234/oom_score_adj
# OOM 이력
journalctl -k | grep -i "out of memory"
cat /sys/kernel/mm/transparent_hugepage/enabled # [always] madvise never
grep Huge /proc/meminfo
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "free가 적으면 메모리 부족" | 틀림. buff/cache가 차지한 것. available로 판단 |
| "swap은 쓰면 안 좋다, 0으로 두자" | swap 0이면 고갈 시 즉시 OOM kill. 안정성 위해 일정 swap 권장 |
| "OOM은 무작위로 잡는다" | 아니다. RSS 큰 프로세스 우선. DB가 희생되기 쉬움 |
| "swappiness=0이면 swap 안 한다" | "안 하려 할 뿐"이지 절대 금지 아님. 극한엔 여전히 swap |
"malloc(1GB)하면 1GB 즉시 할당" |
아니다. demand paging. 실제 접근 전까지 물리 페이지 안 줌 |
| "fork는 느리다(전체 복사)" | COW로 사실상 거의 안 복사. 매우 빠름 |
| "페이지 캐시는 낭비다" | 아니다. 빈 RAM이 낭비. 캐시가 있어야 다음 읽기가 빠름 |
더 깊이
- overcommit 정책:
vm.overcommit_memory— 0(휴리스틱 기본), 1(항상 승인), 2(엄격, CommitLimit 초과 시 실패). overcommit=1이면malloc은 성공하지만 접근 시 OOM 발생 가능. 메모리 집약 서버는 2(엄격) 고려. - NUMA: 다소켓 서버는 CPU 소켓마다 가까운/먼 메모리가 있다.
numactl로 바인딩. 단일 소켓 시스템은 무관.
요약 — 이 글의 결론
- 가상 메모리: 각 프로세스는 자기만의 거대 주소 공간(수십 TB)을 본다. 물리 RAM과 무관. 격리·보호·공유의 기반.
- 페이지(4 KiB) + 페이지 테이블 + TLB. TLB 미스는 비싸므로 huge page로 커버리지를 넓힌다.
- demand paging: 접근할 때 물리 페이지 할당.
malloc은 약속일 뿐이다. - 페이지 폴트: minor(메모리만), major(디스크 I/O), SIGSEGV(권한/매핑 없음).
- COW: fork 시 페이지 공유, 쓰기 시 복사 → fork가 빠르다.
- 페이지 캐시: 남는 RAM은 디스크 캐시. 판단은
available(free 아님). "free 적은 건 정상". - swap: RAM 부족 시 디스크로. zram은 RAM 내 압축 swap(더 빠름). swappiness로 조절.
- OOM killer: 고갈 시 RSS 큰 프로세스를 SIGKILL.
oom_score_adj=-1000으로 보호. - 체감: 메모리 100ns vs SSD 150μs(1500배) vs HDD 10ms(10만 배). swap이 잦으면 이 차이만큼 느려진다.
생각해 볼 문제
- 가상 메모리가 없다면 어떤 3가지 문제가 생기는가?
malloc(1GB)직후와 그 1GB에 실제로 접근한 후의 물리 메모리 점유량 차이는? 왜?free에서free가 낮은데 시스템이 정상인 이유는? 무엇을 봐야 하는가?- COW가 fork 비용을 어떻게 낮추는가?
- swap이 잦으면 왜 시스템이 느려지는가? (수치로)
- OOM killer가 RSS 큰 프로세스를 우선 잡는 이유는? 중요 프로세스는 어떻게 보호하는가?
참고
- Memory Management Concepts — Linux Kernel docs - 접근 2026-07-09
- Page Tables — Linux Kernel docs - 접근 2026-07-09
- zram — Linux Kernel docs - 접근 2026-07-09
- Latency Numbers — Google SRE - 접근 2026-07-09
- man page:
man free,man vmstat,man swapon,man proc(5),man mmap(2)
'Tech Artifacts > Linux' 카테고리의 다른 글
| Linux - 06. io device (0) | 2026.07.09 |
|---|---|
| Linux - 05. filesystem (0) | 2026.07.09 |
| Linux - 03. process (0) | 2026.07.09 |
| Linux - 02. boot systemd (0) | 2026.07.09 |
| Linux - 01. overview (0) | 2026.07.08 |
