리눅스커널의메모리관리
지난 호에 우리는 커널 모듈 프로그래밍에 관한 기본적인 것들을 살펴보았다. 이번 호
에서는 지난 번 모듈 프로그래밍에 이어 커널에서 가장 흔히 사용하는 함수 중의 하나
인 kmalloc을 통해 커널에서 시스템의 메모리를 어떻게 관리하는지 알아보기로 한다.
글 _ 김민찬 KLDP 멤버, 전문 프로그래머
연재 순서
① 커널 프로그래밍 환경 구축하기와 특징
② 모듈 구현하기
③ 리눅스 커널의 메모리 관리
④ 커널의 동기화에 관하여
⑤ 커널의 시간관리 및 지연 함수에 대하여
⑥ 파일시스템과 proc file system 사용하기
메모리 관련 코드는 파일 시스템 코드와도 연관
리눅스 커널의 메모리 서브시스템을 이해하기는 쉬운 일이 아니다. 여러 서브시스템 중에서도 메모리는더욱 그러하다. 그런 이유는 리눅스 커널이 MMU를 사용해 물리 메모리와 가상 메모리를 구분하여 쓰고 있으며 demand paging을 사용하기때문이다. demand paging이란 아래 그림과같이 페이지를 요청했을 경우 바로 할당해주는 것이 아니라 할당받은 페이지를 실제로 사용하게 될 때(그림 상에 진한색의 코드가 실행되었을 경우) 하드웨어(Memory Management Unit)에서 발생시키는 예외를처리하여 그 때 메모리를 프로세스의 페이지 테이블에 매핑시켜주는 기술이다.
메모리를 더욱 이해하기 복잡하게 만드는 이유는 리눅스는 파일을 메모리에 매핑시켜 일반 파일이나 디바이스 파일을 마치 메모리에 접근하는 것처럼 다룰 수 있기 때문이다. 이 때문에 메모리 관련 코드는 파일 시스템의 코드와도 상당 부분 연관이 되어있다. 그렇기 때문에 짧은 지면에 모든메모리 관련 기능들을 설명을 마친다는 것은 무리가 있다. 그러므로 이번 강
좌에서는 리눅스 커널의 메모리 관리에 있어 핵심적인 부분들만 살펴보기로 한다.
1. Kernel Memory Allocator
리눅스 커널에서 가장 흔히 볼 수 있는 함수로써 시스템의 연속된 메모리를 동적으로 할당받는 함수는kmalloc 함수이다. 우리가 일반적으로 유저 모드 프로세스에서 dynamic memory를 할당받기 위해 사용하는 malloc함수의 커널 버전이라고 생각하면 된다. 하지만 메모리를 할당하기 위해 동작하는 방식은 너무나 틀리다. 어디까지나 kmalloc은 커널의 메모리를 할당하는 함수이며 이것을 사용하기 위해서 여러분들이 알고 있어야 할 것들이 꽤 있다. 이 함수는 malloc과 같이 아무곳에서나 사용할 수 있는 것은 아니다.
우선, kmalloc의 인터페이스는 다음과 같다.
static inline void *kmalloc(size_t size, gfp_t flags)
여러분의 예상대로 첫번째 인자는 할당받으려는 메모리의 크기이다. kmalloc으로부터 할당받은 메모리는물리적으로 연속된 공간에 존재한다. 물리적으로 연속되었다는 사실에 대하여 다소 생소하게 받아들일독자가 있을 것 같아 부연하면 다음과 같다. 일반적으로 유저 모드에서 다뤄지는 모든 주소는가상 주소이다. 가상 주소는 MMU를 통하여 물리 메모리에연결되게 되어 있다. 이것은 소프트웨어의 노력도 많이 들어가지만 근본적인 구조는 MMU라는 하드웨어에서 제공하는 기능이다. 이 하드웨어를 소프트웨어가 잘support 하여 그렇게 동작하는 것이니 궁금한 독자는 x86 데이터시트를 천천히 살펴보라. 이 MMU의 기능과 Page Fault의 기능을 사용하면 실행 중에 사용자가 요청한 메모리를 시스템의 메모리 공간중 연속되어있지 않은 공간에 메모리 영역들로 모아서 짜깁기하여 할당할 수 있게 된다. 이러한 기능은 메모리의 외부단편화 때문에 사용하게 된다. 이 문제에 대해서는 뒤에서 다시 설명하기로 한다. 아래 그림과 같이 이렇게 시스템 메모리가 물리적으로는 연속되어 있지 않지만 MMU 테이블에 연속된가상 주소의 공간에 매핑시켜 놓는다면 유저 모드 프로세스는 연속된 공간으로만 바라보게 될 것이다.아래 그림은 시스템의 페이징 모델을 간단하게 나타낸 그림이다. 실제 페이징의 구현은 3단계나 4단계 레벨로 더욱 복잡하게 되어 있다.
사실 유저 모드 프로그램을 개발하는 사람들은 가상 주소나 물리 주소를 신경 쓸 필요도 없게 되어 있다.하지만 커널은 다르다. 커널 또한 가상주소로 동작하긴 하지만 커널은 시스템의 코어이기 때문에 연속되지 않은 물리메모리를 마치 연속된 것처럼 사용하게 된다면 그 후처리를 위한비용이 크기 때문에 그렇게 다루는 경우는 드물다.(단, vmalloc을 사용할 경우를 제외하고) 또한 kmalloc의 또 다른 특징은 여러분이 요청한 크기보다 더 큰 메모리를 할당하게 된다.(사실 이 부분은여러분이 malloc을 호출했을 때도 마찬가지이긴 하다. malloc의 경우는 상황에 따라 brk나 mmap을 통하여 요청한 크기보다 큰 메모리를 확보하게 된다. 이는 glibc에서 일어나므로 응용 개발자가 신경쓸 일은 아니다). 그런 이유는 커널의 메모리 관리자는 기본적으로 페이지 단위로 메모리를 관리하기 때문이다. 그렇다면 4byte를 요청했는데 무식하게4K(일반적인 시스템의 페이지크기)를 반환하게 될까? 그렇지는 않다.그 이유는 다음 장의 Slab Allocator 장에서 얘기하기로 한다.
두번째 인자는 할당받으려는 메모리의 속성을 나타낸다. 이 플래그는 아래와 같이 3개의 카테고리로 나누어 진다. 중요한 플래그들 몇 가지만을 살펴보기로 하자.
이 할당플래그들은 다음과 같이 or연산으로 묶어 사용할 수도 있다.
ptr = kmalloc(size, _GFP_WAIT | _GFP_IO | _GFP_FS);
위의 인자는 커널의 페이지 할당자에게 이 할당연산은 block 할 수 있으며, I/O 및 filesystem operation을수행할 수 있다는 것을 의미한다. 이렇게 하는 것은 시스템의 사용 가능한 물리 메모리가 모자랄 때 커널로 하여금 메모리를 확보할 수 있는 최대의 자유도를 주는 것이다. 커널은 물리 메모리가 모자랄 경우 익명 페이지들(anonymous page)을 스왑아웃시키거나 페이지 캐시를 해지하고 슬랩 캐시에 사용되는 슬랩 오브젝트들을 줄이는 페이지 회수 알고리즘을 동작시켜 최대한 메모리를확보하려고 한다. 이런 과정에서 커널은 블록될 수 도있고 I/O나 파일시스템 관련 operation들을 호출할수 있게되는 것이다.
이 플래그를 이용해서 커널은 메모리를 획득하기 위한 zone을 결정한다. zone에 대해서는 Buddy Allocator 장에서 자세히 다루도록 하며 여기서는 그냥 그런 것이 있다는 것만 알고 넘어가자. _GFP_DMA 플래그를 명시하는 것은 커널로 하여금 DMA 장치가 인식할 수 있는 물리 메모리 를확보하기 위함이다. 이것이 필요한 이유는 예전 ISA버스에 부착된 DMA 장치들은 RAM의 첫 16M만을 어드레싱 할 수 있었기 때문에 메모리 관리 측면에 있어서 따로 관리를 해주어야 했다. 마찬가지로_GFP_HIGHMEM은 시스템이 어드레싱 할수 없는 물리메모리를 따로 관리하기 위한 zone이다. 일반적인32bit machine에서 물리 메모리 896M까지만 linear address에 속하게 되며 그 이상부터는 high memory로 관리되게 된다. 이는 32bit machine이 표현할 수 있는 주소는 전체 4G이지만 4G 중 3G이상의영역부터 시작해서 1G만이 커널에게 할당된 영역이기 때문이다. 1G - 896M로 남은 공간은 커널이 highmemory나 vmalloc 등의 매핑을 위해 사용하게 된다.
GFP_ATOMIC과 같은 경우는 함수가 블록되면 안되는 상황, interrupt handler, software interrupt(구 bottom half)와 같은 곳에서 주로 사용된다. GFP_NOIO와 GFP_NOFS는 현재 메모리가 모자라 회수를 해야 되는 상황인데 함수를 호출한 context가 block I/O code라던가 파일시스템 코드 일 경우 사용되게 된다. 예를 들면, block I/O의 코드 중에
buffer_head를 할당하는 함수가 있다. 그 경우, 커널이 buffer_head를 할당하려다가 메모리가 모자라다는것을 알아 차렸다면 페이지 회수 알고리즘이 동작하기 시작하게 된다.
이때 메모리를 회수하기 위해 page cache에 있는 페이지들중 dirty 페이지를 하드디스크에 sync시켜 버리고 물리 메모리에서 제거하면 그 메모리는 free 메모리로 사용할 수 있게된다. 페이지를 sync하기 위해서 즉 하드디스크에 write하기위해서는 I/O를 수행하여야 하며 그러기 위해선 또 다시 buffer_head를 할당해야만 하는 웃지 못할 일이 발생한다. 이런 문제들을 방지하기 위하여 있는 플래그이다.
2. Slab Allocator
리눅스 커널의 물리 메모리 관리는 페이지 단위로 이루어진다. 그렇다면 예를 들어 19byte를 요청한다고해서 시스템의 페이지 단위인 4K byte가 할당될까? 그렇지는 않다. 예를 들어 kmalloc을 사용하여 19 byte를 요청하게 되면 32byte가 할당된다. 그렇다면 kmalloc은 어떻게 32byte를 할당하게 할까? 이것이 이번 장에서 얘기할 슬랩 할당자 덕분이다.
커널이 관리하는 페이지 단위는 커널에서 사용하기에 다소 큰단위이다. 4K가 응용 프로그램을 개발하는분들은 쉽게 생각할지 모르겠지만 커널의 입장에서 많은 함수들이 4K씩 사용한다는 것은 시스템의 치명적인 성능저하를 유발할 수도 있으며 메모리 단편화를 너무 많이 일으켜 금방 시스템의 성능저하로 이어지게 될 것이다. 그러므로 커널 입장에서는 몇 byte요청을 4K로 되돌려 준다는 것은 용납할 수 없는 일이된다.
예를 들어19byte를 요청한 입장에서 4K가 할당되었다는 것은 나머지 4077 byte는 사용자가 free하지 않는한 시스템의 입장에서는 사용하지 못하는 영역이 되버리고 말기 때문이다.
이를 우리는 멋있는 말로 내부 단편화라고 한다. 내부 단편화문제를 해결하기 위하여 슬랩 할당자가 나오게 되었다.
슬랩은 내부 단편화 문제를 해결할 뿐만이 아니라 커널 내에서 흔히 일어나는 dynamic memory 할당의overhead를 줄이기 위하여 캐싱하는 역할을 하여 성능 개선에도 큰 도움을 주고 있다. 슬랩 할당자의 기본 구조는 다음과 같다. 하나의 캐시를 나타내는 kmem_cache_s 구조체는 kmem_list3 구조체를 통하여slab들을 관리하며 슬랩 내에는 사용자에게 할당할 object들이 저장되어 있는 풀 구조이다.
캐시는 관리가 필요한 오브젝트 종류별로(예를 들면task_struct, file, buffer_head, inode 등) 작성되고 그오브젝트의 슬랩을 관리한다. 슬랩은 하나 이상의 연속된 물리 페이지로 구성되어 있으며 일반적으로 하나의 페이지로 구성된다. 캐시는 이러한 슬랩들의 복수개로 구성된다.
캐시에는 다음과 같은 3가지의 슬랩 리스트가 있다.
● slab_full : 슬랩내의 오브젝트가 전부 사용 중인 것
● slab_partial : 슬랩내의 오브젝트가 사용중인 것과 미사용중인 것이 혼재되어 있는 것
● slabs_empy : 슬랩내의 오브젝트가 전부 미사용인 슬랩 리스트
위와 같은 구조를 통하여 자주 사용되는 오브젝트들을 미리 할당하여 놓고 사용자 요구가 있을 때 마다 바로 반환하는 것이다. 이것은 물리 메모리를 확보하기 위하여 검색 및 회수, 반환과 같은 긴 여행을 떠날 필요가 없으므로 시스템의 성능을 향상시킨다. 또한 다 사용된 오브젝트들을 반납받아 커널의 메모리 할당자에게 반환하지 않고 보관했다가 재사용하기 때
문에 시스템의 성능을 향상시킬 수 있게 된다.
그렇다면 kmalloc이 어떻게 슬랩 할당자를 사용할까? 그것은 커널이 시스템을 초기화 될 때kmem_cache_init 함수를 통하여 자주 사용되는 커널의 오브젝트들의 크기를 고려하여 일반적으로 사용할 목적으로 추가적인 캐시들을 생성하기 때문이다. 그 크기는 32, 64, 128, 256, 512, 1,024, 2,048, 4,096,8,192, 16,384, 32,768, 65,536, 131,072 byte이다.
이는 곧 kmalloc이 할당 할 수 있는 메모리 크기는 128K라는 것을 의미하기도 한다. 커널은 위의 크기의object로 이루어진 캐시를 미리 할당하여 메모리에 유지하고 있으며 사용자의 요구가 들어왔을 경우 위의크기 중 가장 가깝게 큰 수로 올림하여 할당하게 되므로 페이지 단위로 관리하는 것 보다 내부 단편화를 줄이게 되며 시스템의 성능을 향상시키게 된다.
3. Buddy Allocator
버디 할당자는 슬랩 할당자의 밑에서 실제로 물리 메모리를 확보하는 계층이다. 커널 메모리 관리에 있어제일 하단에 위치한 계층이기도 하다. 슬랩 할당자가 내부 단편화를 줄이기 위하여 사용될 수 있다면 버디 할당자는 외부 단편화를 줄이기 위하여 사용한다고 말할 수 있다. 외부 단편화란 다음과 같다. 예를 들어 시스템 전체에 물리 메모리가 많이 남아 있지만 사용자가 연속된 물리 메모리 16K(4개의 연속된 페이지) 를 요청하였을 때 연속된 공간으로 16K는 할당할 수 없는 경우이다.
리눅스 커널의 버디 할당자를 설명하기 위해서는 먼저 page구조체와 zone 구조체, pglist_data구조체를설명하지 않을 수 없다.
page 구조체
물리 메모리는 물리 페이지(페이지 프레임)단위로 관리된다. page 구조체는 시스템의 물리 메모리의4K와 1:1로 대응된다. 즉 Page 구조체는 시스템의 사용 가능한 물리 메모리를 페이지 크기로 나눈 수만큼 시스템에 존재하게 된다. 그러므로 page 구조체의 크기를 최대한 줄이려고 노력하여 현재는 32bit machine의 경우 32byte로 되어 있다. 페이지를 구조체
는 다양한 필드를 가지고 있지만 버디 할당자를 설명하기 위해서 가장 중요한 필드는 다음과 같다.
page 구제체의 flag 필드 또한 많은 상태를 표시하고 있지만 그 중 버디 할당자와 관련해 중요한 필드만을나열한다면 다음과 같다.
zone 구조체
하드웨어의 제약으로 인하여 커널은 모든 페이지들을 동일하게 관리할 수 없다. 메모리에 물리 주소로 인하여 몇몇 페이지들은 특정한 일을 위해 사용할 수 없게 되는 경우가 있기 때문이다. 이러한 제약 때문에 커널은 페이지들을 zone으로 나누어서 관리한다. 커널은 같은 특성의 페이지들을 모아 하나의 zone을 만든다. 리눅스가 갖는 하드웨어 제약이란 Zone
Modifier절에서 언급했던 DMA 영역이나 시스템이 어드레싱 할 수 없을 만큼 큰 물리 메모리 주소를 의미한다.
페이지는 주소의 범위에 따라 3개의 영역으로 나뉘어 진다.
메모리 zone의 실제 사용과 구성은 각 아키텍쳐마다 다르지만 일반적인 x86에서는 16M까지가ZONE_DMA, 16~896M까지를 ZONE_NORMAL, 그 이상을 ZONE_HIGHMEM으로 분류한다. 또한 커널2.6.14 이후부터는 ZONE_DMA32 영역이 추가되어 4G 미만의 DMA 가능한 영역으로 x86_64 아키텍쳐에서 사용되고 있다.
zone 구조체에 있어 중요한 필드들은 다음과 같다.
이중 free_area는 버디할당자에 있어 가장 중요한 역할을 하는 자료구조로써 아래의 그림처럼 배열의 인덱스를 지수로 하는 크기만큼의 페이지를 관리한다.
free_area의 페이지들은 page구조체의 lru 필드를 사용하여 연결되며 지수가 1 이상인 경우 즉 4K 이상인경우에는 연속된 페이지들의 가장 선두에 있는 페이지의 lru를 이용하여 관리하게 된다.
버디 시스템의 동작과정은 다음과 같다. 예를 들어 사용자가 지수 3의 페이지, 즉 8개의 연속된 페이지를요청하게 될 경우, 그리고 이때 커널의 버디 시스템에는 free_area[3]에 어떤free 페이지도 연결되어 있지 않다고 가정하자. 그럴 경우 지수 4의 페이지가 반으로 쪼개지며 그 중 하나가 free_area[3]에 속하게 되고. free_area[3]에 새로 속하게 된 페이지는 다시두개로 나뉘어져 그 중 첫번째 것을 반환하게 되는 것이다. 사용하고 난 메모리를 해지하는 것은 거꾸로이다. 현재 할당받은 8개의 연속된 페이지를 반환하게 되면 free_area[3]으로 반환하게 되며 이때 자신의 반쪽이었던 친구(buddy) free 페이지인가 여전히 free 페이지라면 하나로 합쳐져free_area[4]의 위치의 에서 다시 친구를 찾게 된다. 그 친구가 free라면 이와같은 과정을 반복하고 그렇지 않을 경우 멈추게 되는 것이다.
이렇게 메모리를 관리하게 되면 가능한한 연속된 공간의 메모리를 확보할 수 있어 외부 단편화를 줄일 수있는 반면, 메모리의 할당, 해지 시간을 예측할 수 없게 되서 사실 정확한 마감시간을 보장해야 하는 실시간 응용들에게는 적합하지 않은 모델이다. 그러므로 실시간 응용들은 이러한 문제를 해결하기 위하여 미리 메모리 풀을 만들어 사용하는 경우도 흔하다.
우리는 이번 강좌에서 리눅스의 메모리 관리에 대하여 가장 기본적이고 핵심적인 내용들만을 살펴보았다. 이는 메모리 서브시스템의 자료 구조 중 극히 일부에 관한 것이며, kmalloc과 관련된 내용만을 설명한 것이다. 서두에서 얘기하였듯이 리눅스 커널의 메모리 관리는 상당히 복잡하여 모든 것을 다 이해하려고 하는 것은 사실 어려움이 많겠지만 위의 내용들에
대해서만도 정확하게 이해하고 있다면 여러분들의 커널 프로그래밍은 상당히 윤택해질 것이리라 생각한다. 또한 리눅스의 메모리 관리 부분을 구석구석 이해하는 것도 재밌는 도전이라 생각한다. 그렇게 되면여러분은 곧 파일 시스템이라는 또 다른 커다란 산과 맞닥뜨리게 될 것이다
출처 : 공개 SW 리포트 9호 페이지 58 ~ 63 발췌(2007년 10월) - 한국소프트웨어 진흥원 공개SW사업팀 발간
'L I N U X' 카테고리의 다른 글
Step by Step 커널 프로그래밍 강좌⑥ (0) | 2012.02.08 |
---|---|
Step by Step 커널 프로그래밍 강좌⑤ (0) | 2012.02.08 |
Step by Step 커널 프로그래밍 강좌④ (0) | 2012.02.08 |
Step by Step 커널 프로그래밍 강좌② (0) | 2012.02.08 |
Step By Step 커널 프로그래밍 강좌 ① 커널프로그래밍의환 (0) | 2012.02.08 |