Skip to content

Latest commit

 

History

History
653 lines (415 loc) · 25.2 KB

File metadata and controls

653 lines (415 loc) · 25.2 KB

MMU(Memory Management Unit)

1. MMU가 무엇인지

MMU는 메모리 관리 장치로, CPU와 실제 물리 메모리(RAM) 사이에 위치한 하드웨어 모듈이다. 프로세스가 사용하는 **가상 주소(virtual address)**를 실제 메모리의 **물리 주소(physical address)**로 변환하는 역할을 핵심으로 한다. 또한 보호, 캐싱, 페이징 등 메모리 관리의 여러 기능을 담당하는 필수 하드웨어다.

2. MMU의 주요 동작

MMU는 다음과 같은 기능을 수행한다.

● 주소 변환(Address Translation)

  • CPU는 모든 메모리를 가상 주소로 접근한다.
  • MMU는 이 가상 주소를 페이지 테이블을 참조하여 물리 주소로 변환한다.
  • 이 과정에서 TLB(Translation Lookaside Buffer)를 이용해 변환 속도를 높인다.

● 메모리 보호(Memory Protection)

  • 각 페이지에 대해 접근 권한(Read/Write/Execute)을 지정할 수 있다.
  • 프로세스가 허용되지 않은 페이지에 접근하면 MMU가 **페이지 폴트(page fault)**를 발생시켜 보호 기능을 수행한다.

● 페이징(Paging) 및 스와핑(Swapping)

  • MMU는 OS가 사용하는 페이징 기법을 가능하게 한다.
  • 메모리 부족 시 일부 페이지를 디스크로 스왑아웃하고 필요 시 다시 로드할 수 있게 한다.

3. MMU의 장점

MMU의 존재는 현대 운영체제 기능의 전제가 된다.

● 프로세스 간 메모리 독립성 보장

  • 하나의 프로세스가 다른 프로세스의 메모리를 침범할 수 없다.
  • 운영체제 자체의 메모리도 보호된다.

● 가상 메모리(Virtual Memory) 지원

  • 실제 RAM보다 더 큰 메모리를 사용하는 것처럼 운영체제가 동작할 수 있다.
  • 디스크 공간을 확장 메모리로 활용할 수 있다.

● 안정성 및 보안 향상

  • 잘못된 포인터 접근이 곧바로 시스템 전체 붕괴로 이어지지 않는다.
  • 프로세스마다 독립적인 주소 공간을 제공한다.

● 메모리 효율성 증가

  • 페이지 단위로 필요한 부분만 물리 메모리에 올리기 때문에 메모리를 효율적으로 사용한다.

4. MMU가 없었을 때는 어땠는지

MMU가 없던 시절(초기 컴퓨터 또는 일부 임베디드 시스템)에는 다음과 같은 문제가 있었다.

● 프로세스 간 메모리 분리가 불가능

  • 모든 프로그램이 동일한 물리 주소 공간을 공유한다.
  • 버그 있는 프로그램 하나가 전체 시스템을 망가뜨릴 수 있다.

● 가상 메모리 불가

  • 물리 메모리의 크기가 곧바로 프로그램이 사용할 수 있는 메모리의 최대치가 된다.
  • 메모리가 부족하면 프로그램이 실행 자체가 불가능해진다.

● 주소 보호 없음

  • 잘못된 포인터 접근 → 시스템 전체 크래시로 이어질 가능성이 매우 높았다.

5. “용량은 있는데 사용할 공간이 없는 경우” 문제의 해결

이 질문은 두 가지 상황 중 하나를 의미한다.

① RAM은 충분하지만 큰 연속된 물리 메모리 공간이 없어서 할당이 실패하는 경우

  • MMU가 없는 시스템은 연속된 물리 메모리 블록을 요구한다.
  • 단편화(fragmentation)가 발생하면 충분한 총 메모리가 있어도 큰 공간을 확보하지 못했다.
  • 해결책은 메모리 압축(compaction) 또는 재부팅 뿐이었다.

② 가상 메모리 시스템에서 페이지가 부족하거나 연속 공간이 부족한 경우

