지금까지 커널에서 지원하는 동적 메모리 할당 방식에 대해 알아봤습니다. 배운 내용을 한 문장으로 정리해볼까요?
kmalloc() 함수를 호출해 동적 메모리를 할당 받는다.
물론 맞는 이야기입니다만 커널 메모리 관점으로 보면 '추상적'인 내용을 담고 있습니다. 그렇다면 누군가 아래와 같이 질문을 할 수 있습니다.
kmalloc() 함수를 호출하면 커널 내부에서 어떤 동작을 할까?
이에 대한 내용을 이번 절에서 다루고자 합니다. 그러면 커널 내부에서 kmalloc() 함수가 작동하는 세부 원리를 이해하려면 무엇을 배워야 할까요? 다음 개념을 알 필요가 있습니다.
슬랩의 의미
슬럽 오브젝트의 의미
Kmalloc 슬랩 캐시
처음 임베디드 리눅스에 발을 딪는 분들에겐 어려운 내용임에 틀림 없습니다. 그 이유는 다음과 같습니다.
슬럽 캐시와 슬럽 오브젝트는 리눅스 커널 메모리 세부 시스템에서 가장 이해하기 어려운
개념 중 하나입니다.
이번 절에서 다룬 내용을 읽다가 이해가 가지 않으면 넘어가고 나중에 다시 읽으셔도 좋습니다.
참고로 필자도 슬랩 할당자과 슬럽 오브젝트의 의미를 임베디드 리눅스 개발 5년 차에 겨우 이해를 했습니다.
슬랩(Slab)를 이루는 주요 개념
리눅스 커널 메모리 서브 시스템 개발자들은 다음과 같은 목표로 메모리 성능 개선을 해왔습니다.
메모리 할당 속도
메모리가 파편화 최소화
물론 메모리를 빨리 할당하고 되도록 잔여 메모리를 많이 남기려는 미션입니다.
동적 메모리 할당에서 슬랩(Slab)이란 무엇인가
누군가 여러분에게 '리눅스 커널은 메모리를 어떤 단위로 관리하는가?'라고 묻는다면 어떻게 답하실까요? 아마 다음과 같은 대답을 할 것입니다.
리눅스 커널의 물리 메모리는 페이지 단위로 관리합니다.
페이지의 크기는 4K(0x1000) 바이트입니다.
이어서 다음과 같은 질문을 드려볼까요?
제가 디바이스 드라이버에서 할당 받으려는 메모리 크기가 54바이트입니다.
만약 54 바이트만큼 메모리 할당을 요청하면 커널 내부에서는 4K 바이트(0x1000)만큼
메모리를 할당할까요?
이 질문에는 어떻게 답하실까요? 필자는 예전에 다음과 같이 틀린 대답을 했습니다.
맞습니다. 커널에서 4K(0x1000) 만큼 물리 메모리를 관리하니 4K 바이트(0x1000)만큼
메모리를 할당해줍니다.
그런데 이런 대답을 들을수록 의구심은 생깁니다.
어, 저는 54바이트만큼 메모리만 쓰면 되는데, 4K 바이트(0x2000)만큼 메모리를
할당해주면 낭비가 아닐까요? 커널 이 방식으로 '메모리를 퍼주듯 할당'해주면 메모리가
빨리 고갈되지 않을까요?
반복하지만 다음은 틀린 대답이었습니다.
54 바이트만큼 메모리 할당을 요청하면 커널 내부에서는 4K 바이트(0x1000)만큼
메모리를 할당해준다.
그런데 메모리 세미나를 진행하면 예전에 '필자가 잘못 생각했던' 대답을 들을 때가 종종 있습니다. 이번에는 그래도 오류가 없는 대답을 하겠습니다.
만약 54 바이트만큼 메모리 할당을 요청하면 커널 내부의 슬랩 메모리 할당자가 64바이트
만큼 메모리를 할당해줍니다. 이 때 'kmalloc-64' 슬랩 캐시가 작동하게 됩니다.
이렇게 리눅스 커널 동적 메모리 할당자를 “슬랩 할당자”라고 부르고 이미 할당해 놓은 메모리를 주소를 알려주는 방식으로 작동합니다.
슬랩(Slab)의 개념 알아보기
'슬랩(Slab)의 개념'은 매우 어렵습니다. 이해를 돕기 위해 50년전 구내 식당을 예를 들면서 '슬랩(Slab)'을 설명하겠습니다.
예전 개발자들을 대상으로 운영하는 구내 식당이 있었습니다. 점심 시간이 되면 개발자들이 구내 식당에 오면 일일히 먹고 싶은 반찬과 밥의 양을 정해서 주문을 했기 때문입니다. 이 방식으로 주문을 하면 구내 식당의 주방장은 음식을 조리하기 시작했습니다.
그런데 문제가 생겼습니다. 점심 시간에 많은 개발자들이 몰려와 '일일히 먹고 싶은 반찬과 밥의 양을 정해서' 주문을 한 것입니다. 주문을 하면 '반찬의 종류별로' 음식 조리를 시작하는 방식이었기 때문에 주문이 많아지면 음식을 만드는 시간이 비례해서 늘어 났습니다.
그래서 식당 관리자들은 다음과 같은 고민을 했습니다.
점심 식사 시간에 어떻게 하면 빨리 배식할 수 있을까?
먼저 관리자들은 개발자들의 식사와 주문 패턴을 조사했습니다. 데이터를 수집한 결과 다음과 같은 사실을 파악했습니다.
음식을 먹는 반찬 패턴이 어느 정도 정해져 있다.
그래서 다음과 같은 아이디어를 냈습니다.
미리 식판에 음식을 담아 놓고 있다가 사람들이 점심을 먹으러 오면 바로 식판을 주면
된다.
구내 식당에서는 다음과 같은 점심 식사 메뉴를 개발했고 종류 별로 수 백개의 식판에 음식 미리 담아 놨습니다.
첫번째 메뉴: 밥 + 국 + 고기 반찬 + 김치
두번째 메뉴: 라면 + 김치 + 공기밥
세번째 메뉴: 돈까스 + 셀러드
점심 시간이 되기 전 구내 식당에서는 각각 메뉴 별로 식판에 음식을 준비합니다. 드디어 개발자들이 점심을 먹으러 왔습니다. 개발자들은 먹고 싶은 메뉴로 가서 식판을 들고 바로 자리에 가서 점심을 먹습니다.
이 방식으로 식당을 운영하니 메뉴를 기다리며 줄을 서서 기다리는 사람의 숫자가 현저히 줄어 들었습니다. 지금 생각하면 너무나 당연한 이야기지만 슬랩의 개념을 쉽게 설명드리기 위해 길게 말씀드린 것입니다.
이번에는 원래 주제로 돌아가 커널의 동적 메모리 할당에 대한 이야기를 하겠습니다.
마찬가지로 커널 메모리 개발자들도 50년전 구내 식당 관리자와 마찬가지로 비슷한 고민을 했습니다.
'메모리를 어떻게 빨리 할당해줄까'
여러 아이디어를 내던 도중 다음과 같은 기준으로 데이터를 수집했습니다.
커널 내부에서 실제 어떤 패턴으로 메모리를 할당할까?
확인 결과 80% 이상은 특정 패턴으로 메모리를 할당과 해제를 반복한다는 사실을 확인했습니다.
그래서 다음과 같은 아이디어를 발굴 했습니다.
자주 쓰는 메모리 패턴을 정의한 후 미리 할당해 놓자.
해당 패턴에 대한 메모리 할당 요청이 있으면 바로 메모리를 할당해주자.
해당 패턴으로 메모리를 해제하면 우선 그대로 유지하자. 또 다시 해당 패턴을 메모리 할당 요청을 할 가능성이 높기 때문이다.
이렇게 이미 음식을 식판에 준비해 놓듯 동적 메모리도 메모리를 미리 할당해 놓으면 빨리 할당해 줄 수 있습니다. 이전 방식에 비해 당연히 속도가 빨라 질 것입니다. 이런 메모리 관리 방식을 ‘슬랩 메모리’ 혹은 ‘슬랩 메모리 할당’이라고 부릅니다.
이번에는 슬랩 혹은 슬랩 할당자를 이루는 주요 개념을 소개하겠습니다.
슬랩 캐시
슬랩 오브젝트(객체)
위 두 가지 개념을 제대로 이해하면 ‘슬랩 메모리’ 할당 방식을 쉽게 이해할 수 있습니다.
슬랩 캐시란 무엇인가
먼저 '슬랩 캐시'란 용어의 의미를 먼저 알아보겠습니다. 슬랩 캐시는 슬랩과 캐시의 합성어입니다. 슬랩은 이미 설명을 드렸듯이 커널에서 자주 쓰는 구조체 패턴에 따라 미리 할당한 후 이미 할당한 메모리 주소를 알려주는 기법입니다. 캐시는 컴퓨터 용어로 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킵니다.
이 두 용어를 결합한 슬랩 캐시는 다음과 같이 설명을 드릴 수 있습니다.
커널에서 자주 사용하는 구조체에 대한 동적 메모리를 미리 확보하고 관리하는 주체를
슬랩 캐시라고 부릅니다.
이제 슬랩 오브젝트에 대해 설명을 드릴 차례입니다.
슬랩 오브젝트는 슬랩 캐시가 할당해 놓은 메모리를 의미합니다.
슬랩 캐시와 슬랩 오브젝트에 대해 설명을 드렸지만 조금 이해하기 어려운 개념인 것은 사실입니다. 다시 50년 전 구내 식당을 이야기하면서 슬랩 캐시와 슬랩 오브젝트에 대해 설명을 드리겠습니다.
이전 페이지에서 설명했듯이 구내 식당에서는 점심 시간에 배식을 빨리 하기 위해 3개 메뉴의 음식을 식판에 미리 담아 놨습니다.
첫번째 메뉴: 밥 + 국 + 고기 반찬 + 김치
두번째 메뉴: 라면 + 김치 + 공기밥
세번째 메뉴: 돈까스 + 셀러드
식판의 갯수는 수 백개 이상이 될 것입니다. 미리 메뉴별로 식판을 담아 놓고 식판의 갯수를 관리하는 누군가가 있어야 합니다. 마찬가지로, 커널에서 자주 사용하는 구조체에 대한 동적 메모리를 미리 확보하고 관리하는 주체를 슬랩 캐시라고 부릅니다.
슬랩 캐시에 대한 이해를 돕기 위해 한 가지 예를 들어보겠습니다. 프로세스 속성 정보를 표현하는 자료구조는 태스크 디스크립터로 struct task_struct 구조체입니다.
프로세스를 생성할 때 마다 struct task_struct 구조체 크기만큼 메모리 할당 요청을
합니다.
커널은 프로세스를 생성할 때 struct task_struct 구조체 크기만큼 메모리를 할당하고 프로세스가 종료하면 할당한 메모리를 해제합니다. 평소 시스템에 부하가 적을 때 struct task_struct 구조체로 동적 메모리를 할당하면 성능에 문제는 없을 것입니다. 하지만 특정 상황에서 프로세스를 생성하는 빈도가 높을 수 있습니다.
하지만 struct task_struct 구조체 크기만큼 동적 메모리를 할당한 task_struct 슬랩 캐시가 있다면 동적 메모리 할당 속도는 별로 저하되지는 않을 것입니다. 이는 다음과 같은 슬랩 메모리 할당자의 중요한 원칙 때문입니다.
자주 메모리 할당을 요청하는 구조체 크기만큼 미리 동적 메모리를 확보해 놓는다.
특정 패턴으로 동적 메모리 할당을 요청하면 미리 생성한 동적 메모리 시작 주소만
알려준다.
그런데 만약 특정 조건에서 프로세스 생성 빈도가 높으면 생성하는 프로세스 갯수만큼 struct task_struct 구조체 크기만큼 메모리를 할당해달라는 요청은 늘어날 것입니다.
이번엔 task_struct 슬랩 캐시가 하는 일을 정리해보겠습니다.
자주 쓰는 struct task_struct 구조체(메모리 패턴)을 정의한 후 미리 할당해 놓자.
struct task_struct 구조체(해당 패턴)에 대한 메모리 할당 요청이 있으면 이미 할당한 메모리 시작 주소를 알려주자.
struct task_struct 구조체(해당 패턴)으로 메모리를 해제하면 우선 그대로 유지하자. 또 다시 struct task_struct 구조체(해당 패턴)을 메모리 할당 요청을 할 가능성이 높기 때문이다.
그런데 조금 더 조사를 해보니 파일을 생성할 때 struct inode 아이노드 구조체 그리고 가상 메모리 공간을 관리하는 struct mm_struct도 자주 할당한다는 사실을 확인했습니다.
그래서 다음 구조체에 대한 메모리를 미리 확보해 놓기로 했습니다.
struct task_struct
struct inode
struct mm_struct
그렇다면 리눅스 커널에서 슬랩 캐시를 표현하는 자료 구조는 무엇일까요?
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/slub_def.h]
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retriving partial slabs etc */
slab_flags_t flags;
unsigned long min_partial;
...
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
};
리눅스 커널에서 슬랩 캐시라고 말하면 위 구조체를 떠 올리면 됩니다.
필자는 자주 쓰는 구조체를 패턴을 3개만 소개했습니다. 그러면 실제 리눅스 커널에서 슬랩 캐시는 몇 개나 될까요? 터미널을 열고 'cat /proc/slabinfo' 명령어를 입력하면 다음과 같은 정보를 확인할 수 있습니다.
root@raspberrypi:/proc# 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>
fuse_request 56 56 288 28 2 : tunables 0 0 0 : slabdata 2 2 0
fuse_inode 16 16 512 16 2 : tunables 0 0 0 : slabdata 1 1 0
ip6-frags 0 0 136 30 1 : tunables 0 0 0 : slabdata 0 0 0
UDPv6 68 68 960 17 4 : tunables 0 0 0 : slabdata 4 4 0
...
kmalloc-512 1408 1408 512 16 2 : tunables 0 0 0 : slabdata 88 88 0
kmalloc-256 973 1136 256 16 1 : tunables 0 0 0 : slabdata 71 71 0
kmalloc-192 4633 4851 192 21 1 : tunables 0 0 0 : slabdata 231 231 0
kmalloc-128 3616 3616 128 32 1 : tunables 0 0 0 : slabdata 113 113 0
kmalloc-64 13965 14336 64 64 1 : tunables 0 0 0 : slabdata 224 224 0
kmem_cache_node 192 192 64 64 1 : tunables 0 0 0 : slabdata 3 3 0
kmem_cache 105 105 192 21 1 : tunables 0 0 0 : slabdata 5 5 0
확인하니 80개 정도가 됩니다. 리눅스 커널 내부 세부 서브 시스템별로 자주 쓰는 구조체 패턴을 모았으니 이 정도 갯수가 나온 듯 합니다.
슬랩 오브젝트란 무엇인가
독자 분들은 이미 슬랩 오브젝트가 무엇인지 눈치를 챘을 것이라 생각합니다. 이제 슬랩을 이루는 슬랩 오브젝트에 대한 정의를 내려 보겠습니다.
슬랩 오브젝트는 슬랩 캐시가 이미 할당 해 놓은 메모리 공간이다.
슬랩 캐시는 이미 다음 구조체에 대한 동적 메모리를 미리 확보해 놨습니다.
struct task_struct
struct inode
struct mm_struct
그러면 리눅스 커널 세부 드라이버나 함수에서 위 구조체로 메모리 할당 요청을 할 때가 있을 것입니다. 각 종류 별 슬랩 캐시는 이미 할당해 놓은 구조체 별 메모리 시작 주소를 알려줍니다. 이를 조금 더 전문적인 문장으로 표현하면 다음과 같습니다.
슬랩 캐시는 슬랩 오브젝트의 시작 주소를 반환한다.
이제 다시 이해를 돕기 위해 구내 식당 이야기를 하겠습니다.
구내 식당에서는 다음과 같은 점심 식사 메뉴를 개발했고 종류 별로 식판을 만들어 음식을 준비했습니다.
첫번째 메뉴: 밥 + 국 + 고기 반찬 + 김치
두번째 메뉴: 라면 + 김치 + 공기밥
세번째 메뉴: 돈까스 + 셀러드
점심을 먹으러 개발자가 오면 각 메뉴별로 준비한 식판을 바로 줄 것입니다.
구내 식당에서 주는 식판은 슬랩 오브젝트와 같은 개념입니다.
이제 다음 그림을 보면서 슬랩 오브젝트에 대해 정리를 해볼까요?

