- 프로세서가 메모리에 접근할 때 가장 작은 단위는 byte 또는 word지만, MMU와 커널은 메모리 관리를 페이지 단위로 처리한다.
- 페이지 크기는 아키텍처 별로 다르며 보통 32-bit 시스템에선 4KB, 64-bit 시스템에선 8KB다.
- 커널은 하나의 페이지를 여러 구역(zone)으로 나눠 관리한다. (
<linux/mmzone.h>
에 정의)- ZONE_DMA: DMA를 수행할 수 있는 메모리 구역
- ZONE_DMA32: 32-bit 장치들만 DMA를 수행할 수 있는 메모리 구역
- ZONE_NORMAL: 통상적인 페이지가 할당되는 메모리 구역
- ZONE_HIGHMEM: 커널 주소 공간에 포함되지 않는 ‘상위 메모리’ 구역
- 메모리 구역의 실제 사용 방식과 배치는 아키텍처에 따라 다르며 없는 구역도 있다.
-
커널은 메모리 할당을 위한 저수준 방법 1개 + 할당받은 메모리에 접근하는 몇 가지 인터페이스를 제공한다.
-
모두
<linux/gfp.h>
파일에 정의돼있으며 기본 단위는 ‘페이지’다.// 방법 1 - alloc_pages struct page* alloc_pages(gfp_t gfp_mask, unsigned int order); // 방법 2 - __get_free_pages unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); // 방법 3 - get_zeroed_page unsigned long get_zeroed_page(unsigned int gfp_mask, unsigned int order);
-
위 세 방법 중 하나를 선택해서 원하는 크기(2 * order 페이지)만큼 메모리를 할당받을 수 있다.
-
차이점은 반환값이다. 첫 번째 함수는 page 구조체를 얻을 수 있고, 두 번째 함수는 할당받은 첫 번째 페이지의 논리적 주소를 반환하며 마지막 함수는 0으로 초기화된 페이지를 얻을 수 있다.
-
특히, 커널 영역에서 메모리를 할당하는 것은 실패할 가능성이 있으며 반드시 오류를 검사하는 과정이 필요하다. 또한, 오류가 발생했다면 원위치 시켜야 하는 과정도 필요하다.
-
할당받은 페이지는 반드시 반환해야 하며, 할당받은 페이지만 반환해야 한다.
-
페이지 반환에는
void __free_pages (struct page *page, unsigned int order);
를 사용한다. -
C의
malloc()
과free()
함수의 관계와 주의점을 생각하면 된다.
kmalloc()
은gfp_mask
플래그라는 추가 인자가 있다는 점을 제외하면 C의malloc()
과 비슷하게 동작한다.- 즉, 바이트 단위로 메모리를 할당할 때 사용하며 커널에서도 메모리를 할당할 때 대부분 이 함수를 사용한다.
gfp_mask
플래그는 동작 지정자, 구역 지정자로 나뉘며 둘을 조합한 형식 플래그도 제공된다.- 커널에서 자주 사용되는 대표적인 플래그는 아래와 같다.
- GFP_KERNEL: 중단 가능한 프로세스 컨텍스트에서 사용하는 일반적인 메모리 할당 플래그다.
- GFP_ATOMIC: 중단 불가능한 softirq, 태스크릿, 인터럽트 컨텍스트에서 사용하는 메모리 할당 플래그다.
- GFP_DMA: ZONE_DMA 구역에서 할당 작업을 처리해야 할 경우 사용한다.
- 메모리를 해제할 때는
<linux/slab.h>
에 정의된kfree()
함수를 사용한다. vmalloc()
함수는 할당된 메모리가 물리적으로 연속됨을 보장하지 않는다는 것을 제외하면kmalloc()
과 동일하게 동작한다.- 물리적으로 연속되지 않은 메모리를 연속된 가상 주소 공간으로 만들기 위해 상당량의 페이지 테이블을 조정하는 부가 작업이 필요하므로
kmalloc()
의 성능이 훨씬 좋다. - 큰 영역의 메모리를 할당하는 경우에만
vmalloc()
을 사용하자. - 메모리를 해제할 때는
vfree()
를 사용한다.
- 물리적으로 연속되지 않은 메모리를 연속된 가상 주소 공간으로 만들기 위해 상당량의 페이지 테이블을 조정하는 부가 작업이 필요하므로
- 사용이 빈번한 자료구조는 사용할 때마다 메모리를 할당하고 초기화하고 사용한 뒤 메모리를 반환하는 것보다 풀(pool) 방식을 사용하는 것이 성능면에서 효율적이다.
- 슬랩 계층은 자료구조를 위한 캐시 계층이며 사용 종료 시 메모리를 반환하지 않고 해제 리스트에 넣어두고 다음 번에 재활용한다.
- 슬랩의 크기는 페이지 1개 크기와 같고, 1개 슬랩에는 캐시할 자료구조 객체가 여러개 들어간다.
- 빈번하게 할당 및 해제되는 자료구조일수록 슬랩을 이용해서 관리하는 것이 합리적이다.
- 사용자 공간은 동적으로 확장되는 커다란 스택 공간을 사용할 수 있지만, 커널은 고정된 작은 크기의 스택을 사용한다.
- 커널 스택은 컴파일 시점의 옵션에 따라 하나 또는 두 개의 페이지(4KB ~ 16KB)로 구성된다.
- 인터럽트 핸들러는 커널 스택을 사용하지 않고 프로세서별로 존재하는 1-page 짜리 스택을 사용한다.
- 특정 함수의 지역변수 크기는 1KB 이하로 유지하는 것이 좋다. 스택 오버플로우는 조용히 발생하며 확실하게 문제를 일으키며 가장 먼저 thread_info 구조체가 먹혀버린 뒤 모든 종류의 커널 데이터가 오염될 여지가 있다.
- 따라서 대량의 메모리를 사용할 때는 앞서 살펴본 동적 할당을 사용해야 한다.
- 1번 항목에서 설명했듯, 상위 메모리에 있는 페이지는 커널 주소 공간에 포함되지 않을 수도 있다. (대부분의 64-bit 시스템은 포함된다.)
alloc_pages()
함수를 호출해서 얻은 페이지에 가상주소가 없을 수 있으므로, 페이지를 할당한 다음에 커널 주소 공간에 수동으로 연결하는 작업이 필요하다.- 이를 위해
<linux/highmem.h>
파일에kmap(struct page* page);
함수를 사용한다.- 페이지가 하위 메모리에 속해 있다면, 그냥 페이지의 가상주소를 반환한다.
- 페이지가 상위 메모리에 속해 있다면, 메모리를 맵핑한 뒤 그 주소를 반환한다.
- 프로세스 컨텍스트에서만 동작한다.
-
SMP를 지원하는 2.6 커널에는 특정 프로세서 고유 데이터인 CPU별 데이터를 생성하고 관리하는 percpu라는 새로운 인터페이스를 도입했다.
-
<linux/percpu.h>
헤더파일과<mm/slab.c>
,<asm/percpu.c>
파일에 정의돼있다.// 컴파일 타임의 CPU별 데이터 DEFINE_PER_CPU(var, name); // type형 변수 var 생성 get_cpu_var(var)++; // 현재 프로세서의 var 증감 // 여기부터 선점이 비활성화 된다. put_cpu_var(var); // 다시 선점 활성화 // 런타임의 CPU별 데이터 void *alloc_percpu(size_t size, size_t align); void free_percpu(const void *);
-
CPU별 데이터를 사용하면 세 가지 장점이 있다.
- 락(스핀락, 세마포어)을 사용할 필요가 줄어든다.
- 캐시 무효화(invalidation) 위험을 줄여준다.
- 선점 자동 비활성화-활성화로 인터럽트 컨텍스트 & 프로세스 컨텍스트에서 안전하게 사용할 수 있다.
-
리눅스는 캐시를 ‘페이지 캐시(Page cache)’라고 부르며 디스크 접근 횟수를 최소화 하기 위해 사용한다.
- 프로세스가
read()
시스템콜 등으로 읽기 요청을 할 때, 커널은 가장 먼저 페이지 캐시를 확인한다. - 만약 있다면, 메모리 또는 디스크 접근을 하지 않고 캐시에서 데이터를 바로 읽는다.
- 만약 없다면, 메모리 또는 디스크에 접근해 읽은 뒤 데이터를 캐시에 채워 넣는다.
- 프로세스가
-
리눅스는 write policy로 지연 기록(write-back)을 채택하고 있다.
- 프로세스의 쓰기 동작은 캐시에 바로 적용된다. (메모리 or 디스크에 적용 X)
- 해당 캐시 라인에 dirty 표시를 한다.
- 적당한 때에 주기적으로 캐시의 dirty 표시된 내용이 메모리 or 디스크에 갱신되고 지워진다.
- 통합해서 한꺼번에 처리하므로 성능이 우수하지만 복잡도가 높다.
-
캐시의 갱신된 페이지 내용을 메모리 or 디스크로 반영하는 작업을 ‘플러시(Flush)’라고 한다.
- 리눅스는 이 작업을 ‘플러시 스레드(Flush thread)’라는 커널 스레드가 담당한다.
- 페이지 캐시 가용 메모리가 특정 임계치 이하로 내려갈 때 dirty 캐시 라인을 플러시 한다.
- 페이지 케시 dirty 상태가 특정 한계 시간을 지나면 플러시 한다.
- 사용자가
sync()
,fsync()
시스템콜을 호출하면 즉시 플러시 한다.
-
리눅스는 replacement policy로 ‘이중 리스트 전략’(Two-list) 라는 개량 LRU(Least Recently Used) 알고리즘을 사용한다.
- 페이지 캐시가 가득찼을 때 어떤 데이터를 제거할 것인지 선택하는 과정이다.
- 언제 각 페이지에 접근했는지 타임스탬프를 기록해둔 뒤 가장 오래된 페이지를 교체하는 방법이다.
- 이중 리스트 전략은 ‘활성 리스트’와 ‘비활성 리스트’ 두 가지 리스트를 활용한다.
- 최근에 접근한 캐시 라인은 활성 리스트에 들어가서 교체 대상에서 제외한다.
- 두 리스트는 큐처럼 앞부분에서 제거하고 끝부분에 추가한다.
- 두 리스트는 균형 상태를 유지한다. 활성 리스트가 커지면 앞쪽 항목들을 비활성 리스트로 넘긴다.
- 다양한 형태의 파일과 객체를 올바르게 캐시하는 것을 목표로
<linux/fs.h>
에address_space
객체가 만들어졌다. - 하나의
address_space
객체는 하나의 파일(inode)을 나타내고 1개 이상의vm_area_struct
가 포함된다. 단일 파일이 메모리상에서 여러 개의 가상 주소를 가질 수 있다는 걸 생각하면 된다. - 특히, 페이지 캐시는 원하는 페이지를 빨리 찾을 수 있어야 하기 때문에
address_space
에는page_tree
라는 이름의 기수 트리(radix tree)가 들어 있다.
참고