Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

229224
1178
109351


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

우리는 '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 슬랩 캐시 분석   
커널 메모리 디버깅


핑백

덧글

댓글 입력 영역