Kernel에서 할당하는 메모리는 주로 엄청 작은 크기의 메모리를 할당한다. 때문에 page단위의 메모리를 할당하게되면 너무 많은 메모리 낭비와 성능에서의 비효율성이 극대화 있다.

 

위의 문제를 해결하기 위해서 Linux에서는 크게 가지의 메모리 할당방법을 지원하고 kernel에서 메모리를 할당 가지 방법을 주로 사용한다.

하나는 kmalloc, 다른 하나는 vmalloc이다.

 

1. kmalloc

DMA physical 메모리와 1:1 매핑되는데 주로 사용하며 기본적으로 메모리를 연속적(물리적으로)으로 할당하고, sleep되지 않는 특징이 있다.

이러한 특징으로 DMA ZONE_NORMAL, ZONE_DMA에서 사용하며 vmalloc보다 빠른 성능을 자랑한다.(vmalloc 가상, kmalloc 사실상 physical 1:1매핑으로 당연히 빠르다)

 

ex) DMA, 인터럽트 핸들러

 

 

 

 

1) flag

kernel에서 메모리를할당 때는 flag 통해서 원하는 메모리 할당을 유도할 있다.

ex) __GFP_DMA : ZONE_DMA 영역에 할당을 요청

 

ZONE 영역을 정할 있고 page, 워터마크, 회수, 액션 복합적인 플래그가 존재한다.

include/linux/gfp.h

/* Plain integer GFP bitmasks. Do not use this directly. */
#define ___GFP_DMA                0x01u
#define ___GFP_HIGHMEM                0x02u
#define ___GFP_DMA32                0x04u
#define ___GFP_MOVABLE                0x08u
#define ___GFP_RECLAIMABLE        0x10u
#define ___GFP_HIGH                0x20u
#define ___GFP_IO                0x40u
#define ___GFP_FS                0x80u
#define ___GFP_ZERO                0x100u
#define ___GFP_ATOMIC                0x200u
#define ___GFP_DIRECT_RECLAIM        0x400u
#define ___GFP_KSWAPD_RECLAIM        0x800u
#define ___GFP_WRITE                0x1000u
#define ___GFP_NOWARN                0x2000u
#define ___GFP_RETRY_MAYFAIL        0x4000u
#define ___GFP_NOFAIL                0x8000u
#define ___GFP_NORETRY                0x10000u
#define ___GFP_MEMALLOC                0x20000u
#define ___GFP_COMP                0x40000u
#define ___GFP_NOMEMALLOC        0x80000u
#define ___GFP_HARDWALL                0x100000u
#define ___GFP_THISNODE                0x200000u
#define ___GFP_ACCOUNT                0x400000u
#define ___GFP_ZEROTAGS                0x800000u
#define ___GFP_SKIP_KASAN_POISON        0x1000000u
#ifdef CONFIG_LOCKDEP
#define ___GFP_NOLOCKDEP        0x2000000u
#else
#define ___GFP_NOLOCKDEP        0
#endif
ref : https://elixir.bootlin.com/linux/v5.14.16/source/include/linux/gfp.h

 

 

 

2) 할당

include/linux/slab.h

/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
**/
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
unsigned int index;
#endif
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
index = kmalloc_index(size);


if (!index)
return ZERO_SIZE_PTR;


return kmem_cache_alloc_trace(
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
#endif
}
return __kmalloc(size, flags);
}
ref : https://elixir.bootlin.com/linux/v5.14.16/source/include/linux/slab.h

할당 코드를 보면 크게 3가지 방법으로 할당을 진행한다.

 

가장 먼저 사이즈에 대해서는 2^order 이용한 buddy 활용하고

상수의 경우(CONFIG_SLOB) kmalloc_index() 통해서 상수 size 맞는 free index 뽑아내고 해당 index 할당을 진행한다.

