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_DMAZONE_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가지 문제를 발생시킨다.

    1. cpu0에서 per-CPU data를 이용한 작업을 하고 있던 task A가 reschedule되어 cpu1에서 재실행되는 경우
    2. 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 */
    

results matching ""

    No results matching ""