ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

192239
1625
172589


[리눅스커널] 메모리 관리: kmalloc 캐시 슬럽 오브젝트 할당 커널 함수 분석하기 14. 메모리 관리

우리는 'kmalloc() 함수를 호출하면 동적 메모리를 할당할 수 있다.'란 사실을 알고 있습니다. 하지만 kmalloc() 함수에서 호출하는 커널 내부 함수는 거의 분석하지는 않는 듯 합니다.

이번 시간에는 kmalloc() 함수와 이 함수에서 호출하는 다음 함수를 분석합니다.
kmalloc_index()
kmem_cache_alloc_trace()
slab_alloc()
slab_alloc_node()

kmalloc() 함수 분석하기

우리가 드라이버 드라이버에서 동적 메모리를 할당할 때 주로 사용하는 함수는 kmalloc()입니다. 그런데 kmalloc() 함수는 생각보다 분석하기 어렵습니다.

    슬랩 캐시와 슬럽 오브젝트의 개념을 알고 있어야 분석이 가능하기 때문입니다.

이어서 kmalloc() 함수 구현부를 보겠습니다. 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/slab.h]
01 static __always_inline void *kmalloc(size_t size, gfp_t flags)
02 {
03 if (__builtin_constant_p(size)) {
04 if (size > KMALLOC_MAX_CACHE_SIZE)
05 return kmalloc_large(size, flags);
06 #ifndef CONFIG_SLOB
07 if (!(flags & GFP_DMA)) {
08 unsigned int index = kmalloc_index(size);
09
10 if (!index)
11 return ZERO_SIZE_PTR;
12
13 return kmem_cache_alloc_trace(kmalloc_caches[index],
14 flags, size);
15 }
16 #endif
17 }
18 return __kmalloc(size, flags);
19 }

먼저 kmalloc() 함수 선언부를 보면 __always_inline 키워드가 보입니다. GCC 컴파일러는 함수에 이 키워드가 선언됐으면 함수 내 코드를 소스에 붙혀줍니다. 그래서 어셈블리 코드에서 kmalloc() 함수를 호출한 부분을 보면 kmalloc() 함수 대신 kmem_cache_alloc_trace() 함수 심볼을 볼 수 있습니다.

kmalloc() 함수 동작은 다음과 같이 분류할 수 있습니다.
kmalloc_index() 함수를 호출해 메모리 할당 사이즈에 맞는 kmalloc 슬랩 캐시 인덱스 선택 
슬랩 캐시 주소와 함께 kmem_cache_alloc_trace() 함수 호출 

이제 kmalloc() 함수 코드를 분석하겠습니다. 먼저 04번째 줄 코드를 보겠습니다.
04 if (size > KMALLOC_MAX_CACHE_SIZE)
05 return kmalloc_large(size, flags);

동적 메모리 할당 사이즈인 size가 8192보다 크면 kmalloc_large() 함수를 호출합니다.
참고로 KMALLOC_MAX_CACHE_SIZE 는 다음 계산식으로 8192가 됩니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/slab.h]
#define KMALLOC_MAX_CACHE_SIZE (1UL << KMALLOC_SHIFT_HIGH)

8192 = 1UL << (12 + 1))

이어서 06~16번째 줄 구간 코드를 분석하기 전에 코드 실행 조건을 점검해보겠습니다.
06번째 줄 코드를 보면 CONFIG_SLOB 컨피그가 선언돼 있지 않을 때 컴파일된다는 사실을 알 수 있습니다. 라즈비안을 비롯한 대부분 리눅스 시스템에서는 CONFIG_SLUB 컨피그로 커널을 사용하니 06~16번째 줄 코드는 컴파일 됩니다.

다음으로 07번째 줄 코드를 보겠습니다. kmalloc() 함수를 호출할 때 GFP_DMA 플래그를 설정했는지 체크하는 동작입니다. 대부분 디바이스 드라이버에서 kmalloc() 함수를 호출할 때 GFP_KERNEL을 호출하니 일반적으로 08~14번째 줄 코드가 실행합니다.

이어서 08번째 줄 코드를 보겠습니다.
08 unsigned int index = kmalloc_index(size);

kmalloc_index() 함수를 호출해 kmalloc 슬랩 캐시 인덱스를 읽습니다. kmalloc_index() 함수 코드는 조금 후 분석하겠습니다.
 
이어서 13~14번째 줄 코드를 보겠습니다.
13 return kmem_cache_alloc_trace(kmalloc_caches[index],
14 flags, size);

다음 인자와 함께 kmem_cache_alloc_trace() 함수를 호출합니다.
kmalloc_caches[index]: kmalloc 슬랩 캐시 인덱스인 index로 kmalloc_caches 배열 인덱스 값을 전달함. 타입은 struct kmem_cache 구조체임
flags: kmalloc() 함수 호출 시 플래그 
size: 메모리 할당 사이즈 