static __always_inline unsigned int __kmalloc_index(size_t size,
    bool size_is_constant)
{
if (!size)
return 0;


if (size <= KMALLOC_MIN_SIZE)
return KMALLOC_SHIFT_LOW;


if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
return 1;
if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
return 2;
if (size <=          8) return 3;
if (size <=         16) return 4;
if (size <=         32) return 5;
if (size <=         64) return 6;
if (size <=        128) return 7;
if (size <=        256) return 8;
if (size <=        512) return 9;
if (size <=       1024) return 10;
if (size <=   2 * 1024) return 11;
if (size <=   4 * 1024) return 12;
if (size <=   8 * 1024) return 13;
if (size <=  16 * 1024) return 14;
if (size <=  32 * 1024) return 15;
if (size <=  64 * 1024) return 16;
if (size <= 128 * 1024) return 17;
if (size <= 256 * 1024) return 18;
if (size <= 512 * 1024) return 19;
if (size <= 1024 * 1024) return 20;
if (size <=  2 * 1024 * 1024) return 21;
if (size <=  4 * 1024 * 1024) return 22;
if (size <=  8 * 1024 * 1024) return 23;
if (size <=  16 * 1024 * 1024) return 24;
if (size <=  32 * 1024 * 1024) return 25;


if ((IS_ENABLED(CONFIG_CC_IS_GCC) || CONFIG_CLANG_VERSION >= 110000)
    && !IS_ENABLED(CONFIG_PROFILE_ALL_BRANCHES) && size_is_constant)
BUILD_BUG_ON_MSG(1, "unexpected size in kmalloc_index()");
else
BUG();


/* Will never be reached. Needed because the compiler may complain */
return -1;
}
ref : https://elixir.bootlin.com/linux/v5.14.16/source/include/linux/slab.h#L379

위와 같이 상수 size 대하여 분기문을 통해서 가장 최적화된 index 출력해준다. (가장 메모리 낭비가 적은 크기의 index 사용해서 할당을 진행하는 )

요청한 크기가 상수이므로 상수에따른 적합한 free index 활용하는 방법이다.

 

 

그리고 마지막은 동적으로 할당해주는 방법이다. 경우에는 크지 않은 메모리 상수 size 아닌 경우를 예로들 있다.

경우에는 physical 주소를 찾아서 size만큼 할당을 진행한다.

 

 

 

 

 

 

 

2. vmalloc

vmalloc 가상메모리 공간을 사용하는 방법으로 가상메모리는 연속적으로 할당하나 physical메모리는 연속적이지 않다. 때문에 메모리를 효율적으로 사용할 있는 장점이있다. 하지만 가상메모리를 물리주소로 치환하기위한 TLB등의 시스템이 부가적으로 필요하고 이로인해 효율성 저하가 발생한다.

때문에 kernel단에서는 주로 vmalloc보다는 kmalloc 사용해서 할당을 진행한다.

 

vmalloc kamlloc과달리 가상메모리를 이용하기 때문에 kmalloc에서 할당,해제만 있는 것과는 달리 vmalloc에서는 할당, 해제, 매핑, 언매핑이 API 존재한다.

 

 

 

 

1) 할당

vmalloc 할당하기위해서는 가장먼저 init과정을 거치며 init 통해 object 할당과 vmem_list 설정한다. vmem 효율성을 위해 레드블랙트리 알고리즘을 사용하며 init과정에서 기존 object로하여금 insert하여 사용가능한 레드블랙트리를만드는 정도까지의 작업을 init으로 보면된다.

 

이후 vmalloc() 통해서 할당이 진행된다. vamlloc struct 만들고 area 할당받아서 레드블랙트리에 insert 진행한다.

kmalloc 마찬가지로 flag를통해서 원하는 메모리 할당을 유도할 있다.

 

할당한 메모리들은 가상메모리 공간에서 연속적으로 존재해야 하기 때문에 mapping 과정을 따로 거치게 된다.

 

mm/vmalloc.c

