[Research] Linux Kernel Basic - (5) 커널에서의 메모리 할당 3편

[Research] Linux Kernel Basic - (5) 커널에서의 메모리 할당 3편

서론

해당 포스트에서는 리눅스 커널(Linux Kernel)에서도 연산을 수행하기 위해서 데이터가 저장되어야만 하는 공간이 있어야 한다. 이러한 공간의 주체는 메모리(memory)이며 리눅스 커널(Linux Kernel)에서의 메모리의 할당 방식을 알아본다. 이전 포스트에서는 페이지 단위로 메모리를 할당하는 alloc_page에 대해 언급하였고 해당 포스트에서는 작은 메모리 공간 동적 할당에 대한 포스팅을 진행한다.

페이지 단위 할당의 문제

Internal Fragmentation

일반적인 리눅스 시스템의 경우는 물리 메모리를 페이지 단위로 관리한다. 메모리 할당과 메모리 해제에 있어서 페이지의 크기인 4KB(0x1000)을 기준으로 이루어진다는 것이다. 일반적으로 사이즈가 페이지 단위에 근접하는 프로그램이 메모리에 로드되어 프로세스가 되는 경우는 문제가 느껴지지 않을 수 있다. 하지만 커널의 경우에서도 작은 사이즈의 메모리의 동적할당이 필요한 경우가 있을 것이다. 만약 32바이트의 메모리가 필요한 경우에 다음과 같이 페이지 단위로 관리하게 된다면 엄청난 양의 Internal Fragmentation이 발생한다. 더 나아가 빈번한 할당과 해제는 속도에 있어서도 매우 비효율적이다. 이러한 특성들은 메모리의 관리를 페이지 단위로 하기에 발생하는 특성이며 다음 언급할 내용은 이러한 문제를 해결하는 방법이다.

슬랩 할당자(Slab Allocator)

슬랩 할당자의 개념을 이해하기 위해서는 커널 개발자가 되었다고 가정해보겠다. 운영체제를 사용하며 프로세스를 시작하거나 하는 등의 일들을 진행할 수 있다. 그 과정에서 다음과 같은 특성을 발견하였다.

특정한 크기를 가진 메모리가 할당과 해제를 반복한다.

이러한 특성을 이용하면 보다 효율적인 메모리 할당 시스템을 구현할 수 있다.

자주 쓰는 메모리 패턴을 정의한 후 미리 할당해 놓는다.
해당 패턴에 대한 메모리 할당 요청이 발생할 경우 할당해 놓은 메모리를 사용하게 한다.
해당 패턴에 대한 메모리 해제 요청이 발생하면 해제하지 않고 대기한다. 
다시 해당 패턴으로 메모리 할당 요청을 할 가능성이 높다.

해당 구조를 메모리 풀(memory pool) 구조라고 한다. b30w0lf님의 운영체제 시간에 잠깐 여러가지 공을 수영장에 넣어두었다가 준다는 클립 아트를 보았었는데 그것이 바로 이것이다!

이러한 형태의 메모리 관리 구조를 슬랩 메모리 할당이라고 한다.

슬랩 캐시, 슬랩 오브젝트, 슬랩 페이지

슬랩 캐시(Slab Cache)

슬랩 메모리 할당방식을 이야기하며 언급하고 넘어가야 하는 개념이 있다. 슬랩 캐시(Slab Cache), 슬랩 오브젝트(Slab Object), 슬랩 페이지(Slab Page)에 관한 내용이다.

슬랩 캐시(Slab Cache)란 일종의 슬랩 메모리 할당을 하며 관리하는 주체인 매니저와 같은 형태라고 볼 수 있다. 예를 들어 특정 바이트 만큼의 메모리 할당 요청이 들어올 경우 이를 제공해주고 할당 해제한 슬랩 메모리를 받아서 다시 요청이 들어올 경우 이를 내어주는 주체가 슬랩 캐시라고 보면 된다.

ray@ubuntu:~/Kernel/device_driver$ sudo cat /proc/slabinfo 
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>

mm_struct            180    180   1088   30    8 : tunables    0    0    0 : slabdata      6      6      0
task_struct          940    955   6080    5    8 : tunables    0    0    0 : slabdata    191    191      0
.
kmalloc-512        21464  21632    512   64    8 : tunables    0    0    0 : slabdata    338    338      0
kmalloc-256         4843   5248    256   64    4 : tunables    0    0    0 : slabdata     82     82      0
kmalloc-192         2058   2058    192   42    2 : tunables    0    0    0 : slabdata     49     49      0
kmalloc-128         1472   1472    128   64    2 : tunables    0    0    0 : slabdata     23     23      0
kmalloc-96          6029   6300     96   42    1 : tunables    0    0    0 : slabdata    150    150      0
kmalloc-64         12318  12800     64   64    1 : tunables    0    0    0 : slabdata    200    200      0
kmalloc-32         11050  11136     32  128    1 : tunables    0    0    0 : slabdata     87     87      0
kmalloc-16         10240  10240     16  256    1 : tunables    0    0    0 : slabdata     40     40      0
kmalloc-8          13312  13312      8  512    1 : tunables    0    0    0 : slabdata     26     26      0