MMU가 있는 시스템에서는 다음과 같이 해결한다.

  • OS는 가상 주소에서는 연속된 공간을 제공한다.
  • 실제 물리 메모리에서는 불연속 페이지를 여러 개 묶어 매핑한다.
  • 큰 연속 공간이 필요할 때도 물리 주소의 연속성은 고려할 필요가 없다.
  • 부족한 경우에는 페이지를 디스크로 스왑하여 공간을 확보할 수 있다.

결론적으로:

MMU가 있으면 연속된 물리 메모리 부족 문제를 거의 고민하지 않아도 된다. 가상 주소 공간은 넓고, 물리 메모리 단편화와 무관하게 주소 공간을 할당할 수 있기 때문이다.

6. MMU 덕분에 이제 이런 문제를 걱정하지 않아도 되는지?

완전히는 아니다. 그러나 대부분은 해결된다.

해결된 문제

  • 물리 메모리 단편화로 인해 큰 연속 메모리를 못 할당하는 문제
  • 프로세스 간 메모리 보호
  • 물리 메모리 용량 부족 시 가상 메모리를 활용하는 문제
  • 주소 범위 충돌

여전히 고려해야 하는 문제

  • 주소 공간 부족(32비트 시스템에서는 여전히 문제)
  • 물리 메모리 부족 → 스와핑 증가 → 성능 저하
  • 실시간 시스템에서는 스와핑이 문제가 되므로 MMU를 사용하더라도 제약이 있음
  • GPU나 DMA 장치에서는 연속된 물리 메모리가 필요한 경우가 있음(이를 위해 IOMMU를 사용)

7. MMU 내부 구성 요소

1) 페이지 테이블(Page Table)

프로세스의 가상 주소를 물리 주소로 변환하기 위한 핵심 자료 구조다. 가상 주소는 보통 다음 두 부분으로 구성된다.

  • 페이지 번호(Page Number): 가상 페이지의 인덱스
  • 페이지 오프셋(Offset): 페이지 내에서의 위치

페이지 테이블은 페이지 번호를 인덱스로 하여 해당 페이지가 어느 물리 페이지에 매핑되는지 저장한다.

예:

가상 페이지 3 → 물리 페이지 7  
가상 페이지 4 → 스왑 영역(디스크)  
가상 페이지 10 → 물리 페이지 2

2) TLB(Translation Lookaside Buffer)

TLB는 ‘주소 변환 캐시’다. CPU가 매우 자주 접근하는 주소에 대해 변환 결과를 캐싱한다.

특징

  • 매우 빠른 메모리(일종의 작은 내부 캐시)로 구성된다
  • 페이지 테이블 접근을 줄여 주소 변환 속도를 크게 높인다
  • TLB에 없으면 “TLB 미스”가 발생하고, MMU가 페이지 테이블을 다시 조회한다

3) 페이지 워커(Page Table Walker)

TLB 미스 시 페이지 테이블의 자료 구조를 따라가며 정확한 매핑을 찾아오는 하드웨어 또는 마이크로코드다. 특히 다단계 페이지 테이블 구조에서 많이 사용된다.

4) 보호 비트(Protection Bits)

각 페이지 엔트리에는 다음과 같은 플래그가 포함된다.

  • Read / Write / Execute 권한
  • 페이지 존재 여부(Present bit)
  • Dirty bit, Accessed bit
  • Cacheable 여부

이 플래그가 잘못된 경우 MMU는 하드웨어적으로 페이지 폴트를 발생시킨다.

8. x86 MMU 구조

x86 아키텍처는 계층적 페이지 테이블 구조를 사용한다. 64비트 모드에서 보통 4단계 또는 5단계 페이지 테이블을 사용한다.

예: (4단계 페이징)

VA → PML4 → PDP → PD → PT → PA

특징

  • 4KB 페이지 외에 2MB, 1GB의 대형 페이지(huge page)를 지원한다
  • 페이지 테이블 계층이 깊어 탐색 비용이 증가하므로 TLB 의존도가 매우 크다
  • PC와 서버 환경에서 가장 보편적으로 사용된다

9. ARM MMU 구조

ARM 아키텍처는 모바일 및 임베디드에 특화되어 있어 구조가 단순하면서도 유연하다.

