본문 바로가기

Programming

Linux kernel slab(SLUB) memory allocator

리눅스의 메모리 Allocator 는 SLAB 이라고 부르는데 이것은 일반적인 메모리 할당자의 이름이고

2.6 커널 이후부터는 SLUB 가 디폴트 메모리 할당자로 쓰인다.  이는 커널컴파일 옵션을 어떻게 주느냐에 따라서(make menuconfig)

각종 메모리 Allocator 알고리즘을 선택하여 사용할 수 있는데, 여기서는 디폴트 옵션들만 생각한다.


일단 커널을 컴파일하여 QEMU 에 올릴 준비를 하자(다른 문서 참조)

준비가 끝났으면 이제 커널 소스를 분석하면서 재컴파일하고 결과를 확인해보는 작업을 반복한다.


참고로 커널컴파일이 1~2시간가량 소요되지만 특정 C 파일만 수정했을때는 해당 부분만 재컴파일하여

링킹만 하기때문에 2~3분이면 재컴파일이 완료된다.  그러나 주의할것은 헤더파일을 수정하게 되면 그것이

포함되는 모든 오브젝트 파일들이 재컴파일 되기때문에 사실상 전체커널이 재컴파일 된다.


아무튼 특정 오브젝트 파일만 재컴파일할때는 아래처럼 간단히 끝난다.



이제 소스코드 분석을 시작하기에 앞서 SLAB 메모리 할당자에 대해서 다시 공부를 좀 하자(다른 문서 참조)

기본적인 SLAB 알고리즘에 따르면 SLAB 자체는 페이지 경계에 할당이 되며, SLAB 내부에 메모리 객체(task_struct, inode 등) 이

배열처럼 연속적으로 할당 될 것이다.  그러나 SLAB 들 자체는 kmem_cache_alloc 에 의해 동적으로 할당 되기 때문에 하나의 커널객체가

2개이상의 SLAB 에 나뉘어 할당된 경우 메모리상에 연속적으로 존재할 수 없다.


일단 이 점이 실제로 커널상에서 적용되고 있는지를 확인해보기 위해서 task_struct 를 순회하면서 그 시작포인터의

값을 출력해주는 루틴을 돌려보고싶다.  그러나 LKM 을 올리기 어려운 바닐라 커널이므로 커널 자체에 해당작업을 수행하는

코드를 집어넣어서 테스트 하기로했다.  그 방법으로는 제일 만만한 mkdir 시스템콜의 function body 를 아래처럼 이용했다.

mkdir 은 fs/namei.c 에 매크로로 정의되어있다.



위와같이 mkdir 시스템콜을 수정하고 커널을 재컴파일하여 task_struct 자료구조들의 시작주소들을 찍어봤다.



확인해보면 전부 특정한 base 주소 + proc/slabinfo 상에서 확인한 task_struct 의 크기 (0xcd0) * N 인 것을 알 수 있다.

스크린샷에서 swapper 부터 kworker/0:1 까지 c7430000, c7480000, c7530000, 의 세가지 베이스주소가 사용된것을 보면 SLAB 을 3개를

이용했고, SLAB 한개당 9개의 task_struct 가 들어가는 것을 볼 수 있다.


또한 해당 커널가상주소가 커널 가상주소 메모리레이아웃상의 어디에 들어가는지 확인해보면 아래처럼 low_mem 영역에 들어간다.




참고로 커널소스를 뒤지다가 SLAB 에 대한 메타데이터 자료구조가 아래처럼 선언된 것을 확인하였다



여기서 s_mem 이 실제 SLAB 공간의 시작주소이고 list_head 를 통해 모든 슬랩들의 메타데이터가 연결리스트로

이어져 있다고 추측했다.  nodeid 의 경우 NUMA 관련 필드인것 같았고, free 멤버가 슬랩상의 빈 공간에 대한 정보를

가지고 있는 것으로 추정됬다.  SLAB 상에 메모리를 확보하고 그 주소를 리턴할때는 s_mem 멤버가 어떻게든 참조될것 같아서

이 필드명으로 grep 을 하고 추적해보니 아래의 함수를 찾을 수 있었다.



그리고 SLAB 할당관련된 이름을 가진 함수들을 추적하다가 위의 함수를 최종적으로 호출하게 되는 상위 함수를 찾을 수 있었는데...

함수명은 kmem_cache_alloc_node 였다.



이 함수 에서부터 따라들어가다보면 slab.c 에 예상대로  SLAB 베이스 + item size * N 형태의 주소지정을 하는 코드가 나온다.

그러나 이부분이 SLAB 상에 아이템을 할당할때 사용될줄 알고 테스트를 해봤는데 컴파일이 되지 않는 소스부분이었다.

확인해보니 slab_common.c 는 언제나 컴파일이되고 slab.c/slub.c/slob.c 셋중에 하나가 커널 컴파일 옵션에 따라

