Kernel Memory영역은 Zone 단위로 나누어 노드를 관리한다. 이는 메모리 제약 메모리 단편화를 최소화 하기 위함이다.

A. ZONE

1. Zone 종류

메모리 Zone 여러 종류를 가지고 있다. 일반적으로 메모리 존은 Kernel Memory영역내에서 사용하는 개념이다.

커널 메모리 영역은 ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_HIGHMEM, ZONE_MOVABLE, ZONE_DEVICE 타입을 지원한다.

 

그리고 ARM64 모드에서는 ZONE_DMA, ZONE_NORMAL, ZONE_MOVABLE, ZONE_DEVICE 지원한다.

위와 같이 기본적으로 kernel memory physical memory가 구성된다. Zone이라는 영역을 나누는 개념을 통해서 메모리 단편화 등의 문제점을 해결한다.

 

그럼 여기서 가지 의문점이 생기게 된다.

기존 User, Kernel 메모리 구조를 보면 위와 같이 구분 되는 것으로 확인되었다. 그럼 여기서 ZONE으로 메모리를 나누다면 ZONE_NORMAL 해당하는 곳은 어디이고 ZONE_DMA, ZONE_HIGHMEM등의 위치는 어디를 가리키는 것일까?

 

=> 궁금증의 답은 OS마다 다르다는 것이다. ZONE 그냥 메모리를 특징에 따라 ZONE이라는 영역으로 나눈 뿐이다. ZONE_NORMAL user영역을 가리킬 수도 있고 kernel영역의 일부를 가리킬 수도 있다. 조금 구체적으로 예를들면 Kernel 영역중에 Linear-Region physical 1:1 매핑되는영역으로 ZONE 이용할 ZONE_NORMAL 이용할 것이다. 그리고 vamlloc영역은 ZONE_NORMAL 아닌 다른 ZONE으로 연결할 것이다. 왜냐하면 ZONE_NORMAL vmalloc 아닌 physical 1:1 매핑하는 kmalloc 지원하는 ZONE 타입이기 때문이다.

 

  • kamlloc vmalloc

kmalloc vmalloc 차이를 간단히 짚고 넘어가면 우선 둘다 kernel영역에 메모리를 할당하는 기술이다. 하지만 kmalloc 경우 ZONE_NORMAL area physical 1:1 매핑되는 kernel memory 할당하는 기술이고 vmalloc virtual기술 page table 이용해서 kernel memory 할당하는 기술이다. 때문에 vmalloc 경우 physical address 연속적이지 않아도된다. Page table설정을 위한 시간이 소요되지만 kmalloc보다 메모리를 할당할 유리하다.

 

 

2. ZONE_NORMAL

ZONE_NORMAL 1:1 physical memory 매핑되는 kernel memory 영역이다. 32bit에서는 해당 메모리 영역이 제한적이기 때문에 남은영역을 ZONE_HIGHMEM으로 사용하였는데 64bit에서는 제한이 없어 ZONE_NORMAL 사용한다.

 

 

3. ZONE_MOVABLE

ZONE_MOVABLE 페이지 단편화 문제를 해결하는데 사용하거나 Hot-Plug-in 위해 사용된다.

ZONE_MOVABLE 가장 마지막(상단) memory영역을 이용해서 사용하며 2가지 방법으로 사용이되며 메모리구조는 아래와 같다.

위와 같이 ZONE_MOVABLE 존재하게되면 페이지 단편화를 목적으로 사용하는 경우이다. 각각의 ZONE_NORMAL 노드의 뒤에 ZONE_MOVABLE 존재하여 페이지 단편화를 최소화한다.

위와 같이 ZONE_MOVABLE 가장 마지막에 몰아서 하나의 노드로 두는 경우는 HOT-Plug-IN 지원하기 위함이다.

 

 

 

 

 

B. Sparse

1. 초기화

ZONE 영역 초기화는 부트 메모리 초기화 작업 진행된다.


arch/arm64/mm/init.c
bootmem_init() function 통해서 부트 메모리 초기화가 진행된다.

그리고 여기서 zone 영역을 초기화하는

 

function 존재한다. 해당 function sparse_init() 이후 sparsemem 지원하는 경우 sparse init초기화를 진행하고 호출된다.

 

1) Sparse_init()

ARM64 기본적으로 SPARSEMEM방식으로 메모리를 관리한다 이로인해 부트 메모리 초기화를 진행할 기본적으로 sparse_init() function 호출해서 section단위 초기화 작업이 필요하다.

 

  • mm/sparse.c