[그림 14.24] 4K 구간 메모리 범위 내 슬랩 오브젝트
위 그림에서 보이듯 4K 사이즈(0x1000)인 물리 메모리 범위 내 10개 슬랩 오브젝트가 있습니다. 이 중에 슬랩 캐시에 의해 동적으로 메모리를 할당된 슬랩 오브젝트는 Alloc으로 표시된 메모리 공간입니다. 이미 사용 중인 슬랩 오브젝트입니다. 반대로 Free로 표시된 부분은 아직 할당이 안된 메모리 공간입니다. 아직 사용하지 않는 슬랩 오브젝트입니다.
이 내용을 살펴보니 슬랩 오브젝트를 관리하는 자료구조가 있어야 할 것 같습니다.
슬랩 캐시는 다음 3가지의 자료구조로 슬랩 오브젝트를 관리했습니다.
slab_full: 슬랩내 전부 사용 중인 오브젝트
slab_partial: 사용 중이거나 미사용중인 슬랩 오브젝트가 같이 있음
slabs_empy: 슬랩내 전부 미사용 중인 오브젝트
커널 2.6 이전 버전에서는 이렇게 슬랩 캐시로 슬랩 할당자를 적용했었습니다.
슬럽(Slub)이란 무엇일까?
한 동안 커널은 슬랩(Slab) 할당자로 동적 메모리 할당을 처리했습니다. 처음 슬랩(Slab) 할당자를 커널에서 사용할 때는 별 문제가 되지 않았습니다. 하지만 메타 데이터를 관리하는 오버해드 문제로 커널 2.6 버전에서 슬럽(Slub) 메모리 할당자로 대체됐습니다. 그렇다면 슬랩과 슬럽의 차이점은 무엇일까요?
슬랩 오브젝트 관리를 위한 메타 정보를 최적화한 구조가 슬럽입니다.
그런데 커널 소스를 보면 슬랩과 슬럽이란 용어가 많이 보입니다.
어떤 개발자는 슬럽 오브젝트라고 말하고 어떤 분은 슬랩 오브젝트란 용어를 쓰기도 합니다.
일반적으로 슬럽 캐시는 슬랩 캐시, 슬럽 오브젝트는 슬랩 오브젝트와 거의 같은 개념이라고 보면 됩니다.
다음 리눅스 커널 검색 사이트에 접속해 kmem_cache 구조체를 검색해 볼까요?
https://github.com/raspberrypi/linux/blob/rpi-4.19.y
검색 결과 kmem_cache 구조체는 두 해더 파일에서 선언돼 있습니다.
1 include/linux/slab_def.h, line 11 (as a struct)
2 include/linux/slub_def.h, line 82 (as a struct)
여기서 한 가지 의문이 더 생깁니다.
그러면 어느 해더 파일에 선언된 구조체가 정말 kmem_cache 일까요?
두 번째 줄 해더 파일에 선언된 구조체를 라즈비안 기준으로 쓰고 있습니다. 참고로 라즈비안을 포함한 대부분 리눅스 배포판에서 ‘슬럽 할당자’를 동적 메모리 할당자로 적용하고 있습니다.
다음은 슬랩/슬럽/슬롭 할당자 코드를 지정한 컨피그에 따라 컴파일하는 규칙을 정한 메이크파일입니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/mm/Makefile]
...
1 obj-$(CONFIG_SLOB) += slob.o
2 obj-$(CONFIG_MMU_NOTIFIER) += mmu_notifier.o
3 obj-$(CONFIG_KSM) += ksm.o
4 obj-$(CONFIG_PAGE_POISONING) += page_poison.o
5 obj-$(CONFIG_SLAB) += slab.o
6 obj-$(CONFIG_SLUB) += slub.o
라즈비안에서는 CONFIG_SLUB 컨피그가 켜져 있으니 커널 소스 트리 기준으로 linux/mm/slub.c 파일을 컴파일합니다.
다음 그림을 보면서 슬랩과 슬럽 할당자의 개념을 정리해볼까요?

