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/
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] Kernel 분석(v5.14.16) - Buddy (2) (0) | 2022.03.07 |
---|---|
[Linux Kernel] Kernel 분석(v5.14.16) - Buddy (1) (0) | 2022.02.26 |
[Linux Kernel] Kernel 분석(v5.14.16) - Memory Model (0) | 2022.02.01 |
[Linux Kernel] NUMA 메모리 관리 기법 (0) | 2022.01.31 |
[Linux Kernel] Kernel 분석(v5.14.16) - memblock (0) | 2022.01.23 |