본문 바로가기
Kernel/Memory Management

Memory Model

by samdong22 2026. 9. 2.

이번 페이지에서는 물리 메모리 모델에 대해서 정리를 할 생각이다.

 

** 커널 버전에 따라 내용이 다를 수 있고, 틀린 내용이 있을 수 있음

 

1. Physical Memory Model

개념적으로 물리 메모리는 할당되는 과정에서 완벽히 연속적으로 할당되기는 쉽지 않다.

할당되다보면은 hole이 생기기 마련이다.

 

리눅스 커널에서는 물리 메모리 할당을 페이지 프레임 단위(4KB)로 할당하는 페이지 구조체를 사용한다.

전체 메모리 크기가 4GB인 경우에 4KB의 페이지 프레임 단위로 쪼개면 총 1M개의 프레임 방이 생깁니다.

즉 1M개의 페이지 프레임과 이를 관리하는 1M개의 페이지 구조체가 1:1로 생성되는 개념입니다.

 

이때 페이지 구조체 하나는 64바이트로 이뤄지며, 총 1M * 64B = 64MB의 크기를 차지하게 되고, 이 페이지 구조체 배열을 mem_map이라고 부릅니다.

 

물리메모리는 FLATMEM, DISCONTIGMEM, SPARSEMEM 으로 세가지 종류가 있다.

보통 32비트 시스템은 FLATMEM, 64비트 시스템은 SPARSEMEM을 사용한다.

FLATMEM

리눅스 커널에서 물리메모리가 대체로 연속적이라고 가정할 때 사용하는 모델이다.

물리 주소 공간에 hole이 거의 없는 flat한 형태에 주로 사용되며, non-NUMA에 적합하다.

작은 hole이 존재하더라도 FLATMEM을 사용할 수 있지만, 그 공간은 낭비하게 된다.

 

FLATMEM은 memory map이 mem_map이라는 전역변수로 존재한다. 이는 1차원 배열이다.

 

 

DISCONTIGMEM

지금은 사용하지 않는 모델이다. 

2009년에 FLATMEM과는 달리 큰 hole을 대응할 수 있는 모델로 사용되었고, NUMA시스템에 적합했다.

그러나 핫플러그에 대응하지 못하고, 이를 대응하기 위한 SPARSEMEM이 등장하면서 사라졌다.

 

SPARSEMEM

SPARSEMEM은 가장 가변적인 모델이고, 가장 고급 모델이다.

hole이 존재하고, 메모리 영역이 띄엄띄엄존재 하더라도 효율적으로 관리한다.

NUMA 시스템도 효율적으로 지원하고, FLATMEM보다는 SPARSEMEM이 기본적으로 용된다.

 

SPARSEMEM은 물리메모리를 섹션별로 구분해서 관리한다.

하나의 섹션은 struct mem_section으로 표현되고, 섹션별로 struct page를 가진다.

struct mem_section {
	unsigned long section_mem_map;
	struct mem_section_usage *usage;
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *page_ext;
	unsigned long pad;
#endif
};
  • section_mem_map : 해당 섹션에 속한 struct page 디스크립터 배열을 가리키는 논리적 포인터이다. 단순한 포인터가 아니라, 부팅 초기 단계에 NUMA 노드 ID를 기록하거나 PFN(페이지 프레임 번호) 계산을 돕는 추가적인 데이터가 함께 인코딩되어 저장되기 때문에 포인터 타입이 아닌 unsigned long 타입으로 선언한다.
  • usage : 섹션 내의 서브섹션들의 정보를 관리하는 구조체를 가리킨다.

 

2. mem_map

mem_map은 물리 메모리 페이지 프레임에 대한 정보를 가진 page 구조체 배열이다.

mem_map의 접근은 NUMA와 UMA시스템에서의 방식도 다르고 아키텍처별로도 각각 다르다.

 

아래는 물리 메모리 모델별로 mem_map을 관리하는 방법이다.

 

FLATMEMORY mem_map in NUMA

책을 참고해서 정리를 하는중인데, 책에서는 UMA시스템에서는 mem_map포인터를 사용하고, NUMA시스템은 NODE_DATA()함수로 관리한다는 내용이었는데, 검색을 좀 해보니 이 방법도 맞고 대체로는 두 시스템 모두 mem_map포인터를 사용한다.

 

typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[MAX_ZONELISTS];
    int nr_zones;

#ifdef CONFIG_FLATNODE_MEM_MAP
    struct page *node_mem_map;
#endif
    int node_id;

    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd;
} pg_data_t;

pg_data_t 구조체 내부에 존재하는 *node_mem_map을 통해서 관리를 하고, 커널이 특정 노드의 메모리에 접근할 때, NODE_DATA(node_id)->node_mem_map의 형태로 포인터를 따라간다.

 

SPARSE MEMORY mem_map in NUMA

위에 설명했듯이, 섹션을 나눠서 관리하며, 하나의 섹션을 관리하는 mem_section 구조체를 사용한다.

mem_section 구조체의 구현은 두가지로 나뉘어져있다.

  • SPARSEMEM_STATIC : 보통 32비트에서 사용되며, 작은 사이즈이므로 mem_section구조체를 한꺼번에 컴파일 타임에 생성한다. 컴파일 타임에 생성하면 운영체제가 빌드할때 mem_section배열 한번에 고정 할당합니다.

 

  • SPARSEMEM_EXTREME : 64비트 시스템처럼 큰 시스템에서 사용되며, mem_section구조체를 필요할 때 런타임에 생성한다. 위의 STATIC과 다르게 런타임에 생성하기 때문에, 커널이 하드웨어를 스캔을 하고, hole을 구분하여 실제로 RAM이 존재하는 구역만 할당을 하므로 유용합니다.  
  •  
#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section **mem_section;
#else
extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT];
#endif

SPARSEMEM_EXTREME코드중에서 mem_section 이중포인터를 사용하는데, 이유는 단일 포인터로만 사용하면 64비트의 방대한 시스템에서 hole을 건너뛰면서 인덱싱하기게 매우 까다롭기 때문이다.

 

먼저 단일 포인터를 담을 수 있는 1차원 배열을 만들고, 실제 메모리가 존재하는 곳에만 열에 해당하는 구조체 배열을 달아주는 방식으로 큰 시스템에서 효율성을 높인다. 아래에서 그림을 참고해보자.

출처 :http://jake.dothome.co.kr/mem_map/

가장 먼저 PFN(페이지 프레임 넘버)가 넘어오면 자신의 NR_SECTION_ROOTS(행)을 찾는다. 이후에 PAGE_PER_SECTION(열)에서 그 행 안에서 정확히 몇번째 칸에 위치하는지 찾는다.

 

REFERENCE

http://jake.dothome.co.kr/mem_map/

'Kernel > Memory Management' 카테고리의 다른 글

Memory mapped I/O  (0) 2026.09.16
node & zone  (0) 2026.08.29