리눅스 - 메모리 관리
메모리 관리
1. 페이지
커널은 물리적 페이지를 메모리 관리의 기본 단위로 사용한다.
메모리 관리 장치(MMU)는 페이지 단위로 처리한다. 이는 사용자 레벨에서만이 아니라 커널 역시 동일하기 때문에 MMU는 페이지 크기의 정밀도를 가진 시스템 페이지 테이블을 관리한다.
페이지 크기는 아키텍쳐별로 다르다. 하지만 대부분 32bit 아키텍쳐의 페이지 크기는 4KB이며 64bit 아키텍처의 페이지 크기는 8KB이다. 커널에서는 이러한 물리적 페이지를 표현하기 위해 struct page 구조체를 사용한다. 이 구조체는 “include/linux/mm_types.h”에 명시되어있다.
struct page 구조체를 시스템 전체의 물리적 페이지를 표현하며 이 구조체를 이용하여 모든 페이지를 관리한다. 시스템의 물리 메모리가 4GB고 물리 페이지 크기를 8KB라고 할 때 전체 구조체의 크기는 20MB이다. 전체 비율을 생각해보면 그렇게 큰 건 아닌셈이다.
1) 구역
하드웨어적인 문제로 인해 커널은 모든 페이지를 동일하게 취급할 수 없다.
이는 아래와 같은 하드웨어 문제 때문이다.
- 일부 하드웨어 장치는 특정 실제 메모리 주소로만 DMA를 수행 할 수 있다.
- 일부 아키텍처에서는 가상적으로 접근 할 수 있는 것보다 더 많은 양의 메모리를 물리적으로 접근할 수 있다. 따라서 일부 메모리는 커널 주소 공간에 상주할 수 없다.
위 두 가지 문제로 인해 총 네 가지의 주요 메모리 구역을 두고 있다.
- ZONE_DMA : DMA를 수행 할 수 있는 페이지가 있다.
- ZONE_DMA32 : DMA를 수행 할 수 있지만 32bit 장치만 접근 할 수 있다.
- ZONE_NORMAL : 통상적으로 할당 되는 페이지가 있다.
- ZONE_HIGHMEN : 커널 주소 공간에 상주하지 않는 페이지인 상위 메모리가 있다.
아키텍쳐마다 위의 구역의 사용방식과 배치는 다를 수 있다. 실제 X86-32 아키텍쳐의 경우 아래와 같이 구역과 영역이 정해져있다.
구역 | 설명 | 물리적 메모리 주소 |
ZONE_DMA | DMA 가능한 페이지 | <16MB |
ZONE_NORMAL | 일반적으로 접근 가능한 페이지 | 16~896MB |
ZONE_HIGHMEN | 동적으로 연결되는 페이지 | >896MB |
X86-32 에는 ZONE_HIGHMEM 구역이 없듯이 모든 아키텍처가 모든 구역을 정의하진 않는다. 위에 구역을 나눠놓은 이유대로 DMA가 필요하면 ZONE_DMA에서, 일반적인 경우 어지간하면 커널은 ZONE_NORMAL에서 페이지를 할당하려고 한다. 물론 메모리가 부족해지면 커널은 ZONE_DMA고 뭐고 다 갖다가 쓴다.
2) 페이지 가져오기
커널은 메모리 획득을 위한 저수준 방법 하나와 할당 받은 메모리에 접근하는 몇가지 인터페이스를 제공한다. 이 방법은 “include/linux/gfp.h”에 정의되어있다.
이름 | 설명 |
alloc_page(gfp_mask) | 페이지 하나를 할당하고, 할당된 page 구조체 포인터를 반환한다. |
alloc_pages(gfp_mask, order) | 2 개수의 페이지를 페이지를 할당하고, 할당된 첫번째 페이지의 page 구조체 포인터를 반환 |
__get_free_page(gfp_mask) | 페이지 하나를 할당하고, 할당된 페이지의 논리적 주소 포인터를 반환한다. |
__get_free_pages(gfp_mask, order) | 2 개수의 페이지를 할당하고, 할당된 첫 번째 페이지의 논리적 주소 포인터를 반환한다. |
get_zeroed_page(gfp_mask) | 페이지 하나를 할당하고, 페이지 내용을 0으로 초기화 한 다음, 페이지의 논리적 주소 포인터를 반환한다. </table> ### 3) 페이지 반환 할당하는 함수가 있듯이 반환하는 함수 역시 있다. 이 함수를 사용하는데는 깊은 주의가 필요한데, 주소나 order 값을 잘못 지정하면 메모리가 깨질 수 있다. ```c void __free_pages(struct page *page, unsigned int order) void free_pages(unsigned long addr, unsigned int order) void free_page(unsigned long addr) ``` ※ gfp_mask 동작 지정자, 구역 지정자, 형식 세 가지로 분류 할 수 있는데, 이는 "include/linux/gfp_types.h"에 정의되어있다. 해당 flag에 권장 사용법은 "Documentation/core-api/memory-allocation.rst"에 명시되어있으며, 플래그에 대한 설명은 "include/linux/gfp.h"에 주석으로 적혀있다. 동작 지정자는 할당 작업의 중단, 실패시 어떻게 처리할지 메모리 할당시에 별도의 어떤 처리를 할지 명시적으로 전달한다. 구역 지정자는 ZONE_DMA, ZONE_DMA32, ZONE_HIGHMEN 등 해당 구역에서만 할당해야할때 지정한다. 형식 지정자은 동작 지정자와 구역 지정자를 특정 형식 작업을 하는데 적절한 것을 지정해둔것이다. 이 형식 지정자는 동작 지정자와 구역 지정자의 OR 형식으로 지정된다. ## 2. 메모리 할당 페이지 할당과 반환 함수의 경우 물리적으로 연속된 페이지 단위의 메모리가 필요한 경우, 특히 한 두개 정도가 필요할때 유용하지만 바이트 단위의 메모리 할당을 위한 함수 역시 지원한다. ### 1) 할당 함수 #### a. kmalloc() 이 함수는 "include/linux/slab.h"에 정의된 함수로 아래와 같다. ```c static __always_inline __alloc_size(1) void *kmalloc(size_t size, gfp_t flags) ``` flag 값이 포함되었다는 것을 제외한다면 malloc 함수와 비슷하다. 할당에 성공할 경우 할당된 메모리 주소를 반환하고, 실패할 경우 null을 반환한다. #### b. vmalloc() "include/linux/vmalloc.h"에 선언되어있는 함수이다. ```c extern void *vmalloc(unsigned long size) __alloc_size(1); ``` 가상적으로 연속된 메모리 영역을 할당하는것이다. 이는 사용자 공간 메모리 할당 함수가 작동하는 방식으로 실제 RAM에서도 연속적이라는 보장은 없다. ### 2) 해제 함수 #### a. kfree() kmalloc과 쌍으로 이루는 것으로 "include/linux/slab.h"에 정의된 함수이다. ```c void kfree(const void *objp); ``` 인터럽트 핸들러에서도 사용가능하다. #### b. vfree() vmalloc과 쌍으로 이루는 것으로 "include/linux/vmalloc.h"에 정의된 함수이다. ```c extern void vfree(const void *addr); ``` vmalloc으로 할당한 addr 포인터가 가르키는 메모리를 해제하며 휴면상태로 전환될 수 있기 때문에 인터럽트 컨텍스트에서는 사용할 수 없다. ## 3. 슬랩 계층 자료구조를 할당하고 해제하는 작업은 커널 내부에서 매우 빈번하게 일어난다. 할당과 해제를 하게 되면 시간이 많이 걸리므로 그냥 다 쓴 자료구조를 특정 리스트에 넣어놓고 필요할 경우 다시 꺼내서 쓰는 식으로 운용하는데 이를 해제 리스트라고 한다. 이러한 해제리스트를 사용하는데 가장 큰 문제점은 전체적인 제어 방법이 없다는 것인데, 시스템의 메모리가 부족해졌을때 커널이 해제 리스트의 캐시 크기를 줄여서 메모리를 확보할 방법이 없다는 것이다. 어떤 해제리스트를 선택해서 없애야 효율적인지 알수 없다는 뜻이다. 이를 위해 리눅스는 슬랩계층(Slab Layer)라는 것을 지원한다. 슬랩 계층은 아래와 같은 구조를 따른다. ![img.png](/assets/blog/linux/memory_manage/img.png) 슬랩 계층은 유형 별로 객체를 저장하는 캐시(Cache)에 분류한다. 객체 유형 별로 캐시가 존재하며, 캐시는 슬랩을 나눌 수 있다. 일반적으로 슬랩은 페이지 하나로 되어있는 경우가 많으며, 캐시 한개는 다수의 슬랩을 가질 수 있다. 각 슬랩에는 캐시할 자료구조에 해당하는 객체가 여러개 들어가며 이 객체의 사용 여부에 따라 슬랩의 상태는 모두 사용, 부분 사용, 미사용 중 하나가 된다. 슬랩에 속한 객체가 모두 사용중이라면 모두 사용, 일부만 사용중이면 부분 사용, 아무것도 사용되지 않고 있다면 미사용으로 설정되는 식이며 사용되는 슬랩의 순서는 "부분 사용", "미사용", "새로 슬랩을 만듬" 순이 된다. (모두 사용 슬랩은 당연히 사용될 수 없다.) 슬랩 계층의 가장 최상위 부분은 kmem_cache 구조체를 사용해 표현되며 이 구조체는 "mm/slab.c"에 정의되어있다. ## 4. 스택에서 정적으로 할당 사용자 공간에서와 달리 커널은 고정된 작은 크기의 스택을 사용할 수 있다. 각 프로세스별로 고정된 작은 크기의 스택을 가지고 있다. 이는 아키텍처와 컴파일 시점의 설정에 따라서 달라지지만 전통적으로 프로세스별로 두 페이지의 커널 스택을 가지고 있으며, 페이지 크기가 4KB인 32비트 아키텍처의 경우에는 커널 스택 크기가 8KB, 페이지 크기가 8KB인 64비트 아키텍처의 경우 16KB가 된다. 두 페이지가 아닌 한 페이지만 할당 할 수도 있는데 이런 경우 인터럽트 핸들러가 들어갈 자리가 없다. 이런 경우를 위해 인터럽트 스택이라는 기능이 있는데 프로세서 별로 존재하는 한 페이지짜리 스택으로 인터럽트 핸들러에만 사용하는 스택이다. ## 5. CPU 별 할당 대칭형 다중 프로세서(Symmetric Multi-Processor)를 지원하는 현대 운영체제는 CPU 별로 데이터를 저장할 수 있다. 이는 배열에 저장되며 각 항목이 각 프로세스에 대응되는 방식이다. get_cpu() 함수를 통해 프로세서 번호를 얻고 커널 선점을 비활성시킨다음 프로세서 번호로 배열의 데이터에 엑세스해서 필요한 작업을 해서 다시 커널 선점을 활성화 시키는 방식으로 일을 처리할 수ㅍ있다. 이를 위한 함수는 "include/linux/percpu.h"에 모두 정의되어있으며 실제 구현 내용은 "mm/slab.c"와 "arch/{대상 아키텍처}/include/asm /percpu.h"에 구현되어있다. # 참고문헌 - 리눅스 커널 심층분석 (에이콘 임베디드 시스템프로그래밍 시리즈 33, 로버트 러브 저자(글) · 황정동 번역) - [리눅스 커널 6.6.7 버전](https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.6.7.tar.gz) |