특징

  • 1단계 또는 2단계 페이지 테이블 구조
  • granule(4KB / 16KB / 64KB) 선택이 가능하다
  • PMP(Physical Memory Protection), PXN (Privileged eXecute Never) 등 보안 기능이 발달했다
  • ARMv8 이후로 x86과 비슷한 가상 주소 확장 및 EL(권한 레벨) 기반 접근 제어를 지원한다

모바일/임베디드 환경에서 효율성과 전력 최적화가 뛰어나다

9. 페이지 폴트(Page Fault)가 일어날 때의 동작 과정

페이지 폴트는 대부분 다음 원인으로 발생한다.

  • 페이지가 물리 메모리에 없음
  • 접근 권한 위반
  • 읽기 전용 페이지에 쓰기를 시도
  • 잘못된 주소 접근

페이지 폴트 처리 흐름

  1. CPU가 메모리에 접근
  2. MMU가 페이지 테이블을 확인
  3. 페이지가 없거나 권한 위반 → 페이지 폴트 예외 발생
  4. OS 커널이 예외 핸들러 실행
    • 필요한 페이지를 디스크에서 읽어와서 물리 메모리에 올림
    • 페이지 테이블 업데이트
    • TLB 업데이트
  5. CPU 명령 재시도

이 흐름 덕분에 물리 메모리가 부족해도 프로그램은 연속 주소를 사용하는 것처럼 실행된다.

10. IOMMU가 필요한 이유

CPU는 MMU가 있으므로 가상 주소를 보호받지만, DMA 같은 장치는 가상 주소를 이해하지 못하고 물리 주소만 직접 접근한다.

이때 IOMMU가 필요하다.

IOMMU 기능

  • 장치가 접근하는 주소도 가상화한다
  • DMA가 잘못된 주소에 접근하는 것을 막는다
  • 장치 메모리 맵핑을 더 쉽게 한다
  • GPU, 네트워크 장비, SSD에서 널리 사용된다

MMU만 있을 때 생기는 문제

DMA가 잘못된 물리 주소를 접근하면 OS가 파괴될 수 있다. IOMMU가 이를 방지한다.

11. MMU의 고급 기능이 왜 중요한가

MMU가 제공하는 핵심 가치

  • 메모리 보호
  • 주소 변환
  • 프로세스 간 메모리 격리
  • 메모리 오버커밋(virtual memory) 가능
  • 메모리 단편화 문제 제거
  • 스왑/페이징 가능

그래서 현대 시스템에서 필수다

MMU가 없다면 현대적인 OS(Windows, Linux, Android, iOS)는 성립할 수 없다. 멀티태스킹도 사실상 불가능하다.

12. 큰 그림 다시 한 번 정리

현대 OS의 메모리 관리는 크게 네 층으로 나눌 수 있다.

  1. CPU + MMU

    • 가상 주소 → 물리 주소 변환
    • 접근 권한 체크, 페이지 폴트 발생
  2. 페이지 테이블 구조

    • 어떤 가상 페이지가 어떤 물리 페이지(또는 디스크)에 있는지 매핑
  3. OS의 메모리 관리 알고리즘

    • 어떤 페이지를 메모리에 유지하고, 어떤 페이지를 내보낼지 결정
    • LRU, Clock, Working Set 등
  4. 동적 메모리 할당기 / 단편화 처리

    • malloc/free, 커널의 buddy allocator, slab allocator 등

각 층을 하나씩 깊게 들어가보겠다.

1. TLB(Translation Lookaside Buffer) 상세 동작

1.1 TLB가 왜 필요한가

페이지 테이블은 메모리에 있다. 가상 주소를 물리 주소로 바꾸려면:

  • 다단계 페이지 테이블(예: x86-64 4단계)을 여러 번 메모리에서 읽어야 한다
  • 매번 메모리 접근이 몇 번씩 더 늘어나 속도가 확 떨어진다

그래서 MMU는 **“최근에 변환한 VA → PA 결과를 캐싱”**하는데, 이게 TLB다.

1.2 TLB 구조

TLB는 CPU 내부에 있는 작은 캐시라고 보면 된다.

  • 엔트리 예시

    • 가상 페이지 번호(VPN)
    • 물리 페이지 번호(PPN)
    • 접근 권한(R/W/X)
    • ASID 또는 PCID(어느 프로세스의 주소 공간인지 식별)
    • 유효 비트 / 글로벌 비트 등