kmalloc_index() 함수

kmalloc_index() 함수는 kmalloc() 함수로 요청한 메모리 사이즈에 맞게 kmalloc 슬랩 캐시 인덱스를 계산하는 기능입니다. kmalloc_index() 함수 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/slab.h]
01 static __always_inline unsigned int kmalloc_index(size_t size)
02 { 
03 if (!size)
04 return 0;
05
06 if (size <= KMALLOC_MIN_SIZE)
07 return KMALLOC_SHIFT_LOW;
08
09 if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
10 return 1;
11 if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
12 return 2;
13 if (size <=          8) return 3;
14 if (size <=         16) return 4;
15 if (size <=         32) return 5;
16 if (size <=         64) return 6;
17 if (size <=        128) return 7;
18 if (size <=        256) return 8;
19 if (size <=        512) return 9;
20 if (size <=       1024) return 10;
21 if (size <=   2 * 1024) return 11;
...
22 if (size <=  64 * 1024 * 1024) return 26;
23 BUG();
24
25 /* Will never be reached. Needed because the compiler may complain */
26 return -1;
27 }

할당하려는 메모리 사이즈에 따라 인덱스를 반환하는 코드입니다.

0~64M 구간 별로 kmalloc 슬랩 캐시 인덱스를 반환합니다. 인덱스의 범위는 0~26입니다.

kmalloc() 함수로 요청한 메모리 사이즈에 따른 인덱스는 다음과 같습니다.
2 = 128~192
6 = 33~64
7 = 65~127  
8 = 193~256
26 = 32M-1 .. 64M

예를 들어 size가 54이면 다음 코드와 같이 6을 반환합니다.
16 if (size <=         64) return 6; 

kmalloc 슬랩 캐시를 관리하는 자료구조는 kmalloc_caches 배열 타입 전역 변수이며
다음 코드에 정의돼 있습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/mm/slab_common.c]
struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1] __ro_after_init;
EXPORT_SYMBOL(kmalloc_caches);

이제부터 슬럽 오브젝트를 할당하는 커널 내부 함수를 분석합니다.

kmem_cache_alloc_trace() 함수 분석하기

kmem_cache_alloc_trace() 함수는 slab_alloc() 함수를 호출해 슬럽 오브젝트를 할당하는 기능입니다.

[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/mm/slub.c]
1 void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
2 {
3 void *ret = slab_alloc(s, gfpflags, _RET_IP_);
4 trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
5 kasan_kmalloc(s, ret, size, gfpflags);
6 return ret;
7 }

3 번째 줄 코드는 slab_alloc() 함수를 호출한 포인터형 반환값을 ret에 저장합니다.

다음 4 번째 줄 코드는 kmalloc 이벤트를 켰을 때 kmalloc() 함수 동작을 저장합니다.
kmalloc 이벤트를 키고 ftrace 로그를 받으면 다음 패턴의 메시지를 볼 수 있습니다.
<...>-10672 [001] ....  2122.398293: kmalloc: call_site=80294218 ptr=b546bd00 bytes_req=200 bytes_alloc=256 gfp_flags=GFP_KERNEL|__GFP_ZERO

slab_alloc() 함수 코드를 보겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/mm/slub.c]
1 static __always_inline void *slab_alloc(struct kmem_cache *s,
2 gfp_t gfpflags, unsigned long addr)
3 {
4 return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
5 }   

slab_alloc() 함수를 특별한 동작을 하지 않습니다. slab_alloc_node() 함수를 호출한 후 결과를 반환합니다.

slab_alloc_node() 함수 분석하기

slab_alloc_node() 함수는 슬럽 오브젝트를 할당하는 핵심 역할을 수행합니다. 코드를 보면서 세부 동작을 파악해볼까요?
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/mm/slub.c]
01 static __always_inline void *slab_alloc_node(struct kmem_cache *s,
02 gfp_t gfpflags, int node, unsigned long addr)
03 {
04 void *object;
05 struct kmem_cache_cpu *c;
06 struct page *page;
07 unsigned long tid;
...
08 redo:
...
09 do {
11 tid = this_cpu_read(s->cpu_slab->tid);
12 c = raw_cpu_ptr(s->cpu_slab);
13 } while (IS_ENABLED(CONFIG_PREEMPT) &&
14 unlikely(tid != READ_ONCE(c->tid)));
...
15 object = c->freelist;
16 page = c->page;
17 if (unlikely(!object || !node_match(page, node))) {
18 object = __slab_alloc(s, gfpflags, node, addr, c);
19 stat(s, ALLOC_SLOWPATH);
20 } else {
21 void *next_object = get_freepointer_safe(s, object);
22
23 if (unlikely(!this_cpu_cmpxchg_double(
24 s->cpu_slab->freelist, s->cpu_slab->tid,
25 object, tid,
26 next_object, next_tid(tid)))) {
27
28 note_cmpxchg_failure("slab_alloc", s, tid);
29 goto redo;
30 }
31 prefetch_freepointer(s, next_object);
32 stat(s, ALLOC_FASTPATH);
33 }
34
35 if (unlikely(gfpflags & __GFP_ZERO) && object)
36 memset(object, 0, s->object_size);
37
38 slab_post_alloc_hook(s, gfpflags, 1, &object);
39
40 return object;
41 }

