1. early 메모리 할당
early 메모리할당은 커널 초반 작업을 위해 메모리를 할당하는 것이다. 이 때는 memblock밖에 메모리 관리 기법이 존재하지 않는다 때문에 early 작업들은 모두 memblock을 사용한다.
2. memblock
memblock은 세가지 타입으로 메모리 관리를 진행한다.(memory, reserved, physmem)
코드는 include/linux/memblock.h에 정의되어 있다.
struct memblock { bool bottom_up; /* is bottom up direction? */ phys_addr_t current_limit; struct memblock_type memory; struct memblock_type reserved; }; |
memblock의 정의부는 mm/memblock.c에 정의되어있으며 기본적으로 배열형태로 메모리를 관리하기 때문에 메모리가 overflow되는지 배열이 꽉차지 않았는지 확인해가면서 할당하는 것으로 보인다.
또한 할당과정에서 overlap을 확인하고 merge하는 작업등을 진행하는것 같은데(?) 이 부분은 조금 더 조사해봐서 확실히 할 필요가 있을 것 같다.
- memory 타입
사용할 물리 메모리영역에 등록하여 사용한다. memory 타입을 위해 할당된영역은 regions[128]크기이다.
물리 메모리 영역에 등록이다.
- reserved
사용중이거나 사용한 물리 메모리 영역을 등록하여 사용한다. reserved 타입을 위해 할당된영역은 regions[128]크기이다.
reserved영역에 등록이다.
- physmem
물리적으로 감지된 메모리 영역을 등록하여 사용되고 등록 후에 수정은 없다. physmem 타입을 위해 할당된영역은 regions[128]크기이다.
위 각 memblock type간에 중요한 부분은 서로 다른 타입끼리 메모리영역이 중복되는 경우가 발생할 수 있다는 것이다.
이 경우 메모리 버그에 속하며 fix를 해줘야 한다.
위 그림과 같이 memory region과 reserve region이 겹치는 것을 볼 수 있다. 이렇게 겹치는 것은 일종의 버그로 할당 시에 겹치지 않도록 사전 작업을 진행 후 할당을 진행한다 또한 region의 개수를 줄이기 위해 merge등의 작업을 또 진행한다.
Memory영역은 기본적으로 DTB를 읽어서 장치 드라이버 관련해서 메모리를 할당해주는 역할을 담당하며 이 때 호출되는 API가 memblock_add() function이다.
Reserved영역은 kernel, initrd, page table, DTB area, CMA-DMA 영역에 대한 할당을 진행하고 초기 커널 부트업 과정에서 해당 항목들의 할당을 진행한다. Reserved영역의 할당을 위해 사용하는 API는 memblock_reserve()이다.
기본적으로 앞에서 언급한 바와 같이 memblock 할당에 있어 우선적으로 range를 조사하고 할당을 진행하며 이웃한 할당 값의 경우 merge를 진행한다.
여기서 memory할당을 위한 API는 memblock_add(),
reserved할당을 위한 API memblock_reserve(),
physical 할당을위한 API memblock_phymem_add()
이 있으며 각각의 API는 모두 memblock_add_range()를 호출함으로써 range 영역 판단 및 할당작업을 진행한다.
1) memblock 초기화
초기화 관련 코드는 mm/memblock.c에서 확인 가능하다.
위와 같이 기본적으로 위에서 언급한 3가지 type의 메모리 region을 변수로 만들어서 관리한다.
그리고 바로 밑에 각 변수들을 할당해주는 동작이 정의되어 있다.
/arch/arm64/mm/init.c
void __init arm64_memblock_init(void) { s64 linear_region_size = PAGE_END - _PAGE_OFFSET(vabits_actual); /* Handle linux,usable-memory-range property */ fdt_enforce_memory_region(); if (memory_limit != PHYS_ADDR_MAX) { memblock_mem_limit_remove_map(memory_limit); memblock_add(__pa_symbol(_text), (u64)(_end - _text)); } if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) { /* * Add back the memory we just removed if it results in the * initrd to become inaccessible via the linear mapping. * Otherwise, this is a no-op */ u64 base = phys_initrd_start & PAGE_MASK; u64 size = PAGE_ALIGN(phys_initrd_start + phys_initrd_size) - base; /* * We can only add back the initrd memory if we don't end up * with more memory than we can address via the linear mapping. * It is up to the bootloader to position the kernel and the * initrd reasonably close to each other (i.e., within 32 GB of * each other) so that all granule/#levels combinations can * always access both. */ if (WARN(base < memblock_start_of_DRAM() || base + size > memblock_start_of_DRAM() + linear_region_size, "initrd not fully accessible via the linear mapping -- please check your bootloader ...\n")) { phys_initrd_size = 0; } else { memblock_remove(base, size); /* clear MEMBLOCK_ flags */ memblock_add(base, size); memblock_reserve(base, size); } } /* * Register the kernel text, kernel data, initrd, and initial * pagetables with memblock. */ memblock_reserve(__pa_symbol(_stext), _end - _stext); if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) { /* the generic initrd code expects virtual addresses */ initrd_start = __phys_to_virt(phys_initrd_start); initrd_end = initrd_start + phys_initrd_size; } early_init_fdt_scan_reserved_mem(); reserve_elfcorehdr(); high_memory = __va(memblock_end_of_DRAM() - 1) + 1; } |
가장 처음으로 fdt_enforce_memory_region() 함수를 통해서 메모리 영역의 range를 결정한다. 이 메모리 range는 할당가능한 부분인지 계산을 진행하고 할당 가능한 메모리 range를 얻어온다.
이후 52bit kernel인지 48bit를 사용하는지에 따른 kernel bit 수를 정하고 DRAM() 공간을 초과하지 않도록 분기를 통해서 안전한 Memory range를 가져온다. DRAM공간 중 안전한 영역만을 memory영역에 할당한다.
또한 initrd영역을 reserved 영역에 할당, kernel 영역 reserved에 할당을 진행한다.
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) { /* the generic initrd code expects virtual addresses */ initrd_start = __phys_to_virt(phys_initrd_start); initrd_end = initrd_start + phys_initrd_size; } |
위와 같이 feature에 따라서 initrd영역은 물리주소 -> 가상주소로 변환하여 저장한다. 또한 DMA, CMA, DTB관련해서도 초기화를 진행한다.
특히 DTB관해서 early_init_fdt_scan_reserved_mem()에서 진행한다.
void __init early_init_fdt_scan_reserved_mem(void) { int n; u64 base, size; if (!initial_boot_params) return; /* Process header /memreserve/ fields */ for (n = 0; ; n++) { fdt_get_mem_rsv(initial_boot_params, n, &base, &size); if (!size) break; early_init_dt_reserve_memory_arch(base, size, false); } of_scan_flat_dt(__fdt_scan_reserved_mem, NULL); fdt_init_reserved_mem(); } |
Early_init_fdt_scan_reserved_mem()을 자세히 보면 fdt_get_mem_rsv를 통해서 우선 예약된 Device Tree Entry들을 불러오고 각 entry의 base, size값을 얻어온 뒤 early_init_dt_reserve_memory_arch()를 통해서 실제적으로 memblock_reserve()를 호출함으로써 reserve영역에 할당을 진행한다.
2) Memblock 할당
Memblock할당에 대해서 알아보면 기본적으로 memory 타입에 대한 할당을 기준으로 설명하면 memblock_add() API를 통해서 할당이 진행된다.
하지만 여기서 NUMA 시스템인가 아닌가로 사용하는 API가 다르다.
NUMA system 즉 메모리 관리를 진행할 때 NUMA node형태로 관리한다면 memblock_add_node() API를 통해서 할당을 진행해야 한다.
NUMA여부에 따른 차이는 node id값 밖에 존재하지 않으며 NUMA를 지원하지 않는 경우 node 0으로 통일해서 처리한다.
할당 순서는 앞서 본바와 같이 memblock_add_range()를 통해서 할당할 영역을 찾고 만약 이웃하는 영역에 예쁘게 할당되어 블록 간 노드 구분이 따로 없다면 인접한 블록과 merge를 진행한다. 또한 만약 관리하는 블록의 개수가 MAX값이라면 이웃하지 않더라도 블록 크기를 2배로 크게하는 등의 작업을 거쳐서 이미 할당된 다른 블록과 merge를 진행한다.
Merge를 진행할 때는 node id값이 같은 경우에만 merge를 진행한다. NUMA의 경우 node id가 다르다는 것은 아예 다르게 묶이는 패키지 요소개념과 같으므로 이를 구분해주기위해서 node id가 다른 경우에는 이웃하는 영역이라 할지라도 merge하지 않는다.
- ref
코드로 배우는 ARM 리눅스 커널
https://elixir.bootlin.com/linux/v5.14.16/source/...
https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-1.html
'OS > Linux' 카테고리의 다른 글
[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) - I/O mapping (0) | 2021.12.26 |
[Linux Kernel] Kernel 분석(v5.14.16) - Page Table (2) (0) | 2021.12.12 |
[Linux Kernel] Kernel 분석(v5.14.16) - Page Table (1) (0) | 2021.12.06 |