보통 L1 TLB, L2 TLB 같이 여러 계층이 있고, 명령용 TLB, 데이터용 TLB를 분리하기도 한다.

1.3 TLB 조회 흐름

CPU가 어떤 가상 주소 VA로 메모리에 접근할 때:

  1. VA에서 페이지 번호를 뽑는다

  2. TLB에서 이 페이지 번호를 키로 검색한다

    • 있으면 TLB hit → 물리 페이지 번호(PPN) 얻음
    • 없으면 TLB miss → 페이지 테이블 walk 수행
  3. PPN + offset을 합쳐 물리 주소 PA를 만든다

1.4 TLB 미스 처리

TLB 미스가 나면 MMU는 페이지 테이블을 직접 뒤져서 PTE(Page Table Entry)를 찾는다. 이 작업을:

  • 하드웨어가 직접 하는 구조(x86, ARM 대부분)
  • OS가 트랩 받아서 직접 하는 구조(옛날 MIPS, 일부 RISC)

둘 다 있다.

찾은 PTE가:

  • 유효하고, 권한도 OK면 → TLB에 새 엔트리로 넣고, 다시 명령 재시도
  • 아예 존재하지 않거나 권한이 안 되면 → 페이지 폴트 예외 발생

1.5 TLB 교체 알고리즘

TLB도 캐시이므로 가득 차면 뭔가를 쫓아내야 한다.

  • 하드웨어는 보통 간단한 pseudo-LRU, FIFO, random 교체를 사용한다
  • 너무 똑똑한 알고리즘을 쓰면 하드웨어가 복잡해지기 때문이다

1.6 TLB flush / shootdown

프로세스 전환(context switch)이나 페이지 테이블 변경 시:

  • 예전 주소 공간의 TLB 엔트리를 무효화해야 한다
  • 그렇지 않으면 다른 프로세스가 엉뚱한 주소 매핑을 사용하게 된다

그래서:

  • 전체 TLB flush
  • 혹은 주소 부분 무효화(INVLPG 등)
  • 멀티 코어에서는 다른 코어의 TLB도 함께 무효화(TLB shootdown)

성능 상 매우 중요한 영역이라 커널 코드에서 신중히 다룬다.

2. 페이지 테이블이 실제로 어떻게 배치되는지

2.1 1단계 vs 다단계 페이지 테이블

1단계(Linear) 페이지 테이블
  • 가상 페이지 번호를 그대로 인덱스로 쓰는 커다란 배열
  • 예: 32비트 주소, 페이지 크기 4KB라면 페이지 개수 = 2²⁰ = 약 100만
  • PTE 하나 4바이트라고 하면 4MB (프로세스 하나당)
  • 64비트 주소에서는 완전히 비현실적이라 거의 안 씀
다단계 페이지 테이블(Multi-level)
  • 상위 비트부터 나눠서 트리 구조처럼 사용
  • 실제로 사용되는 범위에 대해서만 테이블을 할당
  • x86-64 4단계 페이지 테이블이 대표적이다

2.2 x86-64 4단계 예 (간략)

64비트 주소 중 상위 몇 비트만 사용한다고 치고:

VA:
[ PML4 index | PDP index | PD index | PT index | Offset ]

각 index는 보통 9비트씩, offset은 12비트(4KB 페이지)다.

  • PML4: 최상위 테이블, 보통 프로세스마다 하나
  • PDP, PD, PT: 각각 더 세부 레벨로 내려가는 테이블

각 레벨의 엔트리는 다음 레벨 페이지 테이블의 물리 주소를 가리킨다. 마지막 PT 엔트리(PTE)가 실제 물리 페이지 번호를 담고 있다.

2.3 페이지 테이블은 메모리 어딨나

페이지 테이블 자체도 결국 일반 물리 메모리에 존재하는 데이터 구조다.

  • 커널이 kmalloc 등으로 페이지를 할당받아 테이블로 사용
  • 그 페이지들의 물리 주소를 상위 테이블에 적어 넣는다
  • CPU는 CR3 레지스터 등에 최상위 테이블(PML4)의 물리 주소를 담고 있다

