← 문제로

19. 가상 메모리와 페이징: 주소 변환·페이지 폴트·copy-on-write

난이도 중
내 답안
모범답안

난이도: 중상

모범답안 — 가상 메모리와 페이징

1. 가상↔물리 주소 변환

프로세스는 연속돼 보이는 가상 주소 공간을 갖지만 실제 물리 RAM 은 조각나 있고 프로세스 간 격리돼야 한다. OS 는 주소 공간을 고정 크기 페이지(보통 4KB)로, 물리 메모리를 같은 크기 프레임으로 나누고, 페이지 테이블로 "가상 페이지 번호(VPN) → 물리 프레임 번호(PFN)" 를 매핑한다. 주소 = (페이지 번호, 오프셋)으로 쪼개, 페이지 번호로 테이블을 찾아 프레임을 얻고 오프셋을 붙여 물리 주소를 만든다.

단일 배열 페이지 테이블은 너무 크다: 48비트 가상 주소·4KB 페이지면 항목이 2^36 개라 프로세스마다 수백 GB 테이블이 필요하다. 그래서 다단계(multi-level) 페이지 테이블(x86-64 는 4~5단계)로 트리를 만들어 실제로 쓰는 영역만 하위 테이블을 할당한다. 대부분의 주소 공간이 비어 있으니 트리는 희소하게 유지돼 메모리를 아낀다. 변환은 CPU 의 MMU 가 하드웨어로 수행한다.

2. TLB

매 접근마다 4~5단계 테이블을 걷는 것은 느리므로, MMU 는 최근 변환(VPN→PFN)을 TLB 라는 작은 고속 캐시에 담는다. TLB 히트면 한 사이클에 변환 완료. TLB 미스면 하드웨어(또는 OS)가 페이지 테이블을 워크해 변환을 채우는데, 이는 여러 번의 메모리 접근이라 상대적으로 비싸다. 코드/데이터 지역성이 좋으면 TLB 히트율이 높다.

컨텍스트 스위칭 시 다른 프로세스는 다른 매핑을 쓰므로 TLB 를 플러시해야 한다. 매번 비우면 스위칭 직후 미스 폭풍이 나므로, 현대 CPU 는 ASID/PCID(주소공간 식별자)로 항목에 프로세스 태그를 붙여 플러시 없이 공존시킨다.

3. 페이지 폴트

접근한 가상 페이지가 물리 프레임에 없거나 권한이 안 맞으면 CPU 가 페이지 폴트 예외를 일으키고 OS 핸들러가 처리한다.

  • 마이너 폴트: 매핑만 없을 뿐 데이터는 이미 메모리에 있음(예: 공유 라이브러리 페이지가 페이지 캐시에 있거나, 지연 할당된 익명 페이지에 제로 페이지 연결). 디스크 I/O 없이 매핑만 채우면 되므로 빠르다(마이크로초 단위).
  • 메이저 폴트: 데이터를 디스크/스왑에서 읽어와야 함. 밀리초 단위로 느리다.

디맨드 페이징/지연 할당: malloc/mmap 은 가상 영역만 예약하고 물리 프레임은 첫 접근 시 폴트로 붙는다. 그래서 "할당은 성공했는데 첫 접근에서 느리다". 게임 서버가 큰 버퍼를 잡고 처음 쓸 때 폴트가 몰리면 그 틱만 지연이 튄다.

틱 지연 영향: 게임 루프는 일정 틱(예: 50ms)마다 도는데, 그 안에서 메이저 폴트(스왑 인)가 나면 수십 ms 가 날아가 프레임 드롭·입력 지연으로 나타난다. tail latency 관점에서 스왑은 실시간 서버의 적이다.

4. copy-on-write (COW)

fork 는 자식에게 부모 주소 공간을 복제하지만, 실제로는 물리 페이지를 즉시 복사하지 않고 부모·자식이 같은 프레임을 읽기 전용으로 공유한다. 어느 쪽이든 쓰기를 시도하면 폴트가 나고 OS 가 그 페이지만 복사해 사적으로 만든다(copy-on-write). 대부분 페이지는 안 바뀌므로 fork 비용이 싸진다(로그 스냅샷용 fork, Redis 의 BGSAVE 등).

함정: (1) fork 직후 큰 영역에 쓰기가 몰리면 COW 폴트+복사가 폭발해 지연이 튄다. (2) GC 언어에서 GC 가 객체 헤더(마크 비트)만 건드려도 페이지가 더럽혀져(dirtied) COW 복사가 유발 → fork 로 아낀 메모리가 도로 늘어난다. (3) fork 후 자식이 곧 exec 할 거면 COW 도 아깝다(vfork/posix_spawn 고려).

5. 서버 실무 적용

  • 사전 폴트: mmap(..., MAP_POPULATE) 나 시작 시 버퍼를 한 번 터치(touch)해 폴트를 로딩 단계로 몰아, 런타임 틱에서 폴트가 안 나게 한다.
  • mlock/mlockall: 핵심 상태 페이지를 물리 메모리에 고정해 스왑 아웃을 막는다(메이저 폴트 제거).
  • 스왑 비활성화/vm.swappiness=0: 실시간 서버는 스왑보다 OOM 킬을 택하는 편이 tail latency 에 낫다는 판단을 하기도 한다(트레이드오프: OOM 위험).
  • huge page(2MB/1GB): 한 TLB 항목이 더 넓은 영역을 커버해 TLB 미스를 줄인다(대용량 맵/캐시에 유효). 단, 내부 단편화·THP 지연 이슈 주의.
  • NUMA 인지 배치: 워커 스레드가 접근하는 메모리를 같은 노드에 두어 원격 접근 지연을 줄인다.

핵심 요약

가상 메모리는 격리·유연성을 주지만 공짜가 아니다 — 변환 비용(TLB), 지연 매핑(페이지 폴트), 공유 비용(COW)이 실시간 서버의 tail latency 로 드러난다. 큰 인메모리 상태를 다루는 서버는 폴트를 로딩 단계로 몰고, 핵심 페이지를 고정하고, TLB 커버리지를 넓혀 런타임 지연을 억제한다.