/**
 * __vmalloc_node_range - allocate virtually contiguous memory
 * @size:                  allocation size
 * @align:                  desired alignment
 * @start:                  vm area range start
 * @end:                  vm area range end
 * @gfp_mask:                  flags for the page level allocator
 * @prot:                  protection mask for the allocated pages
 * @vm_flags:                  additional vm area flags (e.g. %VM_NO_GUARD)
 * @node:                  node to use for allocation or NUMA_NO_NODE
 * @caller:                  caller's return address
 *
 * Allocate enough pages to cover @size from the page level
 * allocator with @gfp_mask flags.  Map them into contiguous
 * kernel virtual space, using a pagetable protection of @prot.
 *
 * Return: the address of the area or %NULL on failure
 */
void *__vmalloc_node_range(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, unsigned long vm_flags, int node,
const void *caller)
{
struct vm_struct *area;
void *addr;
unsigned long real_size = size;
unsigned long real_align = align;
unsigned int shift = PAGE_SHIFT;


if (WARN_ON_ONCE(!size))
return NULL;


if ((size >> PAGE_SHIFT) > totalram_pages()) {
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, exceeds total pages",
real_size);
return NULL;
}


if (vmap_allow_huge && !(vm_flags & VM_NO_HUGE_VMAP)) {
unsigned long size_per_node;


/*
 * Try huge pages. Only try for PAGE_KERNEL allocations,
 * others like modules don't yet expect huge pages in
 * their allocations due to apply_to_page_range not
 * supporting them.
 */


size_per_node = size;
if (node == NUMA_NO_NODE)
size_per_node /= num_online_nodes();
if (arch_vmap_pmd_supported(prot) && size_per_node >= PMD_SIZE)
shift = PMD_SHIFT;
else
shift = arch_vmap_pte_supported_shift(size_per_node);


align = max(real_align, 1UL << shift);
size = ALIGN(real_size, 1UL << shift);
}


again:
area = __get_vm_area_node(real_size, align, shift, VM_ALLOC |
  VM_UNINITIALIZED | vm_flags, start, end, node,
  gfp_mask, caller);
if (!area) {
warn_alloc(gfp_mask, NULL,
"vmalloc error: size %lu, vm_struct allocation failed",
real_size);
goto fail;
}


addr = __vmalloc_area_node(area, gfp_mask, prot, shift, node);
if (!addr)
goto fail;


/*
 * In this function, newly allocated vm_struct has VM_UNINITIALIZED
 * flag. It means that vm_struct is not fully initialized.
 * Now, it is fully initialized, so remove this flag here.
 */
clear_vm_uninitialized_flag(area);


size = PAGE_ALIGN(size);
kmemleak_vmalloc(area, size, gfp_mask);


return addr;


fail:
if (shift > PAGE_SHIFT) {
shift = PAGE_SHIFT;
align = real_align;
size = real_size;
goto again;
}


return NULL;
}
ref : https://elixir.bootlin.com/linux/v5.14.16/source/mm/vmalloc.c

vmalloc 할당하는 메인 동작을 진행하는 function으로 vmalloc 경우 새로운 area 할당해서 넣을 있는 기존의 area단위로 먼저 구하고 __vmalloc_area_node 통해서 할당과 매핑을 진행한다.

 

와같이 kmalloc_node 통해서 실제적으로 area page할당을 진행한다. 할당하는 방법은 kmalloc 마찬가지로 index 구해서 freeList해당 인덱스에 할당을 진행한다.

 

staticintvmap_pages_range(unsignedlongaddr,unsignedlongend,pgprot_tprot,structpage**pages,unsignedintpage_shift){interr;err=vmap_pages_range_noflush(addr,end,prot,pages,page_shift);flush_cache_vmap(addr,end);returnerr;}


출처: <https://elixir.bootlin.com/linux/v5.14.16/source/mm/vmalloc.c#L603>

그리고 매핑은 함수를 호출해서 진행한다. area->page할당을통해서 셋업된 area 대하여 virtual memory 연결해주는 역할을 진행한다.

 

 

- ref

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

+ Recent posts