즉, “MMU가 특별한 메모리에 페이지 테이블을 저장한다”가 아니라 OS가 일반 메모리 위에 페이지 테이블 구조를 만들고, 그 위치를 MMU에게 알려주는 구조다.

2.4 Huge Page / Large Page

TLB 엔트리 하나가 커버할 수 있는 메모리 크기를 키우기 위해:

  • x86: 4KB, 2MB, 1GB 페이지 지원
  • ARM: 4KB, 16KB, 64KB 등

장점

  • TLB 엔트리 수가 적어도 많은 메모리를 커버 가능
  • TLB 미스가 줄어듦

단점

  • 단편화 증가(큰 단위로 잡기 때문)
  • 세밀한 보호/매핑이 어려움

DB, 게임 서버, JVM 같은 경우 huge page를 활용하는 경우가 많다.

3. Demand Paging & 페이지 폴트 상세

3.1 Demand Paging

현대 OS는 필요할 때만 페이지를 메모리에 올리는 방식을 사용한다.

  • 프로세스를 실행할 때 전체 코드를 RAM에 올리지 않는다
  • 코드/데이터/스택/힙 페이지가 실제로 접근되는 순간에만 페이지를 채운다

장점:

  • 시작 시점 메모리 사용량 감소
  • 프로그램 로딩 속도 향상
  • 사용되지 않는 부분은 끝까지 디스크에 남겨둘 수 있다

3.2 페이지 폴트 종류

  1. 존재하지만 아직 메모리에 안 올라온 페이지

    • 예: .text 섹션이 아직 로드되지 않았거나, lazy allocation 등
    • OS가 디스크에서 해당 페이지를 읽어오고, 페이지 테이블 업데이트 후 재시도
  2. 스왑 아웃된 페이지

    • 예전에 메모리에서 쫓겨나 swap 공간에 저장됨
    • 다시 접근하면 swap에서 읽어와 복구
  3. 권한 위반 페이지

    • 읽기 전용인데 쓰기를 하려는 경우 등

    • 경우에 따라:

      • 진짜 버그 → SIGSEGV
      • copy-on-write(COW) 최적화의 일부로 쓰기도 한다 (fork 후 첫 write 등)
  4. 완전히 잘못된 주소

    • 매핑 자체가 없음
    • 즉시 SEGFAULT 같은 예외

4. Swapping vs Paging

두 용어가 섞여서 쓰이지만 전통적 의미는 조금 다르다.

4.1 Swapping (전통적 의미)

  • 프로세스 전체를 통째로 디스크에 내보내고, 필요 시 다시 올리는 방식
  • 매우 오래된 기법, 현대 범용 OS에서는 거의 사용하지 않는다
  • 요즘 “swap”이라고 말할 때도 내부적으로는 페이지 단위로 움직이는 경우가 많다

4.2 Paging

  • 페이지 단위로 메모리와 디스크 사이를 오르내리는 방식

  • 현대 가상 메모리 시스템의 기본

  • OS는 메모리가 부족하면

    • 덜 쓰이는 페이지를 선택해서 디스크(스왑 영역)에 저장
    • 그 물리 페이지를 다시 다른 용도로 사용

4.3 실제 Linux 기준으로 보면

  • swap partition / swap file에 페이지 단위로 저장
  • 커널 입장에서는 “페이지를 스왑으로 보낸다” → paging이지만 용어는 그냥 “swap out”이라고 부르는 경우가 많다

요약하면, 요즘은 사실상 ≒ “paging + swap 공간 사용” 구조로 이해하면 충분하다 정도로 보면 된다.

5. 페이지 교체 알고리즘 (LRU, Clock 등)

메모리에서 어떤 페이지를 내보낼지 결정하는 것이 페이지 교체 알고리즘이다.

5.1 OPT (Belady’s Optimal Algorithm)

  • 앞으로 가장 오래 동안 쓰이지 않을 페이지를 내보내는 알고리즘
  • 이론상 최적이지만, 미래를 알아야 하므로 현실에서는 불가능
  • 다른 알고리즘을 평가할 때 기준으로 사용된다

5.2 LRU (Least Recently Used)

  • 가장 오래 동안 참조되지 않은 페이지를 내보냄
  • “최근에 사용된 것은 또 쓸 가능성이 높다”는 locality 가정에 기반