선택적으로 컴파일 되는것 같았다.  커널소스를 거슬러 올라가고 삽질에 삽질을 거쳐 slab.h 상의 아래 inline 함수가

동일한 이름으로 또 존재했었고, 이것이 컴파일되서 사용되고 있는것을 확인했다.



이 함수는 struct kmem_cache 구조체의 포인터를 받는데, 이것이 확인해보면 아래처럼 SLAB 에 들어갈 아이템에 대한 메타데이터 라고 추정할 수 있다.



kmem_cache_alloc_node 함수는 SLAB 을 이용하는 어떠한 메모리객체를 할당할때도 전부 호출이 되기때문에

여기서 struct kmem_cache 구조체의 name 멤버를 통해 원하는 자료구조를 필터링하고 그에따라 메모리 Allocation 알고리즘을

다르게 적용해주면 되겠다 싶었다.  그런데 SLAB 할당할 준비를 모두 마치고 마지막에 다른 Allocator 를 쓰는것보다 차라리 해당 객체를

할당하려는 상단에서 애당초 SLAB Allocator 로 진입하지 않고 따로 메모리할당을 하는게 더 나을것 같았다.


그래서 task_struct 를 최초할당할만한 부분을 뒤지다가 init 태스크를 생성하고 fork 를 구현하는 부분을 찾았다.


이부분은 사실 fork 를 구현하기위한 초기작업들을 수행하는 부분인데 task_struct 에 대한 메타데이터도 여기서 생성해놓는다.

이부분은 start_kernel 함수에서부터 각종 초기화 함수들과 함께 호출되게 된다.




소스를 뒤지고 테스트해보다가 실제로 fork 시스템콜이 구혈될때 task_struct 를 새로 할당하는 함수는 아래의 함수임을 발견했다.



여기서 alloc_task_struct_node 함수부터 쭉 따라가다보면 kmem_cache_alloc 으로 들어가고 내부적으로 SLAB 알고리즘을 구현하는

함수들이 호출되고 마지막에 page 자료구조를 만드는 함수들도 호출된다.


큰 그림으로 볼때 task_struct 를 생성하기 위해 필요한 일들은


1. task_struct 관련 kmem_cache 구조체 생성

2. SLAB 메타데이터 구조체 생성

3. SLAB 공간 확보

4. SLAB 공간에 대한 page 구조체 생성

5. SLAB 공간상의 빈 공간에 task_struct 할당.


이런 정도를 알 수 있었고 내부적으로는 훨씬 더 복잡하다.


아무튼 결과적으로 dup_task_struct 함수에서 alloc_task_struct_node 함수가 task_struct 할당을 위한 거의 최상단의 high level 함수로 보였다

이 함수를 호출하는 대신 아래와 같이 커널 low-mem에 메모리 할당을 할수있는 allocator 를 따로 작성하고 대체시켰다.

low-mem 을 이용하기 위한 방법으로는 제일 단순하게 그냥 커널 전역변수 공간을 이용했다.



이렇게 메모리 할당자를 초단순하게 만들어놓고 아래의 alloc_task_struct_node 함수에서 kmem_cache_alloc_node 대신

내가 만든 메모리 할당자를 호출하도록 수정했다.



최종적으로 커널을 재컴파일하여 다시 task_struct 들이 어떻게 할당되었는지 확인하니 아래처럼 linear mapping 영역에

하나의 Base 주소에 대해서 연속적으로 할당 되는 것을 확인하였다. (size of task_struct : 3276 byte. 계산해보면 딱맞음) 




참고로 slab 들은 아래의 kmem_list3 구조체를 통해 전부 연결리스트로 연결되어있다.



task_struct 에 대해서 수행한 것과 마찬가지로 inode 도 비슷하게 아래처럼 상위 할당함수를 찾을 수 있었다.



참고로 page 구조체는 아래와 같다.

하나의 physical memory page 마다 page 자료구조가 존재하고 virtual address 에 대한 매핑정보를 가진다.




또한가지 참고로 highmem 영역을 매핑할때는 kmap, kunmap 을 사용해서 일시적으로 물리메모리를 가상주소랑 매핑시켜야 접근이 가능하다.

정리하자면

1. low-mem : page-offset 의 차이로 가상/물리 주소변환가능. 물리적으로 연속된공간에 메모리 할당됨

2. high-mem : 페이징으로 addressing 해야함, 페이지가 불연속적으로 할당될 수 있음.


커널 가상주소 공간상에서 어디까지를 low-mem 으로 매핑하는지는 확실치 않지만 page-offset 상위 초반주소를 확실히 선형매핑된다고 보면될듯...  정확히 확인하려면 page table 을 찍어보면됨.  하지만 여기선 어차피 고정적인 크기의 kernel data section 을 메모리 할당공간으로 사용했으므로 100% 선형매핑된다고 봐도될듯.