Chapter 12 - Memory Management
Memory allocation inside the kernel is not as easy as outside the kernel
커널 내부의 메모리 할당 방법 소개
Pages
- 메모리 관리의 기본단위
- page size는 architecture dependent + 여러 종류의 size 제공
- 모든 physical page들은 page 구조체로 관리
- 실제로 이것보다 더 많은 parameter가 존재하지만, 페이지 관리에 필수적인 인자들만 모아보면 다음과 같다.
/include/linux/mm_types.h
two word block size -> for atomic operating
struct page {
unsigned long flags; /* status of the page */
atomic_t _count; /* reference count */
atomic_t _mapcount; /* mapping count */
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; /* virtual address */
};
Zones
비슷한 일을 하는 page들을 구역별로 나눠 놓은 것
Memory addressing의 한계 두 가지 >> Zone이 생긴 이유
- device의 DMA(Direct Memory Access)를 허용해야 함
- Virtual memory보다 Physical memory를 크게 잡아주는 경우가 생길 수 있음
일반적으로 다음과 같이 zone을 나누지만, 구역 설정은 architecture dependent하다.
ZONE_DMA
,ZONE_DMA32
- DMA가 가능한 구역 (0~16M)ZONE_NORMAL
- default zone. 가장 일반적으로 할당받는 구역 (16M~896M)- 시스템에서 지원하는 물리메모리가 가상주소에 1:1로 mapping되어 사용할 수 있는 영역.
ZONE_HIGHMEM
- high memory 구역. (896M~)- physical memory가 커널로의 virtual-physical의 1:1 mapping을 허용하는 구간을 초과하는 경우이다.
- 이 영역에서는 virtual-physical의 1:1 mapping이 되어있지 않고 physical memory만 할당되어있는 상태이다.
ZONE_NORMAL
이 처리하지 못하는 경우 모두 이 영역에 할당된다.- 64bit 시스템에서는 모든 physical memory가 1:1 mapping이 가능하므로
ZONE_HIGHMEM
을 사용하지 않는다. (ZONE_DMA
와ZONE_NORMAL
로만 구성)
zone이라는 구조체를 통해 관리한다.
/include/linux/mmzone.h struct zone { unsigned long watermark[NR_WMARK]; unsigned long lowmem_reserve[MAX_NR_ZONES]; struct per_cpu_pageset pageset[NR_CPUS]; spinlock_t lock; /*protect struct from concurrent access*/ struct free_area free_area[MAX_ORDER]; spinlock_t lru_lock; struct zone_lru { struct list_head list; unsigned long nr_saved_scan; } lru[NR_LRU_LISTS]; struct zone_reclaim_stat reclaim_stat; unsigned long pages_scanned; unsigned long flags; atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; int prev_priority; unsigned int inactive_ratio; wait_queue_head_t *wait_table; unsigned long wait_table_hash_nr_entries; unsigned long wait_table_bits; struct pglist_data *zone_pgdat; unsigned long zone_start_pfn; unsigned long spanned_pages; unsigned long present_pages; const char *name; /*name of zone*/ };
watermark는 메모리 소비에 따른 할당의 한계치를 표시한 것으로 다음과 같은 할당/회수관계를 갖는다.
Getting Pages
- 커널이 페이지 단위로 할당을 받고 싶을 때의 함수가 구현되어있다.
/include/linux/gfp.h
struct page* alloc_pages(gfp_t gfp_mask, unsigned int order) {
return alloc_pages_current(gfp_mask, order);
}
#define alloc_pages(gfp_mask, order) \
alloc_pages_node(numa_node_id(), gfp_mask, order);
- NUMA 시스템일 경우 메모리 정책에 따라 노드를 선택하고 buddy 시스템을 통해 2^order 만큼의 연속된 페이지를 할당받는다
- NUMA 시스템이 아닐 경우 현재 노드에서 buddy 시스템을 통해 2^order 만큼의 연속된 페이지를 할당받는다.
page_address()
를 통해 물리페이지가 가상메모리 어느 부분과 mapping되어있는지를 확인할 수 있다. 해당 페이지와 mapping된 가상메모리의 주소를 반환해준다.__get_free_pages(gfp_t gfp_mask, unsigned int order)
는 alloc_pages와 같으나 HIGHMEM이 아닌 구역의 page를 할당해주고 할당 후 mapping된 가상메모리의 시작주소를 반환해준다./mm/page_alloc.c unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) { struct page *page; VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); page = alloc_pages(gfp_mask, order); if(!page) return 0; return (unsigned long) page_address(page); }
만약 페이지 하나만 할당받고 싶을 때는
alloc_page()
,__get_free_page()
를 이용한다. oreder이 0으로 세팅되어있다.get_zeroed_pages()
는__get_free_pages()
를 한 후 모든 bit을 0으로 clear해준다.할당받은 페이지만 free해야 한다. 그렇지 않을 경우 치명적인 오동작을 일으킬 수 있다.
__free_pages()
,free_pages()
,free_page()
함수를 이용한다.
kmalloc()
- 페이지보다 작은 단위(예를 들어, byte단위)로 메모리를 할당받고 싶을 때 사용한다.
/include/linux/slab.h
static __always_inline void *kmalloc(size_t size, gfp_t flags)
할당받은 공간의 제일 낮은 주소값을 반환한다.
유저공간의 메모리요청방식인
malloc()
과 유사하지만, flag를 설정할 수 있다는 점에서 다르다.flag는 크게 세 가지 카테고리로 나누어져 있다.
action modifier - allocator이 어떤 특성을 가지고 동작할지를 설정할 수 있다.
zone modifier - 어느 구역의 공간을 할당해 줄 지를 설정할 수 있다.
types - action modifier와 zone modifier의 여러 flag들을 혼합해놓은 flag이다. 주로 여러 기능이 포함되어있는 이 flag를 사용한다.
- GFP_KERNEL이 가장 기본적인 flag이다.
- GFP_ATOMIC은 요청받은 할당을 atomic하게 수행해야 하기 때문에 sleep할 수 없다. 하지만 이는 메모리 부족시 swap을 위한 sleep도 막기 때문에 할당을 실패하기 쉽다.
kmalloc()
을 이용해 메모리를 할당받은 경우만kfree()
를 사용한다.
vmalloc()
kmalloc()
은 연속된 메모리공간을 할당해주지만, 현실적으로 연속된 공간을 찾는 것이 쉽지 않다.vmalloc()
은 가상메모리는 연속적이지만 물리메모리는 연속적이지 않아도 되는 할당요청이다.- 즉, 가상메모리공간의 16M-64M까지를 물리메모리와 mapping시키려 할 때, 물리메모리공간에서는 72M-120M처럼 연속적으로 할당할 필요 없이 메모리들의 파편들을 모아서 할당해줄 수 있다는 것이다.
- 하지만 필연적으로 page table을 만들어야한다는 성능상의 문제점 때문에 주로
kmalloc()
을 사용한다. vfree()
로 할당을 해제할 수 있다.
Slab Layer
- 자주 사용하는 메모리 크기를 미리 할당받아놓고 해당 크기의 요청이 들어오면 slab allocator이 미리 할당받아놓은 공간을 지급해주는 방식.
- free list가 가진 한계를 해결해 준 방식이다.
__alloc_pages_node()
를 통해 새로운 slab을 만든다
/mm/slab.c
kmem_getpages() >> __alloc_pages_node()
static struct page *kmem_getpages(struct kmem_cache *cachep, gfp_t flags,
int nodeid) {
struct page *page;
int nr_pages;
flags |= cachep->allocflags;
if(cachep->flags & SLAB_RECLAIM_ACCOUNT)
flags |= __GFP_RECLAIMABLE;
page = __alloc_pages_node(nodeid, flags | __GFP_NOTRACK, cachep->gfproder);
if(!page) {
slab_out_of_memory(cachep, flags, nodeid);
return NULL;
}
....(후략)
}
kmem_cache_create()
를 이용해 cache를 생성하고kmem_cache_destroy()
를 이용해 제거한다.
Statically Allocating on the Stack
기존의 커널스택 할당 >> 연속된 두 페이지
컴퓨터의 동작시간이 길어질수록 '연속된' 두 페이지를 제공해주기가 매우 힘들어진다.
따라서 single-page kernel stack을 제공하는 옵션이 생김
- 연속된 페이지를 제공해야하는 어려움을 해결한 대신 커널스택의 여유가 줄어듦
- interrupt handler는 독자적인 interrupt stack을 만들어서 사용
High Memory Mappings
High memory구역에 있는 메모리들은 커널의 virtual(logical) address와 매핑이 되어있지 않은 상태
kmap()
을 이용해 실제적으로 해제하기 전까지 영구적 mapping이 가능/include/linux/highmem.h -> mm/highmem.c kmap()->page_address() void *page_address(const struct page *page) { unsigned long flags; void *ret; struct page_address_slot *pas; if(!PageHighMem(page)) return lowmem_page_address(page); pas = page_slot(page); ret = NULL; spin_lock_irqsave(&pas->lock, flags); if(!list_empty(&pas->lh)) { struct page_address_map *pam; list_for_each_entry(pam, &pas->lh, list) { if(pam->page == page) { ret = pam->virtual; goto done; } } } done: spin_unlock_irqrestore(&pas->lock, flags); return ret; }
- high memory, low memory 모두에 사용 가능
- low memory에 사용할 경우 이미 mapping이 되어있는 상태이므로 mapping된 virtual address를 반환
- high memory에 사용할 경우 virtual-physical mapping이 생성된 후 mapping된 virtual address를 반환
kunmap()
을 이용해 mapping을 해제할 수 있다.
kmap_atomic()
을 이용해 영구적 mapping이 아닌 일시적 mapping도 가능하다/include/linux/highmem.h static inline void *kmap_atomic(struct page *page) { preempt_disable(); pagefault_disable(); /* can't sleep through preemption or swap */ return page_address(page); } static inline void *kmap(struct page *page) { might_sleep(); return page_address(page); }
- interrupt handler같은 sleep하지 않는 상황에서 사용한다.
- 오직 한 page의 virtual-physical mapping만 잡고있을 수 있다.
kunmap_atomic()
으로 mapping을 해제 할 수 있으나, 다음 temporary mapping이 들어오면 자동으로 기존 mapping이 해제되므로 잘 사용하지 않는다.
Per-CPU Allocations
SMP를 지원하는 OS는 per-CPU data를 이용한다.
per-CPU data는 각각의 core 자신만 볼 수 있는 data이다
- 4-core >> 4개의 per-CPU data
배열의 형식으로 저장된다.
core가 본인의 per-CPU data를 보는 데는 lock이 필요하지 않다. 각 core의 독자적인 data이기 때문에 다른 core에서 본인의 per-CPU data가 아닌 per-CPU data를 볼 수 없다.
- 2.6 이후 버전에서는 새로운 percpu interface를 소개한다
- 이 interface에서는 다른 core의 per-CPU data를 볼 수 있지만, 이 경우에는 concurrency 문제가 생길 수 있으므로 locking이 필요하다.
kernel preemption은 per-CPU data 사용에 있어서 2가지 문제를 발생시킨다.
- cpu0에서 per-CPU data를 이용한 작업을 하고 있던 task A가 reschedule되어 cpu1에서 재실행되는 경우
- cpu0에 preemption이 일어나 task B 또한 per-CPU data를 이용한 작업을 해 task A가 이용하던 data를 건드리는 경우
-- 이를 해결하기 위해 per-CPU data를 사용하는 경우, 즉
get_cpu_var()
함수를 부르게 되면preempt_disable()
이 일어나 kernel preemption을 막아버린다.
--put_cpu_var()
를 이용해 per-CPU data 사용을 끝낼 때preempt_enable()
해준다.compile-time에서는 다음 두 함수를 이용해 per-CPU data를 선언한다.
/include/linux/percpu-defs.h #define DEFINE_PER_CPU(type, name) \ DEFINE_PER_CPU_SECTION(type, name, "") #define DECLARE_PER_CPU(type, name) \ DECLARE_PER_CPU_SECTION(type, name, "") /* for avoid compile warning */