문제:

  • 진짜 LRU를 구현하려면 페이지 접근 순서를 정확히 기록해야 한다
  • 하드웨어/소프트웨어 모두 비용이 크다

그래서 근사 LRU를 많이 쓴다.

5.3 Clock(Second-Chance) 알고리즘

현실에서 많이 사용하는 LRU 근사 알고리즘이다.

구조:

  • 페이지들을 원형 큐(“시계 바늘”)로 관리
  • 각 페이지마다 **참조 비트(reference bit)**를 둔다

동작:

  1. 바늘이 가리키는 페이지의 참조 비트를 본다
    • 참조 비트가 0이면 → 이 페이지를 내보낸다
    • 참조 비트가 1이면 → 0으로 지우고, 바늘을 다음 페이지로 이동

결국 한 번이라도 최근에 사용된 페이지는 한 바퀴 기회를 더 받는 구조다. 단순하면서도 locality를 어느 정도 반영한다.

5.4 NRU(Not Recently Used), Aging 등

  • NRU: 참조 비트, 수정 비트 등을 조합해서 “최근에, 또는 자주 쓰이지 않은 페이지”를 후보로
  • Aging: 참조 비트를 주기적으로 시프트하면서 “최근 사용 정도”를 점수 형태로 쌓는 방식

실제 OS 구현은:

  • 하드웨어가 제공하는 Accessed/Referenced bit, Dirty bit 등을 활용
  • 커널의 page reclaim 코드가 여러 heuristic을 섞어서 사용

Linux의 active, inactive 리스트, kswapd 같은 것들이 이 영역이다.

5.5 Thrashing(스래싱)

페이지 교체가 너무 자주 일어나서:

  • CPU는 실제 일을 하는 시간보다
  • 페이지를 넣었다 뺐다 하는 데 시간을 더 쓰게 되는 상태다

원인:

  • 메모리가 너무 부족한데 많은 프로세스를 동시에 돌릴 때
  • 워킹 셋(실제로 자주 사용하는 페이지 집합)을 메모리에 모두 담지 못할 때

해결:

  • 동시에 실행되는 프로세스 수 줄이기
  • 더 큰 메모리
  • 워킹 셋 기반 스케줄링 등

6. 단편화(Fragmentation)와 메모리 할당기

6.1 내부 단편화 / 외부 단편화

  • 내부 단편화(Internal)

    • 요청한 크기보다 큰 블록을 할당해야 해서
    • 블록 내부에 사용하지 못하는 남는 공간이 생기는 것
  • 외부 단편화(External)

    • 전체 빈 공간은 충분한데
    • 사이사이에 쪼개져 있어서 “큰 연속 블록”을 못 잡는 상황

MMU + 페이징은 외부 단편화 문제를 크게 줄인다는 점이 중요하다.

6.2 페이징이 외부 단편화를 줄이는 방식

  • 물리 메모리는 고정 크기의 페이지 프레임(예: 4KB) 단위로 관리

  • 프로세스가 연속된 100MB를 가상 주소로 요구하더라도

    • 물리 메모리는 여기저기 흩어진 페이지 프레임 25,600개만 있으면 된다
  • 연속된 물리 주소가 필요하지 않다

그래서 예전 “메모리는 많은데 연속된 큰 블록이 없어서 할당 실패” 문제는 거의 사라졌다고 봐도 된다(일부 특수한 장치/특수 페이지를 제외하고).

6.3 Buddy allocator

커널이 물리 메모리를 관리하는 대표적인 방법이다.

  • 2의 거듭제곱 크기(4KB, 8KB, 16KB, …)의 블록으로 관리
  • 필요할 때 큰 블록을 쪼개서 두 개의 “buddy” 블록으로 만든다
  • 다시 합칠 수 있을 때는 buddy들을 붙여서 큰 블록으로 복구

장점:

  • 블록 합치기가 쉬워 외부 단편화를 어느 정도 제어 가능
  • 구현이 단순

6.4 Slab allocator / Slub 등

자주 사용하는 작은 객체들(커널 구조체 등)에 최적화된 할당기다.

  • 같은 종류의 객체를 모아 “슬랩” 단위로 관리
  • 캐시 친화적이며, 내부 단편화를 줄이고, 할당/해제가 빠르다

