Skip to content

✍️ 필사 모드: 가상 메모리 내부 완전 정복 — 페이지 테이블, TLB, mmap, Copy-on-Write, OOM, Huge Pages까지 (2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

0. 이상한 현상 3가지 — 가상 메모리가 없으면 설명 불가

현상 1: malloc 거짓말

void* p = malloc(1ULL * 1024 * 1024 * 1024 * 100);  // 100GB
printf("%p\n", p);  // 성공!

물리 RAM 이 16GB 인데 100GB 할당이 성공한다. 왜?

현상 2: fork 의 속도

pid_t pid = fork();

10GB 를 쓰고 있는 프로세스에서 fork()1ms 이하 에 끝난다. 10GB 를 복사하려면 몇 초는 걸려야 할 텐데.

현상 3: top 의 숫자가 안 맞음

PID USER  VIRT     RES    SHR   COMMAND
123 alice 2.3G    180M   45M   chrome

VIRT (가상 메모리) 가 2.3GB 인데 RES (실제 사용) 가 180MB? 이 셋은 뭐가 다른가?

이 세 가지는 모두 가상 메모리 (Virtual Memory) 라는 같은 뿌리에서 나온다. 이 글은 가상 메모리의 작동 원리와 현대 OS 가 어떻게 이를 레버리지하는지 파헤친다.

1. 가상 메모리 — 왜 발명됐는가

1.1 물리 메모리 직접 접근의 재앙

1960년대 초 프로그램은 물리 주소에 직접 접근했다. 문제가 많았다:

  • 프로세스 간 간섭: 프로세스 A 가 프로세스 B 의 메모리를 덮어쓸 수 있음.
  • 주소 충돌: 두 프로그램이 같은 주소 0x1000 에 데이터를 두려고 함.
  • 메모리 단편화: "사용 가능한 공간 2GB 는 있지만 연속된 1GB 는 없음".
  • 확장성 없음: 프로그램이 RAM 보다 클 수 없음.

1.2 1962년 Manchester Atlas 와 Multics 의 혁명

Manchester 대학의 Atlas 컴퓨터 (1962) 가 최초의 가상 메모리 시스템을 구현. MIT Multics (1965) 가 이를 발전시켜 세그먼트 + 페이지 혼합 모델을 제안.

핵심 아이디어:

"각 프로세스에게 자기만의 거대한 가상 주소 공간 을 준다. 물리 RAM 은 OS 가 뒤에서 관리한다."

  • 프로세스 A 의 주소 0x1000 과 프로세스 B 의 주소 0x1000 은 다른 물리 메모리 를 가리킨다.
  • 가상 주소가 물리보다 클 수 있다 → 디스크로 스와핑 가능.
  • OS 가 접근 권한을 페이지 단위로 제어.

1985년 386 CPU 이 x86에 페이징을 도입한 이후 모든 범용 OS 의 표준이 됐다.

2. 페이지 테이블 — 가상→물리 변환의 자료구조

2.1 페이지 단위의 매핑

가상 메모리의 기본 단위는 페이지 (보통 4KB). 물리 RAM 도 같은 크기의 프레임 으로 나뉜다. 페이지 테이블은 "가상 페이지 N → 물리 프레임 M" 매핑 테이블.

왜 4KB 인가?

  • 너무 작으면: 페이지 테이블이 커지고 TLB 효율 저하.
  • 너무 크면: 내부 단편화 (한 페이지에 5바이트만 쓰는데 4KB 할당).
  • 4KB 는 1970년대 VAX 이후 사실상 표준. ARM 은 16KB 도 지원.

2.2 단일 레벨 페이지 테이블의 문제

x86-64 에서 가상 주소는 48비트 (실제 사용), 페이지는 4KB (12비트).

→ 가상 페이지 수 = 2^36 = 640억 개. → 각 엔트리가 8바이트면 페이지 테이블만 512GB.

프로세스마다 512GB 를 페이지 테이블에 쓰는 건 불가능. 해결: 다단계 페이지 테이블.

2.3 x86-64 의 4단계 페이지 테이블 (5단계도 있음)

가상 주소 (48-bit):
[ PML4 (9) | PDPT (9) | PD (9) | PT (9) | Offset (12) ]

PML4 (Page Map Level 4)
[PML4 index 로 엔트리 선택]PDPT 주소
PDPT (Page Directory Pointer Table)
[PDPT index]PD 주소
PD (Page Directory)
[PD index]PT 주소
PT (Page Table)
[PT index]                   → 실제 물리 프레임 주소
  • 각 테이블은 512개 엔트리 (9비트 인덱스).
  • 엔트리 크기 8바이트 → 각 테이블 = 4KB (한 페이지에 딱 맞음).
  • Sparse 영역 은 상위 엔트리가 NULL → 하위 테이블 아예 만들지 않음.

실제 프로세스의 페이지 테이블은 보통 수 MB 수준.

2.4 페이지 테이블 엔트리의 비트들

x86-64 PTE 구조 (64비트):

[63 NX | 62..52 Reserved | 51..12 Physical Frame Addr | 11..9 AVL | 8 G | 7 PAT | 6 D | 5 A | 4 PCD | 3 PWT | 2 U/S | 1 R/W | 0 P]

주요 비트:

  • P (Present): 0 이면 "이 페이지는 현재 RAM 에 없음" → 접근 시 페이지 폴트.
  • R/W: 쓰기 가능 여부.
  • U/S: 사용자 모드 접근 허용.
  • A (Accessed): CPU 가 접근 시 자동으로 1 세트 → LRU 힌트.
  • D (Dirty): 쓰기 시 자동으로 1 → 디스크 writeback 필요.
  • NX (No Execute): 이 페이지에서 코드 실행 금지 → 버퍼 오버플로우 공격 방어.

OS 는 이 비트들을 읽고 쓰면서 메모리 관리를 한다.

2.5 CR3 레지스터 — 프로세스 전환의 핵심

CPU 의 CR3 레지스터는 현재 프로세스의 PML4 주소를 가진다. fork() 나 문맥 전환 시:

ctx_switch():
  mov new_process->pgd_phys, %rax
  mov %rax, %cr3    # 페이지 테이블 교체!

이 한 줄이 "프로세스 A 의 주소 공간" → "프로세스 B 의 주소 공간" 전환.

부작용: CR3 을 바꾸면 TLB (Translation Lookaside Buffer) 가 대부분 플러시 → 문맥 전환의 숨은 비용.

3. TLB — 주소 변환의 캐시

3.1 4단계 페이지 워크의 비용

단순 계산: 가상 주소 → 물리 주소 변환마다 4번의 메모리 접근 필요 (PML4, PDPT, PD, PT). CPU 명령 하나 실행하려고 메모리 5번 접근 — 재앙.

3.2 TLB 의 구조

TLB 는 "최근에 변환한 가상 → 물리 매핑" 을 저장하는 작은 하드웨어 캐시:

  • 크기: 보통 64~1024 엔트리.
  • 접근 시간: 1 사이클 (메모리는 100+ 사이클).
  • CPU 칩 안에 내장.
TLB hit: 1 cycle
TLB miss: 4-cycle page walk (실제로는 캐시 덕분에 덜 들지만)

3.3 TLB 친화적 코드 작성

TLB 미스를 줄이는 핵심은 공간 지역성:

// 나쁨: 열 우선 접근
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += a[i][j];  // 매번 다른 페이지 접근

// 좋음: 행 우선 접근
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += a[i][j];  // 같은 페이지 내 순차 접근

대규모 배열 (>TLB 용량 × 페이지 크기) 에서 이 차이가 10배 이상 성능 차이로 이어진다.

3.4 TLB 미스의 숨겨진 비용

행렬 곱셈 벤치마크에서 캐시 미스보다 TLB 미스가 병목인 경우가 많다. Intel 의 한 연구에서는 대형 데이터 처리 시 실행 시간의 20-30% 가 TLB 미스 관련.

해결책: Huge Pages (다음 장).

4. Huge Pages — TLB 커버리지 확장

4.1 표준 페이지의 한계

4KB 페이지 × 1024 TLB 엔트리 = 4MB 만 TLB 로 커버 가능. 10GB 짜리 데이터베이스는 TLB 미스 지옥.

4.2 Huge Page 의 해법

x86-64 는 페이지 테이블 계층을 "조기 종료" 로 큰 페이지 지원:

  • PD 레벨에서 종료 → 2MB 페이지.
  • PDPT 레벨에서 종료 → 1GB 페이지.

2MB 페이지 × 1024 TLB 엔트리 = 2GB 커버 → 데이터베이스, JVM 힙, HPC 배열에 매우 효과적.

4.3 Explicit Huge Pages vs Transparent

Explicit: /proc/sys/vm/nr_hugepages 로 예약. mmap 시 MAP_HUGETLB 명시. Postgres, Oracle, JVM 이 이 방식 활용.

void* p = mmap(NULL, 2*1024*1024, PROT_READ|PROT_WRITE,
               MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);

Transparent (THP): 커널이 자동으로 인접 4KB 페이지들을 2MB 로 병합. 투명 → 앱 수정 불필요.

4.4 THP 의 함정

THP 는 좋아 보이지만 프로덕션에서 악명 높다:

  • khugepaged 백그라운드 스캔: CPU 사용.
  • 메모리 단편화: 2MB 연속 공간 확보를 위해 compaction → latency spike.
  • Redis / MongoDB 가 THP 끄기 권장: 특정 워크로드에서 레이턴시 폭발.
echo never > /sys/kernel/mm/transparent_hugepage/enabled

2015년 Redis 문서에 이 설정이 공식적으로 명시된 이유.

5. 페이지 폴트 — 지연된 할당의 마법

5.1 페이지 폴트의 3가지 유형

프로세스가 가상 주소에 접근했는데 PTE 의 P 비트가 0 → 페이지 폴트 발생. CPU 가 OS 에게 제어권을 넘김. OS 는 이유를 판별:

  1. Minor Fault (Soft Fault): 페이지가 메모리엔 있지만 이 프로세스 PTE 가 연결 안 됨. 즉시 연결하고 복귀.
  2. Major Fault (Hard Fault): 페이지가 디스크 (스왑 또는 파일) 에 있음. 디스크 I/O 필요 → 수 ms.
  3. Invalid Fault: 정말로 권한 없는 접근 → SIGSEGV (Segmentation Fault).

5.2 Demand Paging — malloc 의 거짓말 해설

void* p = malloc(100 * 1024 * 1024);  // 100MB

이 순간 커널이 한 일 :

  • 가상 주소 공간에 100MB 예약 (mmap 으로).
  • 물리 페이지는 한 개도 할당 안 함.
  • 페이지 테이블 엔트리 설정, P=0.
memset(p, 0, 100 * 1024 * 1024);

여기서 실제로 접근 → 페이지 폴트 연쇄 → 커널이 접근한 페이지만 물리 프레임 할당 + zero-filling.

이게 "malloc 100GB" 가 즉시 성공하는 이유. 하지만 실제로 쓰려고 하면 그때 OOM 이 발생.

5.3 Overcommit 정책

리눅스의 /proc/sys/vm/overcommit_memory:

  • 0 (default): 휴리스틱. "명백히 말도 안 되는" 할당만 거절.
  • 1: 항상 허용 (Redis 가 요구).
  • 2: 엄격. swap + RAM × ratio 를 넘으면 거절.

모드 0 에서 malloc 은 낙관적으로 성공시키지만, 실제 접근 시 물리 메모리가 부족하면 OOM Killer 가 등장한다 (7장).

6. mmap — 모든 메모리 매핑의 통합 인터페이스

6.1 malloc 은 사실 mmap + brk

glibc malloc 의 내부:

  • 작은 할당 (128KB 미만 기본): brk 시스템 콜로 힙 확장.
  • 큰 할당 (128KB 이상): mmap(MAP_ANONYMOUS) 로 독립 매핑.

brk 는 연속된 힙 영역을 관리하는 포인터를 움직이는 것. mmap 은 아무 가상 주소에나 새 매핑을 만든다.

6.2 파일 매핑 — mmap 의 진짜 위력

int fd = open("huge.dat", O_RDONLY);
char* data = mmap(NULL, file_size, PROT_READ, MAP_SHARED, fd, 0);
// 이제 data[i] 가 파일의 i번째 바이트!

무엇이 일어났나?

  • 가상 주소 공간에 file_size 만큼 영역 예약.
  • 접근 시 페이지 폴트 → 커널이 해당 페이지만 디스크에서 읽음.
  • 페이지 캐시에 저장되고 공유됨 → 다른 프로세스가 같은 파일 mmap 시 같은 물리 메모리 공유.

6.3 mmap vs read 의 트레이드오프

mmap 장점:

  • 단일 주소 공간에서 큰 파일 접근 (포인터 연산).
  • 페이지 캐시 공유.
  • 시스템 콜 오버헤드 없음 (접근은 일반 메모리 접근).

mmap 단점:

  • 페이지 폴트가 예측 불가능한 지연 을 유발.
  • 큰 파일에 무작위 접근 시 TLB 미스 증가.
  • Signal handler 에서 SIGBUS (디스크 오류 시) 처리가 까다로움.
  • 공유 매핑에 쓸 때 동기화가 미묘 (msync 필요).

2020 CMU 연구 ("Are You Sure You Want to Use MMAP in Your Database?") 는 DBMS 가 mmap 을 쓰지 말라고 경고. I/O 예측성과 에러 처리가 이유.

6.4 익명 매핑과 공유

MAP_PRIVATE | MAP_ANONYMOUS  : 프로세스 전용, 파일 없음 (일반 malloc)
MAP_SHARED  | MAP_ANONYMOUS  : 프로세스 간 공유 (shm)
MAP_PRIVATE | fd             : 파일 기반, 쓰기는 CoW
MAP_SHARED  | fd             : 파일 기반, 쓰기가 파일에 반영

MAP_SHARED | MAP_ANONYMOUS 는 부모-자식 (fork 후) 또는 POSIX shared memory 에서 활용.

7. Copy-on-Write — fork 가 빠른 이유

7.1 fork 의 전통적 모델 vs CoW

전통: 부모의 모든 메모리를 자식에게 복사. 10GB 프로세스 → 10GB 복사 → 수 초.

CoW (Copy-on-Write): 페이지 테이블만 복사. 물리 페이지는 공유. 쓰기 발생 시에만 그 페이지를 복사.

7.2 CoW 내부 동작

fork() 직후:
  Parent PT: VA 0x1000PA 0x42000 (R--, Shared)  # W 비트 꺼짐!
  Child PT:  VA 0x1000PA 0x42000 (R--, Shared)

Child0x1000 에 쓰기:
  페이지 폴트 (R-- 에 쓰기 시도)
  OS: 이건 CoW 페이지다 → PA 0x42000 을 새 페이지 PA 0x80000 에 복사
  Child PT: VA 0x1000PA 0x80000 (RW-)
  쓰기 재시도 → 성공

7.3 exec 이 따라올 때 — fork 의 허세

일반적 패턴:

pid_t pid = fork();
if (pid == 0) {
    execvp("ls", argv);  // 자식 프로세스 이미지 완전 교체
}

exec 이 모든 CoW 페이지를 버리므로 사실상 fork 의 CoW 복사는 낭비. 해결: posix_spawn 또는 vfork — 페이지 테이블 복사도 생략.

7.4 Redis RDB 저장의 구현

Redis 는 백그라운드 저장 시 fork 를 쓴다:

부모: 계속 쿼리 처리 (쓰기 → CoW 발생 → 메모리 증가)
자식: 시점의 일관된 스냅샷을 디스크에 기록

문제: 부모가 계속 쓰면 쓰기 증폭 → 메모리 피크. 최악의 경우 메모리 2배 필요. 2018년 이전 Redis 운영자의 흔한 악몽.

7.5 Kernel Same-page Merging (KSM)

반대 방향: 여러 VM 이 같은 내용의 페이지를 가지고 있으면 하나로 병합. 페이지 A, B 가 같은 바이트 → 모두 PA 0x42000 을 가리키고 원본은 해제. 쓰기 시 CoW.

KVM, Xen 같은 하이퍼바이저에서 메모리 오버커밋에 활용. CPU 비용이 크므로 기본 비활성.

8. OOM Killer — 리눅스의 최후 수단

8.1 OOM 의 순간

물리 메모리 + 스왑 모두 고갈 → 커널이 어떻게든 메모리를 회수해야 함. oom_killer 가 등장.

8.2 OOM 점수 계산

/proc/<pid>/oom_score: 각 프로세스에 점수 매김.

  • RSS 크기 비례 (큰 놈이 죽을 확률 ↑).
  • oom_score_adj 로 사용자가 조정 (-1000 ~ 1000).
  • root 프로세스는 덜 죽음.
  • 자식이 많은 프로세스는 죽어도 효과 작다고 판단.

8.3 잔혹한 현실

Out of memory: Killed process 1234 (postgres) total-vm:...kB, anon-rss:...kB

dmesg 에 이 메시지가 찍히는 순간 postgres 가 죽었다. 경고 없이, 즉시. 시그널 SIGKILL, 핸들러 실행 불가, 정상 종료 루틴 없음. 트랜잭션이 날아갈 수 있음.

8.4 회피 전략

oom_score_adj = -1000: "이 프로세스는 절대 죽이지 마". 모니터링 / DB 를 보호.

echo -1000 > /proc/<pid>/oom_score_adj

swap 확보: 스왑이 있으면 OOM 전에 페이지 아웃.

cgroup memory limit: 컨테이너 단위로 메모리 제한 → 한 놈이 전체를 잡아먹지 않게.

overcommit=2: 엄격 모드. malloc 이 실패하게 만들고 OOM 을 사전 방지. 단, 많은 앱이 overcommit 을 전제로 설계돼 있어서 부작용 큼.

8.5 Early OOM — Ubuntu 22.04 의 선제 대응

시스템이 OOM 에 가깝게 가면 일반 시스템이 매우 느려진다 (스와핑 지옥). earlyoom 은 이 현상을 감지해 OOM 보다 먼저 메모리 사용이 큰 프로세스를 죽임. Fedora, Ubuntu 22.04+ 에서 기본.

9. Swap — 디스크를 메모리처럼

9.1 스왑의 역할

물리 메모리가 부족할 때 덜 쓰이는 페이지를 디스크로 내보내고, 필요 시 다시 읽어들임.

  • Swap partition vs Swap file: 현대 리눅스에서는 성능 차이 거의 없음.
  • zswap, zram: 압축된 메모리를 스왑처럼 사용 (디스크 I/O 없음).

9.2 swappiness — 얼마나 적극적으로 스왑할지

/proc/sys/vm/swappiness (0-100):

  • 0: 스왑을 최후 수단으로.
  • 60 (기본): 균형.
  • 100: 적극적 스왑.

DB 서버는 보통 10-20 으로 설정 → "캐시는 유지, 프로세스 메모리는 가능한 한 RAM 에".

9.3 Anonymous vs File-backed

커널은 두 종류를 구분:

  • Anonymous memory (heap, stack): 스왑 아웃되면 swap 영역에 저장.
  • File-backed memory (mmap'd files, code): 스왑 필요 없이 원본 파일에서 다시 읽으면 됨.

실무에서 "메모리 부족" 이 보이면 file-backed 가 먼저 제거됨 → 반복해서 디스크 I/O.

10. 프로세스 주소 공간의 레이아웃

10.1 64비트 리눅스 프로세스

0xffffffffffffffff ┌────────────────┐
                   │  커널 공간      │
0xffff800000000000 ├────────────────┤
                     (사용 불가)    │  ← 48-bit 가상 주소 한계
0x0000800000000000 ├────────────────┤
스택 (↓ 성장)                   ├────────────────┤
...                     (빈 공간)...                   ├────────────────┤
                   │  mmap 영역     │  ← 공유 라이브러리 등
                   ├────────────────┤
...                     (빈 공간)...                   ├────────────────┤
 (↑ 성장)                   ├────────────────┤
BSS                   ├────────────────┤
Data                   ├────────────────┤
Text (코드)0x0000000000400000 ├────────────────┤
                     (매핑 안됨)   │  ← NULL 포인터 보호
0x0000000000000000 └────────────────┘

10.2 ASLR — 주소 무작위화

보안을 위해 스택, 힙, mmap, 실행 코드 시작 주소를 매 실행마다 랜덤화:

cat /proc/self/maps     # 주소가 매번 다름

버퍼 오버플로 공격자가 return 주소를 맞히기 어려워짐.

10.3 /proc/<pid>/maps 읽기

555555554000-55555555c000 r--p 00000000 08:01 ...  /usr/bin/cat
55555555c000-555555568000 r-xp 00008000 08:01 ...  /usr/bin/cat
...
7ffff7dc0000-7ffff7de8000 r--p 00000000 08:01 ...  /usr/lib/libc.so.6
...
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0    [stack]
7ffffffff000-...           r-xp 00000000 00:00 0    [vdso]

각 라인이 하나의 VMA (Virtual Memory Area). 권한, 파일, offset 을 보여준다. 디버깅의 보물창고.

10.4 vDSO — 시스템 콜의 빠른 경로

gettimeofday(), clock_gettime() 같은 자주 호출되는 시스템 콜은 vDSO 페이지로 매핑된 코드를 호출 → 커널 모드 전환 없이 실행.

100ns 이하의 시스템 콜. 시간 측정이 핫패스인 앱에서 결정적.

11. NUMA — 다중 소켓 서버의 현실

11.1 Uniform 이 아닌 Memory Access

멀티 소켓 서버에서 CPU 가 자기 로컬 메모리에 접근하면 빠르고, 다른 소켓 메모리에 접근하면 느림 (보통 2-3배).

$ numactl --hardware
node 0 size: 64GB
node 1 size: 64GB
node distances:
     0   1
  0: 10  21    # 다른 노드 접근은 2배 느림
  1: 21  10

11.2 NUMA 친화 할당

기본 정책 (numactl --localalloc): "이 스레드가 실행 중인 CPU 의 로컬 노드에서 메모리 할당".

문제:

  • 스레드가 다른 CPU 로 이동하면 메모리는 원래 노드에 남음 → 원격 접근.
  • DB 가 수십 GB 메모리를 하나의 노드에 몰빵하는 실수.

11.3 실무 팁

  • Redis, Postgres: numactl --interleave=all 로 시작. 메모리를 노드 간 라운드 로빈 → 원격 접근 평균화.
  • JVM: -XX:+UseNUMA 로 GC 가 NUMA 인식.
  • 메모리 피닝: 특정 스레드를 특정 CPU 에 고정 (taskset).

12. 컨테이너와 가상 메모리

12.1 cgroup v2 메모리 제한

# 컨테이너의 메모리 상한
memory.max = 512M
memory.high = 400M   # soft limit: 이걸 넘으면 reclaim 압력

12.2 memory.high 의 의미

hard limit 만 있으면 컨테이너가 상한에서 갑자기 OOM 당함. memory.high 는 soft limit — 도달하면 커널이 compaction, direct reclaim 을 적극 → throttle.

12.3 JVM / Node.js 의 흔한 함정

컨테이너에서 JVM 이 "호스트의 전체 메모리" 를 보고 힙을 설정 → cgroup 상한을 넘어 OOM.

해결: Java 10+ 의 -XX:+UseContainerSupport (기본 ON), Node.js 의 --max-old-space-size.

13. 실무 체크리스트

문제 진단:

  • top의 VIRT/RES/SHR: 가상/실제/공유 — 셋 다 봐야 정확.
  • /proc/<pid>/status의 VmRSS, VmData, VmSwap: 세밀한 통계.
  • /proc/meminfo: 시스템 전체 메모리 브레이크다운 (MemFree, Buffers, Cached, SwapUsed).
  • vmstat 1: 1초마다 si, so (스왑 인/아웃) 모니터링. 0 이 아니면 swap 중.
  • sar -B 1: 페이지 폴트 통계 (minor/major).

성능 튜닝:

  • vm.swappiness=10: DB 서버.
  • vm.overcommit_memory=1: Redis.
  • THP off: Redis, MongoDB.
  • Huge pages 예약: Postgres의 huge_pages=on, JVM -XX:+UseLargePages.

OOM 방지:

  • 모니터링 프로세스에 oom_score_adj=-1000.
  • cgroup memory limit 설정.
  • earlyoom 활용.

컨테이너:

  • JVM: -XX:+UseContainerSupport 확인.
  • Node: --max-old-space-size 를 컨테이너 한도의 80%.
  • Go: GOMEMLIMIT (1.19+).

14. 마치며 — 투명한 추상화의 비용

가상 메모리는 프로그래머에게 투명 한 추상화처럼 보인다 — 그냥 malloc 하면 되는 것 같다. 하지만 실제로는:

  • malloc(100GB) 의 성공에는 overcommit 이 있다.
  • fork() 의 속도에는 Copy-on-Write 가 있다.
  • mmap 의 편리함 뒤에는 예측 불가능한 페이지 폴트 가 있다.
  • 데이터베이스 서버의 안정성 뒤에는 Huge Pages, THP off, oom_score_adj 튜닝이 있다.

"memory is just memory" 라고 말하는 개발자와 "virtual memory is a beautiful lie" 라고 말하는 개발자의 차이가 여기서 나온다.

다음 글에서는 리눅스 I/O 의 진화 — 동기 read/write 에서 시작해서 epoll, io_uring 까지 — 를 파볼 예정이다. 왜 Node.js 가 싱글 스레드로 10만 연결을 처리할 수 있고, 왜 io_uring 이 수십 년 만의 I/O 혁명인지.

참고 자료

  • "Understanding the Linux Kernel" — Bovet & Cesati, 3rd ed — Chapter 8-9 (Memory Management).
  • Mel Gorman — "Understanding the Linux Virtual Memory Manager" (Prentice Hall, 2004).
  • Ulrich Drepper — "What Every Programmer Should Know About Memory" (2007) — 필독.
  • "Are You Sure You Want to Use MMAP in Your Database Management System?" — Crotty, Leis, Pavlo (CIDR 2022).
  • Linux kernel documentation: Documentation/vm/ (hugetlbpage, transhuge, overcommit-accounting).
  • Brendan Gregg — "Linux Performance" 블로그 시리즈.
  • Operating Systems: Three Easy Pieces — Remzi Arpaci-Dusseau — Chapters 13-23 (Virtualization of Memory).

현재 단락 (1/278)

void* p = malloc(1ULL * 1024 * 1024 * 1024 * 100); // 100GB

작성 글자: 0원문 글자: 10,945작성 단락: 0/278