slab_alloc_node() 함수 실행은 다음과 같이 분류할 수 있습니다.
1. 슬럽 캐시 cpu_slab 으로 per-cpu 슬럽 캐시를 로딩
2. per-cpu 슬랩 캐시 필드 중 struct page 타입인 page에 접근 
3. 슬럽 오브젝트 할당
  3.1 page->freelist가 Free 슬럽 오브젝트를 가르키고 있으면?
     ; 해당 슬럽 오브젝트 반환 
  3.2 page->freelist가 NULL이면?
     ; __slab_alloc() 함수 호출해 슬럽 페이지를 할당받고 슬럽 오브젝트를 반환함 

slab_alloc_node() 함수 전체 동작을 살펴봤으니 이제부터 소스 코드 분석을 시작하겠습니다. 먼저 12 번째 줄 코드를 먼저 보겠습니다.   
09 do {
11 tid = this_cpu_read(s->cpu_slab->tid);
12 c = raw_cpu_ptr(s->cpu_slab);
   
struct kmem_cache 구조체인 s의 cpu_slab 주소로 per-cpu 슬럽 캐시를 로딩해 struct kmem_cache_cpu 타입인 c 지역 변수에 로딩합니다. 

다음 15 번째 줄 코드를 보겠습니다.
15 object = c->freelist;
16 page = c->page;

struct kmem_cache_cpu 타입인 포인터형 c 지역변수의 freelist를 object에 저장합니다.
c->freelist가 할당할 슬럽 오브젝트 주소입니다.

16 번째 줄은 c 지역 변수 page 필드를 page 지역 변수에 저장합니다.

17~19 번째 줄 코드를 보겠습니다.
17 if (unlikely(!object || !node_match(page, node))) {
18 object = __slab_alloc(s, gfpflags, node, addr, c);
19 stat(s, ALLOC_SLOWPATH);

17 번째 줄 코드는 if문이 있으므로 조건문입니다. unlikely가 지시지는 낮은 확률로 조건문이 실행될 것이라고 GCC 컴파일러에게 알려줍니다.

슬럽 오브젝트가 없거나 per-cpu 슬럽 캐시에서 로딩한 페이지가 노드와 다른 경우 18 번째 줄 코드를 실행합니다. 18 번째 줄은 __slab_alloc() 함수 호출로 슬럽 오브젝트를 할당 받습니다.

21~32 번째 줄은 일반적인 상황에서 호출됩니다.

21 번째 줄 코드를 보겠습니다.
21 void *next_object = get_freepointer_safe(s, object);

슬럽 오브젝트와 슬랩 캐시를 인자로 받아 다음 Free 슬럽 오브젝트 주소를 구합니다.

이번엔 이전 소절에서 소개한 그림을 보면서 슬럽 오브젝트 할당 과정을 살펴볼까요?
 
[그림 14.27] slab_alloc_node() 함수에서 할당하는 슬랩 캐시 관련 자료구조
위 그림에서 ①로 표시된 부분을 보겠습니다. 

    슬랩 캐시 구조체 cpu_slab 구조체에 액세스해 struct kmem_cache_cpu 구조체 
    per-cpu 슬럽 캐시 주소를 로딩하는 동작입니다. 

이는 다음 코드에서 동작합니다.
05 struct kmem_cache_cpu *c;
...
12 c = raw_cpu_ptr(s->cpu_slab);

12번째 줄에서 보이는 raw_cpu_ptr() 함수는 per-cpu 오프셋을 계산한 주소를 반환하는 기능입니다. 

이번엔 위 그림에서 ②부분을 볼까요? struct kmem_cpu_cache 구조체 freelist 필드가 0x93c25080 주소를 가르키고 있습니다. 

    0x93c25080 가 할당할 슬럽 오브젝트 시작 주소입니다.

이 동작은 다음 코드에서 수행합니다.
15 object = c->freelist;
...
40 return object;

코드 분석으로 kmalloc() 함수를 호출하면 커널이 반환하는 포인터의 실체는 슬럽 오브젝트란 사실을 알 수 있습니다.


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

Reference(커널 메모리 소개) 
가상 주소를 물리 주소로 어떻게 변환할까?   
메모리 존(Zone)에 대해서   
커널 메모리 할당은 어떻게 할까   
슬랩 메모리 할당자와 kmalloc 슬랩 캐시 분석   
커널 메모리 디버깅


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2







핑백

덧글

댓글 입력 영역