void __init sparse_init(void)
{
unsigned long pnum_end, pnum_begin, map_count = 1;
int nid_begin;


memblocks_present();


pnum_begin = first_present_section_nr();
nid_begin = sparse_early_nid(__nr_to_section(pnum_begin));


/* Setup pageblock_order for HUGETLB_PAGE_SIZE_VARIABLE */
set_pageblock_order();


for_each_present_section_nr(pnum_begin + 1, pnum_end) {
int nid = sparse_early_nid(__nr_to_section(pnum_end));


if (nid == nid_begin) {
map_count++;
continue;
}
/* Init node with sections in range [pnum_begin, pnum_end) */
sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
nid_begin = nid;
pnum_begin = pnum_end;
map_count = 1;
}
/* cover the last node */
sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
vmemmap_populate_print_last();
}

지난번에 확인한 function으로 이미 정의된 memblock 정보를 얻어와서 반복문을 통한 section할당이 이루어지고 있는 function이다.

memblock_present() 들어가면

memory_present()라는 funtion 호출되는 것을 확인할 있는데 여기서 section node id 매칭을 진행한다.

여기서 node NUMA에서 단위가 되는 node 의미하며 서로다른 section id 하나의 node id 가질 있다.

 

for_each_mem_pfn_range(i, MAX_NUMNODES, &start, &end, &nid)
memory_present(nid, start, end);

코드에서 반복문으로 node id 올려가며 memory_present() 호출하고 있다.

 

static void set_section_nid(unsigned long section_nr, int nid)
{
section_to_node_table[section_nr] = nid;
}

memory_presnet()내부에서는 set_section_nid() 호출함으로써 NUMA node id section별로 호출하고 있다. set_section_nid 반복문을 통해 진행되며 서로 다른 section id 가지는 section들이 같은 node id 가질 있다.

 

static inline struct mem_section *__nr_to_section(unsigned long nr)
{
#ifdef CONFIG_SPARSEMEM_EXTREME
if (!mem_section)
return NULL;
#endif
if (!mem_section[SECTION_NR_TO_ROOT(nr)])
return NULL;
return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

그리고 위와 같이 __nr_to_section function() 통해서 section 구조체의 실제 주소 값을 찾아갈 있다.
여기서 nr 매개변수는 set_section_nid()에서 node, section id set할때 사용한 section id값이다.

_nr_to_section() function에서 ARM64 경우 2차원으로 관리해서 찾아가기 때문에 section id값을 / 값과 % 값을 2차원 배열로 접근하고 있다.

결국에 그림과 같이 2차원 배열형태로 section들을 관리하게 된다.

 

 

2) Memory Map

위와 같이 결과적으로 mem_section 단위로 주소 값을 얻을 있다. 그러나 mem_section 결국 physical memory 영역으로 mapping 되어야 한다.

사용하는 방법이 usemap, mem_map, vmemmap 존재한다. usemap 경우 여러 section 묶어서 node단위로 진행하고 mem_map section단위별로 page table 진행한다고 보면 된다.

 

sparse_init() fucntion에서 map_count값으로 노드 개수를 나타내고 이를 sparse_init_node() function 매개변수로 전달 그리고

위와 같이 호출해서 usemap 할당한다.

 

struct page __init *__populate_section_memmap(unsigned long pfn,
unsigned long nr_pages, int nid, struct vmem_altmap *altmap)
{
unsigned long size = section_map_size();
struct page *map = sparse_buffer_alloc(size);
phys_addr_t addr = __pa(MAX_DMA_ADDRESS);


if (map)
return map;
…(중략)...
}

mem_map 위와 같이 할당을 진행한다. 마찬가지로 sparse_init()에서 호출하며 usemap, mem_map 같이 사용할 있다.

 

결과적으로 위와 같이 mem_section들을 usemap, mem_map 통해서 paging하고 physical memory 연결시켜 있다.

하지만 기법들보다 최근에는 vmemmap방법을 통해서 sparse section들을 physical 매핑해주는 것으로 알고 있다. 부분은 다음에 다뤄봐야 같다.

 

 

 

  • Ref

http://weng-blog.com/2016/08/linux-mm/

코드로 알아보는 ARM리눅스 커널 2

http://www.embeddedlinux.org.cn/essentiallinuxdevicedrivers/final/ch02lev1sec7.html

https://www.programmerall.com/article/34561974480/

https://elixir.bootlin.com/linux/v5.14.16/source/

https://blog.csdn.net/zdy0_2004/article/details/100906367

+ Recent posts