다음의 명령어를 통해 슬랩 캐시를 확인할 수 있다. 해당 결과를 보면 익숙한 구조체인 task_struct, mm_struct등이 보이며 kmalloc-n과 같은 형태의 n바이트의 메모리에 대한 슬랩 캐시들이 존재하는 것을 확인할 수 있다. 다음과 같은 캐시들이 슬랩 메모리를 관리하는 주체이다. 하나의 슬랩 캐시(Slab Cache)내의 모든 메모리들은 동일한 사이즈로 제공된다는 사실을 알고 있자.

슬랩 오브젝트(Slab Object)와 슬랩 페이지(Slab Page)

슬랩 오브젝트(Slab Object)란 슬랩 캐시가 미리 확보해 놓은 메모리를 의미하며 이러한 오브젝트들로 구성된 페이지를 슬랩 페이지(Slab Page)라고 한다. 결과적으로 슬랩 페이지 또한 페이지이며 4KB의 크기를 갖는다. 상단의 그림을 확인해보면 특정한 크기로 미리 확보해 놓은 메모리가 슬랩 객체(Slab Object)임을 확인할 수 있으며 해당 메모리가 사용되는 경우는 Alloc으로 마킹되어 있으며 사용하지 않는 경우는 Free로 마킹되어 있는 것을 확인할 수 있다. 이러한 슬립 객체들이 모여서 슬랩 페이지를 구성하고 있음을 볼 수 있다.

Free된 슬랩 객체의 경우는 freelist라는 리스트 자료구조로 관리되며 이를 통해 해당 메모리에 대해 요청이 들어올 경우 이를 확인하여 슬랩 객체의 주소를 반환할 수 있다.

커널 코드 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}

kmalloc 함수를 확인하면 내부적으로 kmem_cache_alloc_trace를 호출함을 확인할 수 있다.

1
2
3
4
5
6
7
void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
{
void *ret = slab_alloc(s, gfpflags, _RET_IP_);
trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
kasan_kmalloc(s, ret, size, gfpflags);
return ret;
}

kmem_cache_alloc_trace 함수는 slab_alloc함수에 kmem_cache와 gfpflags, 주소인 address를 전달함을 확인할 수 있다.

1
2
3
4
5
static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, unsigned long addr)
{
return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
}

slab_alloc 함수는 slab_alloc_node 함수를 호출함을 알수 있다.

slab_alloc_node의 경우는 코드의 중요한 부분만 오디팅하여 작성하겠다. slab_alloc_node 함수는 다음과 같은 로직을 진행한다.

STEP1. kmem_cache_cpu 구조체로 관리되는 per-cpu 슬럽 캐시를 로딩한다.
STEP2. per-cpu 슬랩 캐시의 속성 중 struct page 타입인 page에 접근한다.
STEP3. 슬랩 오브젝트를 할당한다.
    kmem_cache_cpu 중 freelist에 해제된 슬랩 객체가 있는 경우
             해당 슬랩객체를 반환한다.
    kmem_cache_cpu 중 freelist에 어떠한 객체도 존재하지 않는 경우 
             __slab_alloc() 함수를 호출하여 새로운 슬랩 페이지를 할당한다.
             할당된 슬랩 페이지에서의 슬랩 객체를 반환한다.

슬랩(Slab), 슬럽(Slob), 슬롭(Slub)

슬랩과 슬럽 슬롭은 모두 메모리 할당자이다. 메모리를 미리 만들어 할당하는 형태의 큰 아이디어에서는 변화가 없음으로 동일한 형태로 보아도 무방하다고 조심스럽게 얘기한다. 현재 기준으로는 기본 할당자로 슬럽(Slub)을 사용하고 있다. 슬롭(Slob)의 경우는 지정한 사이즈 내 객체의 메모리 할당은 모두 처리하는 메모리 할당자라고 한다. 메모리를 할당할 경우 속도는 가장 느리나 메타 데이터와 같은 부가 정보가 필요하지 않아 메모리 소모가 적어 임베디드에 적용이 된다고 한다.

결론

3편에 걸쳐 리눅스 커널에서의 메모리 할당 방식을 알아보았다. 다음 편에서는 실제 디바이스 드라이버를 구현하는 모듈 프로그래밍에 대해서 진행해보도록 하겠다.

참고

b30w0lf님의 운영체제(Operating System)
http://egloos.zum.com/rousalome/v/10001242
https://jiravvit.tistory.com/entry/linux-kernel-4-슬랩할당자