[그림 14.25] 커널 메모리 할당자의 종류
슬랩(SLAB)이란 2008년까지 커널 메모리에서 기본 할당자로 사용했던 할당자입니다. 하지만 슬랩 오브젝트를 관리하기 위한 메타 데이터의 사이즈가 커서 많은 양의 메모리를 허비하는 단점이 있습니다.
슬럽(SLUB)은 2009년도부터 컴퓨터나 모바일과 같은 시스템에서 기본으로 사용하고 있는 할당자입니다. 슬럽 할당자는 슬랩의 메타 데이터로 인한 메모리 낭비를 개선하기 위해 고안됐습니다. 그렇다면 슬럽 할당자는 메타 데이터를 어떻게 관리할까요?
페이지 디스크립터인 struct page 구조체 필드를 활용해 슬랩 오브젝트를 관리합니다.
라즈비안도 슬럽을 사용해 커널 동적 메모리 관리를 합니다.
슬롭(SLOB)은 지정한 사이즈 내 객체의 메모리 할당을 모두 처리하는 메모리 할당자입니다. 메모리를 할당할 때 속도는 가장 느리나 메타 데이터 정보를 거의 사용하지 않으므로 메모리 소모가 제일 적습니다.
다음 소절부터는 슬럽을 기준으로 커널 동적 메모리 할당에 대해서 설명을 하겠습니다.
"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!"
Thanks,
Austin Kim(austindh.kim@gmail.com)
Reference(커널 메모리 소개)
가상 주소를 물리 주소로 어떻게 변환할까?
메모리 존(Zone)에 대해서
커널 메모리 할당은 어떻게 할까
슬랩 메모리 할당자와 kmalloc 슬랩 캐시 분석
커널 메모리 디버깅
최근 덧글