각 프로세스는 수십 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 높음(150μs10ms)
SIGSEGV 매핑 자체가 없거나 권한 위반 프로세스 종료

COW(Copy-On-Write) — fork가 빠른 이유

fork() 시 부모의 모든 페이지를 복사하면 느리다. 리눅스는 PTE를 읽기 전용으로 공유한다. 어느 한쪽이 쓰기하면 그때 minor 폴트로 그 페이지만 복사. fork 비용을 극적으로 낮춘다(03장 참조).

"free가 적은 건 정상이다" — 페이지 캐시

초보자가 free를 보고 "free가 거의 없다, 메모리 부족!"이라 패닉하는 게 가장 흔한 실수다. 틀렸다. 리눅스는 남는 RAM을 자동으로 디스크 캐시(페이지 캐시)로 쓴다. 빈 RAM은 낭비라는 철학 때문이다.

  • 파일 읽기/쓰기는 페이지 캐시를 거친다(direct I/O가 아니면).
  • RAM 여유가 많으면 캐시가 커진다 → 다음 읽기가 빨라진다.
  • 메모리 압력이 오면 커널이 캐시를 회수해 프로세스에 준다.

그래서 판단은 free 컬럼이 아니라 available 컬럼으로 해야 한다. free -hbuff/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이 잦으면 이 차이만큼 느려진다.

생각해 볼 문제

  1. 가상 메모리가 없다면 어떤 3가지 문제가 생기는가?
  2. malloc(1GB) 직후와 그 1GB에 실제로 접근한 후의 물리 메모리 점유량 차이는? 왜?
  3. free에서 free가 낮은데 시스템이 정상인 이유는? 무엇을 봐야 하는가?
  4. COW가 fork 비용을 어떻게 낮추는가?
  5. swap이 잦으면 왜 시스템이 느려지는가? (수치로)
  6. OOM killer가 RSS 큰 프로세스를 우선 잡는 이유는? 중요 프로세스는 어떻게 보호하는가?

참고

'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