유저 공간의 malloc/free도 내부적으로 비슷한 아이디어(여러 크기 클래스, freelist 등)를 사용한다.

7. “용량은 있는데 사용할 공간이 없는” 문제와 MMU의 역할

7.1 MMU 없던 시절

  • 프로그램이 물리 메모리를 연속 블록으로 직접 요구했다

  • 외부 단편화 때문에

    • 전체 빈 공간은 예를 들어 300MB인데
    • 200MB 연속 블록이 없어서 할당 실패 같은 일이 자주 있었다

해결 방법은:

  • 메모리 compaction(재배치)을 수시로 하거나
  • 사실상 재부팅에 가까운 수준의 큰 비용을 치러야 했다

7.2 MMU + 페이징 도입 후

이제 프로세스는 연속된 가상 주소 공간만 알고 있으면 된다.

  • OS는 물리 메모리 여기저기 퍼져 있는 페이지들을

    • 연속된 가상 주소로 이어 붙여서 보여준다
  • “전체 용량은 있는데 덩어리로 연속된 블록이 없다” 문제는 대부분 사라진다

다만 예외는 있다:

  • DMA / 장치용으로 연속된 물리 메모리가 필요한 경우

    • IOMMU로 어느 정도 해결
    • 그래도 어떤 상황에서는 여전히 큰 연속 물리 블록이 필요해 실패할 수 있다
  • Huge page(2MB, 1GB) 같은 큰 단위 페이지

    • 물리 메모리의 특정 정렬/연속성을 요구하기 때문에 할당 실패 가능

7.3 그럼 이제 이런 문제를 완전히 안 걱정해도 되나?

완전히 “안 걱정해도 된다” 수준은 아니다.

여전히 고민해야 하는 것:

  1. 물리 메모리 부족 자체

    • 아무리 가상 메모리가 있어도, 실제 RAM이 부족하면 성능이 급격히 떨어진다
    • 스왑이 과도해지면 스래싱 발생
  2. 주소 공간 자체의 한계

    • 32비트 프로세스는 가상 주소 공간이 4GB라서 그 안에서만 놀아야 한다
    • 커널/유저 나눔, mmap, heap, stack 때문에 실제 usable 공간은 더 적어질 수 있다
  3. 특정 요구사항(연속 물리, hugepage, 장치 메모리)

    • 게임 서버, DB, 네트워크 스택에서 성능 최적화를 위해 일부 상황에서는 여전히 고려 대상이다

그래도, 일반적인 “메모리 많아 보이는데 큰 배열 하나 못 잡는” 류의 문제는 MMU + 페이징 덕분에 현실 세계에서 거의 사라졌다고 보면 된다.

서버 개발 관점에서 한 번 묶어보면

온라인 게임 서버 개발자 입장에서 체감할 수 있는 포인트를 정리하면:

  1. OOM과 스왑

    • 프로세스 메모리 사용량이 커지면

      • OS가 다른 페이지를 스왑으로 쫓아내고
      • 심해지면 스래싱 또는 OOM killer 동작
    • “메모리는 어떻게든 OS가 알아서 늘려주겠지”가 아니라

      • 실제 RAM, 스왑 사용량, 워킹 셋을 항상 신경 써야 한다
  2. 메모리 사용 패턴

    • locality가 좋게(연속 접근) 메모리를 설계하면 TLB miss, cache miss 감소
    • “수많은 객체를 랜덤한 포인터로 여기저기 접근”하는 패턴은 페이징 및 캐시 측면에서 손해
  3. Huge page 사용

    • JVM, DB, 대형 메모리 서버에선 성능 이득이 있을 수 있다
    • 하지만 할당 실패, 단편화, 설정 복잡도 등 트레이드오프 존재
  4. fork + COW

    • 리눅스에서 fork()는 copy-on-write 덕분에 빠르게 작동한다

    • 실제로는 페이지를 복사하지 않고,

      • 부모/자식이 같은 페이지를 공유
      • 쓰기 시점에 그 페이지만 복사

이 모든 것이 MMU + 가상 메모리 + 페이지 테이블 + TLB 덕분에 가능한 동작이다.