Vmap

<kernel v5.0>

Vmap

커널에서는 단편화 문제를 유발하는 high order 페이지 할당의 사용을 매우 꺼려한다. 따라서 사이즈가 큰 페이지 할당이 필요하지만 빈번한 할당/해제를 하지 않는 경우에 한해 여러 개의 싱글(order 0) 페이지들을 사용하여 연속된 가상 주소 공간에 모아 매핑하는 방법을 사용한다. 이러한 매핑 방법을 vmap(Virtually contiguous memory area mapping)이라고 한다.

 

vmap을 사용하여 다음 2가지의 주소 공간에 메모리를 할당하는 api는 다음과 같다.

  • vmalloc()
    • 요청 size 만큼의 페이지를 할당하여 vmalloc address space 공간에 매핑
  • module_alloc()
    • 요청 size 만큼의 페이지를 할당하여 module address space 공간에 매핑

 

VM 영역 관리

VMALLOC 가상 주소 공간의 빈 공간 검색을 위해 RB 트리 및 리스트 자료 구조를 동시에 사용한다.

  • RB 트리
  • 리스트

 

다음 그림은 vmalloc 가상 주소 공간내에서 VM 영역들이 RB 트리 및 리스트에서 관리되고 있는 모습을 보여준다.

 

Lazy TLB Flushing(Free)

  • vunmap()을 수행하면 다음과 같은 처리항목이 수행되어야 하는데 즉각 처리되는 항목과 나중에 모아 처리할 항목을 분류한다.
    • 즉각 처리
      • 페이지 테이블에서 매핑 해제
      • 캐시 flush
    • 지연 처리
      • RB tree 및 리스트에서 지연된 vmap_area의 제거
      • 모든 cpu의 TLB flush
        • cpu가 많은 시스템에서 arm64의 경우 inner 영역의 cpu들에 대해 일괄적으로 TLB flush를 수행하게 할 수 있어 arm32보다는 조금 더 낳은 요건이 있다. 이에 비해 arm32 시스템의 경우 IPI(Inter Process Interrupt) 콜을 사용하여 각 cpu로 인터럽트를 발생시킨 후 처리하게 하여 더욱 처리 성능을 떨어뜨리는 요인이 된다.
  • 삭제할 vma를 처리하지 않고 놔두었다가 일정량을 초과하거나 메모리 부족 시 한꺼번에 purge 처리하여 처리 성능을 높인다.
    • 이러한  해지를 유보하는 방법을 사용하여 vmap_area의 관리를 약 20배 이상 빠르게 처리를 하는 구현을 적용하였다.
  • lazy TLB free 상태를 표현하는 플래그 비트는 다음과 같다.
    • VM_LAZY_FREE (0x1)
      • 삭제 요청된 상태
    • VM_LAZY_FREEING (0x2)
      • purge 진행중인 상태
    • VM_VM_AREA (0x4)
      • 할당 상태

 

다음 그림은 VM 영역이 vunmap()에 의해 Lazy TLB 플러시 상태로 변화하고, 그 후 purge될 때 실제 TLB 플러시 처리되는 과정을 보여준다.

 

관련 API

  • vmap()
  • vunmap()

 

대체 API

vmap() 대체 api로 per-cpu map 기반의 api들이 소개되었다. 이 API는 아직 많은 드라이버에 적용되어 사용하지는 않고 일부 드라이버들 에서만 사용하고 있다.

  • vm_map_ram()
  • vm_unmap_ram()

 


vmap 매핑

vmap() 함수를 사용하기 위해서 매핑에 사용할 싱글(order 0) 페이지 단위의 물리 메모리 정보들을 페이지 디스크립터 배열로 구성하여 요청한다. 그러면 vmalloc 가상 주소 공간의 빈 자리를 찾아 페이지들을 주어진 매핑 속성으로 매핑하고 매핑된 가상 시작 주소를 반환한다.

  • 보다 빠른 할당/해제를 위해 캐시 노드와 몇 개의 변수들을 사용한다.
    • free_vmap_cache
      • 빈 공간에 대한 검색을 빠르게 하기 위해 가장 최근에 등록하여 사용한 vmap_area 또는 가장 최근에 free 한 vmap_area의 이전(prev) vmap_area가 보관된다.
    • cached_hole_size
      • 캐시의 바로 앞 hole의 크기를 기억한다.
      • 이 값이 0이면 캐시를 사용하지 않고 처음부터 검색을 수행한다.
    • cached_vstart
      • 캐시된 시작 가상 주소
    • cached_align
      • 캐시된 align 값
  • per-cpu에서 사용하는 변수
    • vmap_area_pcpu_hole
      • per-cpu의 할당 시작 주소로 vmalloc 공간의 끝 부터 시작한다. (초기값은 VMALLOC_END)
      • vmalloc()이 vmalloc 공간의 처음 주소부터 할당하지만, per-cpu는 그 반대이다.

 

다음 그림은 vmap() 함수에 대해 연관 함수들과의 처리 흐름을 보여준다.

 

vmap()

mm/vmalloc.c

/**
 *      vmap  -  map an array of pages into virtually contiguous space
 *      @pages:         array of page pointers
 *      @count:         number of pages to map
 *      @flags:         vm_area->flags
 *      @prot:          page protection for the mapping
 *
 *      Maps @count pages from @pages into contiguous kernel virtual
 *      space.
 */
void *vmap(struct page **pages, unsigned int count,
                unsigned long flags, pgprot_t prot)
{
        struct vm_struct *area;
        unsigned long size;             /* In bytes */

        might_sleep();

        if (count > totalram_pages())
                return NULL;

        size = (unsigned long)count << PAGE_SHIFT;
        area = get_vm_area_caller(size, flags, __builtin_return_address(0));
        if (!area)
                return NULL;

        if (map_vm_area(area, prot, pages)) {
                vunmap(area->addr);
                return NULL;
        }

        return area->addr;
}
EXPORT_SYMBOL(vmap);

연속된 가상 주소 공간에 요청한 물리 페이지들을 매핑하고 매핑한 가상 주소를 반환한다. 실패 시 null을 반환한다.

  • 코드 라인 7에서 CONFIG_PREEMPT_VOLUNTARY 커널 옵션을 사용하는 경우 preempt point로 긴급히 리스케쥴링 요청한 태스크가 있는 경우 sleep 한다.
  • 코드 라인 9~10에서 전체 메모리 페이지보다 더 많은 페이지를 요구하는 경우 처리를 포기하고 null을 반환한다.
  • 코드 라인 12~15에서 VM 할당을 위해, vmap_area 및 vm_struct 정보를 구성한다. 실패하는 경우 null을 반환한다.
  • 코드 라인 17~20에서 vm_struct 정보로 페이지들의 매핑을 시도하고 실패한 경우 해제 후 null을 반환한다.
  • 코드 라인 22에서 매핑한 가상 주소 공간의 시작 주소를 반환한다.

 

다음 그림은 요청한 물리 페이지들에 대해 VMALLOC 가상 주소 공간에서 빈 공간을 찾아 매핑을 하는 모습을 보여준다.

 

VM(가상 주소 영역)  할당

get_vm_area_caller()

mm/vmalloc.c

struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,
                                const void *caller)
{
        return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
                                  NUMA_NO_NODE, GFP_KERNEL, caller);
}

요청한 size(페이지 단위로 정렬된 byte 단위)로 VMALLOC 가상 주소 공간에서 빈 공간을 찾아 VM(vm_area 및 vm_struct) 정보를 구성해온다.

 

__get_vm_area_node()

mm/vmalloc.c

static struct vm_struct *__get_vm_area_node(unsigned long size,
                unsigned long align, unsigned long flags, unsigned long start,
                unsigned long end, int node, gfp_t gfp_mask, const void *caller)
{
        struct vmap_area *va;
        struct vm_struct *area;

        BUG_ON(in_interrupt());
        size = PAGE_ALIGN(size);
        if (unlikely(!size))
                return NULL;

        if (flags & VM_IOREMAP)
                align = 1ul << clamp_t(int, get_count_order_long(size),
                                       PAGE_SHIFT, IOREMAP_MAX_ORDER);

        area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
        if (unlikely(!area))
                return NULL;

        if (!(flags & VM_NO_GUARD))
                size += PAGE_SIZE;

        va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
        if (IS_ERR(va)) {
                kfree(area);
                return NULL;
        }

        setup_vmalloc_vm(area, va, flags, caller);

        return area;
}

@start ~ @end 가상 주소 공간에서 @align 조건의 @size 만큼의 빈 영역을 찾아 vm을 할당 구성한 후 반환한다.

  • 코드 라인 9~11에서 페이지 단위로 사이즈를 정렬한다.
  • 코드 라인 13~15에서 ioremap이 요청된 경우 사이즈를 order 단위로 변환한 align 값을 사용한다. 단 ioremap 최대 order 페이지 수 만큼으로 제한한다.
    • IOREMAP_MAX_ORDER
      • ARM32 pmd 단위(16M) 비트 수 => 24
      • ARM64 4K 페이지를 사용하는 경우 pud 단위(1G) 비트 수 => 30
      • ARM64 16K 페이지를 사용하는 경우 pmd 단위(32M) 비트 수 => 25
      • ARM64, 64K 페이지를 사용하는 경우 pmd 단위 (512M) 비트 수 => 29
  • 코드 라인 17~19에서 vm_struct를 할당한다.
  • 코드 라인 21~22에서 no guard 요청이 없으면 가드 페이지를 위해 1페이지를 추가한다.
  • 코드 라인 24~28에서 요청한 가상 주소 범위에서 빈 매핑 공간을 찾아 vmap_area를 할당 구성하고 RB트리 및 리스트에 insert한 후 엔트리 정보를 반환한다.
  • 코드 라인 30에서 vm_struct & vm_area 구조체를 구성한다.
  • 코드 라인 32에서 구성한 vm_struct 구조체 포인터를 반환한다.

 

공간 검색 후 vmap_area 할당

alloc_vmap_area()

mm/vmalloc.c -1/2-

/*
 * Allocate a region of KVA of the specified size and alignment, within the
 * vstart and vend.
 */
static struct vmap_area *alloc_vmap_area(unsigned long size,
                                unsigned long align,
                                unsigned long vstart, unsigned long vend,
                                int node, gfp_t gfp_mask)
{
        struct vmap_area *va;
        struct rb_node *n;
        unsigned long addr;
        int purged = 0;
        struct vmap_area *first;

        BUG_ON(!size);
        BUG_ON(offset_in_page(size));
        BUG_ON(!is_power_of_2(align));

        might_sleep();

        va = kmalloc_node(sizeof(struct vmap_area),
                        gfp_mask & GFP_RECLAIM_MASK, node);
        if (unlikely(!va))
                return ERR_PTR(-ENOMEM);

        /*
         * Only scan the relevant parts containing pointers to other objects
         * to avoid false negatives.
         */
        kmemleak_scan_area(&va->rb_node, SIZE_MAX, gfp_mask & GFP_RECLAIM_MASK);

retry:
        spin_lock(&vmap_area_lock);
        /*
         * Invalidate cache if we have more permissive parameters.
         * cached_hole_size notes the largest hole noticed _below_
         * the vmap_area cached in free_vmap_cache: if size fits
         * into that hole, we want to scan from vstart to reuse
         * the hole instead of allocating above free_vmap_cache.
         * Note that __free_vmap_area may update free_vmap_cache
         * without updating cached_hole_size or cached_align.
         */
        if (!free_vmap_cache ||
                        size < cached_hole_size ||
                        vstart < cached_vstart ||
                        align < cached_align) {
nocache:
                cached_hole_size = 0;
                free_vmap_cache = NULL;
        }
        /* record if we encounter less permissive parameters */
        cached_vstart = vstart;
        cached_align = align;

        /* find starting point for our search */
        if (free_vmap_cache) {
                first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
                addr = ALIGN(first->va_end, align);
                if (addr < vstart)
                        goto nocache;
                if (addr + size < addr)
                        goto overflow;

        } else {
                addr = ALIGN(vstart, align);
                if (addr + size < addr)
                        goto overflow;

                n = vmap_area_root.rb_node;
                first = NULL;

                while (n) {
                        struct vmap_area *tmp;
                        tmp = rb_entry(n, struct vmap_area, rb_node);
                        if (tmp->va_end >= addr) {
                                first = tmp;
                                if (tmp->va_start <= addr)
                                        break;
                                n = n->rb_left;
                        } else
                                n = n->rb_right;
                }

                if (!first)
                        goto found;
        }

요청한 가상 주소 범위에서 빈 공간을 찾아 va(vmap_area)를 할당 및 구성하고, RB트리 및 리스트에 insert한 후 va(vmap_area)를 반환한다.

  • vmap 캐시에서 먼저 검색하여 재사용할 수 있는지 확인한다.
    • 최종 등록한 vm 또는 최종 free한 vm의 이전(prev)vm부터 검색하면 빠른 성공을 기대할 수 있다.
  • 캐시에서 찾지 못한 경우 RB 트리로 구성된 전역 vmap_area_root에서 요청 시작 범위 바로 위에 있는 엔트리를 찾고
  • 이어서 리스트로 구성한 vmap_area_list에서 빈 공간을 찾는다.
  • 찾은 빈 공간에 별도로 메모리 할당 받은 vmap_area를 구성하고 insert 한다.
  • 만일 한 번의 검색에서 공간을 찾지 못하는 경우 해지를 유보(lazy)한 vmap_area를 flush한 후 다시 검색하여 공간을 찾는다.

 

  • 코드 라인 18~21에서 vmap_area 구조체를 구성하기 위해 reclaim 관련 플래그만 사용하여 할당을 받고 할당 에러인 경우 -ENOMEM을 반환한다.
  • 코드 라인 29~47에서 retry: 레이블이다. spin-lock을 얻고 캐시된 노드 위치를 사용할 수  없는 조건인 경우 이 번 검색에 캐시를 사용하지 못하게 한다. 조건은 다음과 같다.
    • 캐시 바로 이전(prev) 공간에 있는 hole이 새로 요청하는 size를 커버할 수 있는 경우
    • 시작 요청 범위가 캐시 사용 시의 요청 범위보다 작은 경우
    • 요청 align 값이 캐시된 align 값 보다 작은 경우
  • 코드 라인 53~59에서 free_vmap_cache 캐시가 가리키는 rb 노드를 first에 대입하고, 그 노드의 끝 주소를 addr에 대입하여 여기서 부터 검색할 준비를 한다.
    • 최종 등록하였거나 최근 free 시킨 va(vmap_area) 이전(prev) va를 보관한 free_vmap_cache에서 vm 엔트리를 가져온다.
    • 만일 first 엔트리의 끝 주소가 요청 범위를 벗어난 경우 캐시를 사용하지 않게 하기 위해 nocache 레이블로 이동한다.
    • 또한 first 엔트리의 끝 주소에 size를 더해서 범위를 초과한 경우 overflow 레이블로 이동한다.
  • 코드 라인 61~83에서 free_vmap_cache 캐시에 없는 경우 전역 vmap_area_root RB 트리를 통해 요청 범위에서 가장 첫 va를 찾아 first에 대입한다.
    • free_vmap_cache를 사용할 수 없는 경우 first 엔트리의 끝 주소에 size를 더해서 범위를 초과한 경우 overflow 레이블로 이동한다.

 

mm/vmalloc.c -2/2-

        /* from the starting point, walk areas until a suitable hole is found */
        while (addr + size > first->va_start && addr + size <= vend) {
                if (addr + cached_hole_size < first->va_start)
                        cached_hole_size = first->va_start - addr;
                addr = ALIGN(first->va_end, align);
                if (addr + size < addr)
                        goto overflow;

                if (list_is_last(&first->list, &vmap_area_list))
                        goto found;

                first = list_next_entry(first, list);
        }

found:
        if (addr + size > vend)
                goto overflow;

        va->va_start = addr;
        va->va_end = addr + size;
        va->flags = 0;
        __insert_vmap_area(va);
        free_vmap_cache = &va->rb_node;
        spin_unlock(&vmap_area_lock);

        BUG_ON(!IS_ALIGNED(va->va_start, align));
        BUG_ON(va->va_start < vstart);
        BUG_ON(va->va_end > vend);

        return va;

overflow:
        spin_unlock(&vmap_area_lock);
        if (!purged) {
                purge_vmap_area_lazy();
                purged = 1;
                goto retry;
        }

        if (gfpflags_allow_blocking(gfp_mask)) {
                unsigned long freed = 0;
                blocking_notifier_call_chain(&vmap_notify_list, 0, &freed);
                if (freed > 0) {
                        purged = 0;
                        goto retry;
                }
        }

        if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit())
                pr_warn("vmap allocation for size %lu failed: use vmalloc=<size> to increase size\n",
                        size);
        kfree(va);
        return ERR_PTR(-EBUSY);
}
  • 코드 라인 2~13에서 전역 first va(vmap_area)부터 리스트의 끝까지 요청 범위 내에서 size가 들어갈 수 있는 빈 공간을 찾는다.
  • 코드 라인 15~30에서 found: 레이블이다. 적절한 공간을 찾은 경우 RB 트리 및 리스트에 insert 하고, 영역을 반환한다.
  • 코드 라인 32~47에서 overflow: 레이블이다. 빈 공간을 찾을 수 없는 경우 lazy TLB flush 된 상태의 free 상태의 vm 엔트리들을 모두 purge 처리하여 삭제한 후 한 번만 다시 시도한다.

 

다음 그림은 매핑을 위해 요청 가상 주소 범위내에서 빈 공간을 찾을 때 먼저 free_vmap_cache 부터 size가 들어갈 빈공간을 검색하는 모습을 보여준다.

 

다음 그림은 매핑을 위해 요청 가상 주소 범위내에서 빈 공간을 찾을 때 RB 트리로 first vmap_area를 찾고 다시 리스트를 사용하여 size가 들어갈 빈공간을 검색하는 모습을 보여준다.

 

다음 그림은 free_vmap_cache와 cached_hole_size의 변화를 보여준다.

  • cached_hole_size는 캐시의 바로 앞 hole의 크기만을 기억한다.

 

vmap_area 추가

__insert_vmap_area()

mm/vmalloc.c

static void __insert_vmap_area(struct vmap_area *va)
{
        struct rb_node **p = &vmap_area_root.rb_node;
        struct rb_node *parent = NULL;
        struct rb_node *tmp;

        while (*p) {
                struct vmap_area *tmp_va;

                parent = *p;
                tmp_va = rb_entry(parent, struct vmap_area, rb_node);
                if (va->va_start < tmp_va->va_end)
                        p = &(*p)->rb_left;
                else if (va->va_end > tmp_va->va_start)
                        p = &(*p)->rb_right;
                else
                        BUG();
        }

        rb_link_node(&va->rb_node, parent, p);
        rb_insert_color(&va->rb_node, &vmap_area_root);

        /* address-sort this list */
        tmp = rb_prev(&va->rb_node);
        if (tmp) {
                struct vmap_area *prev;
                prev = rb_entry(tmp, struct vmap_area, rb_node);
                list_add_rcu(&va->list, &prev->list);
        } else
                list_add_rcu(&va->list, &vmap_area_list);
}

전역 vmap_area_root RB 트리와 전역 vmap_area_list에 vmap_area 엔트리를 insert 한다.

  • 코드 라인 7~18에서 vmap_area_root RB 트리에서 insert 할 leaf 노드를 찾는다.
  • 코드 라인 20~21에서 leaf 노드에 엔트리를 연결하고, RB 트리의 밸런스를 균형있게 맞춘다.
  • 코드 라인 24~30에서 마지막으로 rcu를 사용하여 vmap_area_list에 vmap_area 엔트리를 끼워 넣는다.
    • RB 트리에 insert한 엔트리를 RB 트리를 이용하여 rb_prev()를 사용하는 경우 바로 앞에 있는 노드를 알아내어 리스트 연결에 끼워넣을 수 있다.

 

다음 그림은 vmap_area 엔트리를 추가할 때의 모습을 보여준다.

 


vm_area 매핑

map_vm_area()

mm/vmalloc.c

int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page **pages)
{
        unsigned long addr = (unsigned long)area->addr;
        unsigned long end = addr + get_vm_area_size(area);
        int err;

        err = vmap_page_range(addr, end, prot, pages);

        return err > 0 ? 0 : err;
}
EXPORT_SYMBOL_GPL(map_vm_area);

요청한 vm_struct 정보에 담긴 가상 주소 범위에 매핑한다.

 

get_vm_area_size()

include/linux/vmalloc.h

static inline size_t get_vm_area_size(const struct vm_struct *area)
{
        if (!(area->flags & VM_NO_GUARD))
                /* return actual size without guard page */
                return area->size - PAGE_SIZE;
        else
                return area->size;

}

영역이 사용하는 페이지 수를 반환한다.

 

vmap_page_range()

mm/vmalloc.c

static int vmap_page_range(unsigned long start, unsigned long end,
                           pgprot_t prot, struct page **pages)
{
        int ret;

        ret = vmap_page_range_noflush(start, end, prot, pages);
        flush_cache_vmap(start, end);
        return ret;
}

요청한 가상 주소 범위를 매핑하고 그 공간을 flush한다.

 

vmap_page_range_noflush()

mm/vmalloc.c

/*
 * Set up page tables in kva (addr, end). The ptes shall have prot "prot", and
 * will have pfns corresponding to the "pages" array.
 *
 * Ie. pte at addr+N*PAGE_SIZE shall point to pfn corresponding to pages[N]
 */
static int vmap_page_range_noflush(unsigned long start, unsigned long end,
                                   pgprot_t prot, struct page **pages)
{
        pgd_t *pgd;
        unsigned long next;
        unsigned long addr = start;
        int err = 0;
        int nr = 0;

        BUG_ON(addr >= end);
        pgd = pgd_offset_k(addr);
        do {
                next = pgd_addr_end(addr, end);
                err = vmap_p4d_range(pgd, addr, next, prot, pages, &nr);
                if (err)
                        return err;
        } while (pgd++, addr = next, addr != end);

        return nr;
}

요청 가상 주소 범위에 해당하는 커널 페이지 테이블을 **pages 와 속성 정보를 사용하여 매핑한다.

  • pgd -> p4d -> pud -> pmd -> pte 테이블 순으로 population해가며 마지막 pte 테이블의 해당 엔트리에 매핑한다.

 

flush_cache_vmap() – ARM32

arch/arm/include/asm/cacheflush.h()

/*
 * flush_cache_vmap() is used when creating mappings (eg, via vmap,
 * vmalloc, ioremap etc) in kernel space for pages.  On non-VIPT
 * caches, since the direct-mappings of these pages may contain cached
 * data, we need to do a full cache flush to ensure that writebacks
 * don't corrupt data placed into these pages via the new mappings.
 */
static inline void flush_cache_vmap(unsigned long start, unsigned long end)
{
        if (!cache_is_vipt_nonaliasing())
                flush_cache_all();
        else
                /*
                 * set_pte_at() called from vmap_pte_range() does not
                 * have a DSB after cleaning the cache line.
                 */
                dsb(ishst);
}

요청한 가상 주소 범위에 대해 flush를 한다.

  • 아키텍처가 pipt 캐시를 사용하고나 vipt nonaliasing을 지원하는 경우 flush를 할 필요가 없어서 성능이 크게 개선된다.

 

flush_cache_vmap() – ARM64

arch/arm64/include/asm/cacheflush.h

/*
 * Not required on AArch64 (PIPT or VIPT non-aliasing D-cache).
 */
static inline void flush_cache_vmap(unsigned long start, unsigned long end)
{
}

ARM64 에서는 vmap 매핑을 위해 캐시를 flush하지 않는다.

 

vm_struct 설정

setup_vmalloc_vm()

mm/vmalloc.c

static void setup_vmalloc_vm(struct vm_struct *vm, struct vmap_area *va,
                              unsigned long flags, const void *caller)
{
        spin_lock(&vmap_area_lock);
        vm->flags = flags;
        vm->addr = (void *)va->va_start;
        vm->size = va->va_end - va->va_start;
        vm->caller = caller;
        va->vm = vm;
        va->flags |= VM_VM_AREA;
        spin_unlock(&vmap_area_lock);
}

vm_struct 및 vmap_area에 정보를 설정한다.

 


vmap 매핑 해제

vunmap()

mm/vmalloc.c

/**
 *      vunmap  -  release virtual mapping obtained by vmap()
 *      @addr:          memory base address
 *
 *      Free the virtually contiguous memory area starting at @addr,
 *      which was created from the page array passed to vmap().
 *
 *      Must not be called in interrupt context.
 */
void vunmap(const void *addr)
{
        BUG_ON(in_interrupt());
        might_sleep();
        if (addr)
                __vunmap(addr, 0);
}
EXPORT_SYMBOL(vunmap);

vmap() 함수로 vmalloc 주소 공간에 매핑한 가상 주소 영역의 매핑을 해제한다. 다만 물리 페이지는 할당 해제하지 않는다.

 

다음 그림은 vunmap() 함수에 대해 연관 함수들과의 처리 흐름을 보여준다.

 

다음 그림은 vummap() 함수가 요청한 가상 주소로 vmap_area()를 찾아 그에 해당하는 매핑을 삭제하는 모습을 보여준다.

vunmap-1b

 

__vunmap()

mm/vmalloc.c

static void __vunmap(const void *addr, int deallocate_pages)
{
        struct vm_struct *area;

        if (!addr)
                return;

        if (WARN(!PAGE_ALIGNED(addr), "Trying to vfree() bad address (%p)\n",
                        addr))
                return;

        area = find_vmap_area((unsigned long)addr)->vm;
        if (unlikely(!area)) {
                WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
                                addr);
                return;
        }

        debug_check_no_locks_freed(area->addr, get_vm_area_size(area));
        debug_check_no_obj_freed(area->addr, get_vm_area_size(area));

        remove_vm_area(addr);
        if (deallocate_pages) {
                int i;

                for (i = 0; i < area->nr_pages; i++) {
                        struct page *page = area->pages[i];

                        BUG_ON(!page);
                        __free_pages(page, 0);
                }

                kvfree(area->pages);
        }

        kfree(area);
        return;
}

요청 가상 주소로 vm 정보를 찾아 매핑을 제거하고 요청에 따라 각 페이지들을 해제하여 버디 시스템으로 돌려준다.

  • RB 트리 vmap_area_root에 등록되어 있는 vmap_area 정보에서 요청 가상 주소를  검색하여 매치된 vmap_area 및 vm_struct 정보를 제거하고 매핑을 해제한 다. 만일 @deallocate_pages 인수의 요청 여부에 따라 물리 페이지들을 해제한다.

 

  • 코드 라인 12~17에서 RB 트리 vmap_area_root에 등록되어 있는 vmap_area 정보에서 요청 가상 주소로 vm을 검색한다.
  • 코드 라인 22에서 vm을 RB 트리 및 리스트에서 제거하고, 매핑 해제 요청한 후 vm을 반환한다.
  • 코드 라인 23~34에서 @deallocate_pages 인수 요청이 설정된 경우 등록된 모든 페이지들을 해제하여 버디 시스템으로 돌려준다.
  • 코드 라인 36~37에서 vm_area 구조체 정보를 할당 해제한다.

 

vmap_area  삭제 및 매핑 해제 요청

remove_vm_area()

mm/vmalloc.c

/**
 *      remove_vm_area  -  find and remove a continuous kernel virtual area
 *      @addr:          base address
 *
 *      Search for the kernel VM area starting at @addr, and remove it.
 *      This function returns the found VM area, but using it is NOT safe
 *      on SMP machines, except for its size or flags.
 */
struct vm_struct *remove_vm_area(const void *addr)
{
        struct vmap_area *va;

        might_sleep();

        va = find_vmap_area((unsigned long)addr);
        if (va && va->flags & VM_VM_AREA) {
                struct vm_struct *vm = va->vm;

                spin_lock(&vmap_area_lock);
                va->vm = NULL;
                va->flags &= ~VM_VM_AREA;
                va->flags |= VM_LAZY_FREE;
                spin_unlock(&vmap_area_lock);

                kasan_free_shadow(vm);
                free_unmap_vmap_area(va);

                return vm;
        }
        return NULL;
}

요청 가상 주소를 RB 트리 vmap_area_root에 등록되어 있는 vmap_area 정보에서 검색하여 매치된 vmap_area 정보를 제거하고 매핑을 해제 요청한 후 vm_struct 정보를 알아온다.

  • 메모리 매핑은 해제하는데 vm_area는 VM_VM_AREA 플래그를 삭제하고 VM_LAZY_FREE 플래그를 추가하기만 한다.
  • 실제 삭제는 vmap_lazy_nr 갯수가 일정량을 초과하거나 메모리 부족 시 VM_LAZY_FREE 설정된 vma들을 한꺼번에 purge 처리한다.
    • vmalloc address space의 매핑이 해지되거나 수정되는 경우 모든 core에서 TLB 플러쉬가 발생되어야 하는데 이를 매 번 수행하는 경우 성능이 현저하게 저하되므로 삭제될 항목을 모아 두었다가  한꺼번에 삭제하는 방식을 취한다. 이를 Lazy TLB flushing이라 부른다.

 

 

vmap_area 검색

find_vmap_area()

mm/vmalloc.c

static struct vmap_area *find_vmap_area(unsigned long addr)
{
        struct vmap_area *va;

        spin_lock(&vmap_area_lock);
        va = __find_vmap_area(addr);
        spin_unlock(&vmap_area_lock);

        return va;
}

vmap_area lock으로 보호한 후 요청 가상 주소로 vmap_area 정보를 찾아온다. 못찾은 경우 null을 반환한다.

 

__find_vmap_area()

mm/vmalloc.c

static struct vmap_area *__find_vmap_area(unsigned long addr)
{
        struct rb_node *n = vmap_area_root.rb_node;

        while (n) {
                struct vmap_area *va;

                va = rb_entry(n, struct vmap_area, rb_node);
                if (addr < va->va_start)
                        n = n->rb_left;
                else if (addr >= va->va_end)
                        n = n->rb_right;
                else
                        return va;
        }

        return NULL;
}

요청 가상 주소로 vmap_area 정보를 찾아온다. 못찾은 경우 null을 반환한다.

  • 요청 가상 주소를 RB 트리 vmap_area_root에 등록되어 있는 vmap_area 정보에서 검색하여 매치된 vmap_area 정보를 찾아온다.

 


vmap_area 매핑 해제

free_unmap_vmap_area()

mm/vmalloc.c

/*
 * Free and unmap a vmap area
 */
static void free_unmap_vmap_area(struct vmap_area *va)
{
        flush_cache_vunmap(va->va_start, va->va_end);
        unmap_vmap_area(va);
        if (debug_pagealloc_enabled())
                flush_tlb_kernel_range(va->va_start, va->va_end);
        free_unmap_vmap_area_noflush(va);
}

아키텍처에 따라 지정된 가상 주소 범위의 데이타 캐시를 비운후 해당 영역의 매핑을 해제한다.

 

flush_cache_vunmap() – ARM32

arch/arm/include/asm/cacheflush.h

static inline void flush_cache_vunmap(unsigned long start, unsigned long end)
{
        if (!cache_is_vipt_nonaliasing())
                flush_cache_all();
}

아키텍처의 데이타 캐시가 vivt 타입이거나 vipt aliasing인 경우 캐시를 모두 비우게 한다.

  • 데이타 캐시가 pipt 타입이거나 vipt nonaliasing 타입인 경우 캐시를 비우지 않아도 되므로 성능이 향상된다.

 

flush_cache_vunmap() – ARM64

arch/arm64/include/asm/cacheflush.h

static inline void flush_cache_vunmap(unsigned long start, unsigned long end)
{
}

ARM64 에서는 vmap 매핑 해제를 위해 캐시를 flush하지 않는다.

 

free_unmap_vmap_area_noflush()

mm/vmalloc.c

/*
 * Free and unmap a vmap area, caller ensuring flush_cache_vunmap had been
 * called for the correct range previously.
 */
static void free_unmap_vmap_area_noflush(struct vmap_area *va)
{       
        unmap_vmap_area(va);
        free_vmap_area_noflush(va);
}

vmap_area가 사용하는 가상 주소 영역의 매핑을 커널 페이지 테이블에서 해제한다. 그런 후 vmap_area를 곧바로 삭제하지 않고 purge_list에 추가한다.

  • vmap_area가 재활용되는 경우 시간 소모가 큰 TLB 캐시를 flush 하지 않아도 되기 때문에 성능이 매우 좋아진다.

 

페이지 테이블 매핑 해제

unmap_vmap_area()

mm/vmalloc.c

/*
 * Clear the pagetable entries of a given vmap_area
 */
static void unmap_vmap_area(struct vmap_area *va)
{
        vunmap_page_range(va->va_start, va->va_end);
}

vmap_area의 가상 주소 영역의 매핑을 커널 페이지 테이블에서 해제한다.

 

vunmap_page_range()

mm/vmalloc.c

static void vunmap_page_range(unsigned long addr, unsigned long end)
{
        pgd_t *pgd;
        unsigned long next;

        BUG_ON(addr >= end);
        pgd = pgd_offset_k(addr);
        do {
                next = pgd_addr_end(addr, end);
                if (pgd_none_or_clear_bad(pgd))
                        continue;
                vunmap_p4d_range(pgd, addr, next);
        } while (pgd++, addr = next, addr != end);
}

요청 가상 주소 범위의 매핑을 커널 페이지 테이블에서 해제한다.

  • pgd -> p4d -> pud -> pmd -> pte 테이블 순으로 찾아가면 마지막 pte 테이블의 해당 엔트리를 언매핑한다.

 

lazy TLB Flush 요청

free_vmap_area_noflush()

mm/vmalloc.c

/*
 * Free a vmap area, caller ensuring that the area has been unmapped
 * and flush_cache_vunmap had been called for the correct range
 * previously.
 */
static void free_vmap_area_noflush(struct vmap_area *va)
{
        int nr_lazy;

        nr_lazy = atomic_add_return((va->va_end - va->va_start) >> PAGE_SHIFT,
                                    &vmap_lazy_nr);

        /* After this point, we may free va at any time */
        llist_add(&va->purge_list, &vmap_purge_list);

        if (unlikely(nr_lazy > lazy_max_pages()))
                try_purge_vmap_area_lazy();
}

vmap_area를 곧바로 삭제하지 않고 VM_LAZY_FREE 플래그를 설정하여 lazy TLB 플러시 요청한다.  그 후 페이지 수가 일정량을 초과하는 경우 purge 처리를 수행한다.

lazy_max_pages()

mm/vmalloc.c

/*
 * lazy_max_pages is the maximum amount of virtual address space we gather up
 * before attempting to purge with a TLB flush.
 *
 * There is a tradeoff here: a larger number will cover more kernel page tables
 * and take slightly longer to purge, but it will linearly reduce the number of
 * global TLB flushes that must be performed. It would seem natural to scale
 * this number up linearly with the number of CPUs (because vmapping activity
 * could also scale linearly with the number of CPUs), however it is likely
 * that in practice, workloads might be constrained in other ways that mean
 * vmap activity will not scale linearly with CPUs. Also, I want to be
 * conservative and not introduce a big latency on huge systems, so go with
 * a less aggressive log scale. It will still be an improvement over the old
 * code, and it will be simple to change the scale factor if we find that it
 * becomes a problem on bigger systems.
 */
static unsigned long lazy_max_pages(void)
{
        unsigned int log;

        log = fls(num_online_cpus());

        return log * (32UL * 1024 * 1024 / PAGE_SIZE);
}

TLB lazy된 페이지들의 purge 처리를 위해 필요한 페이지 수를 산출한다.

  • 32M에 해당하는 페이지 수 * log2(online cpu) + 1에 비례하는 페이지 수를 반환한다.
  • cpu가 많아지는 경우 TLB flush는 시스템의 전체적인 성능을 떨어뜨리므로 cpu 수가 많아질 수록 lazy_max_pages 수는 더 커져야 한다.

 

lazy TLB vmap_area들의 Purge 처리

try_purge_vmap_area_lazy()

mm/vmalloc.c

/*
 * Kick off a purge of the outstanding lazy areas. Don't bother if somebody
 * is already purging.
 */
static void try_purge_vmap_area_lazy(void)
{
        if (mutex_trylock(&vmap_purge_lock)) {
                __purge_vmap_area_lazy(ULONG_MAX, 0);
                mutex_unlock(&vmap_purge_lock);
        }
}

Lazy TLB 플러시 요청된 vmap_area들을 제거하고 전체 가상 주소 범위의 TLB flush를 수행한다.

 

__purge_vmap_area_lazy()

mm/vmalloc.c

/*
 * Purges all lazily-freed vmap areas.
 */
static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end)
{
        struct llist_node *valist;
        struct vmap_area *va;
        struct vmap_area *n_va;
        bool do_free = false;

        lockdep_assert_held(&vmap_purge_lock);

        valist = llist_del_all(&vmap_purge_list);
        llist_for_each_entry(va, valist, purge_list) {
                if (va->va_start < start)
                        start = va->va_start;
                if (va->va_end > end)
                        end = va->va_end;
                do_free = true;
        }

        if (!do_free)
                return false;

        flush_tlb_kernel_range(start, end);

        spin_lock(&vmap_area_lock);
        llist_for_each_entry_safe(va, n_va, valist, purge_list) {
                int nr = (va->va_end - va->va_start) >> PAGE_SHIFT;

                __free_vmap_area(va);
                atomic_sub(nr, &vmap_lazy_nr);
                cond_resched_lock(&vmap_area_lock);
        }
        spin_unlock(&vmap_area_lock);
        return true;
}

Lazy TLB 플러시 요청된 vmap_area들을 모두 제거하고 @start ~ @end 범위의 TLB flush를 수행한다.

  • 코드 라인 10~17에서 purge 리스트에 있는 vmap_area 들의 최초 시작 주소와 마지막 끝 주소를 반영하여 flush할 시작 주소와 끝 주소를 갱신한다.
  • 코드 라인 22에서 갱신된 start ~ end 범위의 가상 주소 영역에 대해 TLB 플러시를 수행한다.
  • 코드 라인 24~32에서 purge 리스트에 있는 모든 vmap_area들을 할당 해제한다.
  • 코드 라인 33에서 성공 true를 반환한다.

 

vmap_area 삭제

__free_vmap_area()

mm/vmalloc.c

static void __free_vmap_area(struct vmap_area *va)
{
        BUG_ON(RB_EMPTY_NODE(&va->rb_node));

        if (free_vmap_cache) {
                if (va->va_end < cached_vstart) {
                        free_vmap_cache = NULL;
                } else {
                        struct vmap_area *cache;
                        cache = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
                        if (va->va_start <= cache->va_start) {
                                free_vmap_cache = rb_prev(&va->rb_node);
                                /*
                                 * We don't try to update cached_hole_size or
                                 * cached_align, but it won't go very wrong.
                                 */
                        }
                }
        }
        rb_erase(&va->rb_node, &vmap_area_root);
        RB_CLEAR_NODE(&va->rb_node);
        list_del_rcu(&va->list);

        /*
         * Track the highest possible candidate for pcpu area
         * allocation.  Areas outside of vmalloc area can be returned
         * here too, consider only end addresses which fall inside
         * vmalloc area proper.
         */
        if (va->va_end > VMALLOC_START && va->va_end <= VMALLOC_END)
                vmap_area_pcpu_hole = max(vmap_area_pcpu_hole, va->va_end);

        kfree_rcu(va, rcu_head);
}

요청한 vmap_area를 RB 트리 vmap_area_root와 vmap_area_list에서 제거한 후 해제한다.

 

참고

 

Vmalloc

<kernel v5.0>

Vmalloc 할당자

커널에서 주로 큰 사이즈의 연속된 가상 주소 공간을 할당 받아 사용한다. 물리적으로 연속되지 않는 공간이므로 dma 버퍼로는 사용할 수 없다.

 

특징

가상 주소가 연속된 큰 사이즈의 커널 메모리의 할당이 필요한 경우 사용된다. vmalloc을 이용한 커널 메모리의 할당은 다음과 같은 특징이 있다.

  • 메모리를 할당할 때마다 vmalloc 가상 주소 공간에 매핑하여 사용할 수 있으므로 매핑/해제에 cost가 많이 소모된다.
  • vmalloc 가상 주소 공간에 매핑하는 메모리 페이지는 버디 시스템을 사용하는 페이지 할당자로 부터 필요한 만큼 order 0인 싱글 페이지만을 할당받아 사용하므로 fragment 관리에 매우 적합하다.
  • 물리적으로 연속된 메모리를 사용하지 않으므로 DMA 용으로 사용될 수 없다.
    • 참고: iommu 장치를 사용하여 dynamic 매핑을 하는 경우 물리적으로 연속되지 않은 메모리에 대해 디바이스가 이 영역을 dma 버퍼로 사용할 방법은 있다. 현재는 모바일 시스템에서 gpu 등의 디바이스가 arm社의 smmu(iommu)를 사용하고 있고, 점점 그 수가 늘어날 전망이다.

 

API

할당 및 해제 관련한 주요 API는 다음과 같다.

  • vmalloc()
  • vfree()

 

vmalloc 가상 주소 공간

버디 시스템을 사용하는 페이지 할당자로 부터 vmalloc() 함수가 요청한 size가 들어갈 수 있을만큼 싱글 페이지들을 할당받아 이를 모아 vmalloc 가상 주소 공간의 빈 공간을 찾아 각 싱글 페이지를 순차적으로 빈 자리에 매핑하고 그 매핑된 가상 주소를 반환한다.

 

다음 그림은 vmalloc 가상 주소 공간의 위치를 보여준다.

 

vmalloc() 함수를 통해 요청한 사이즈가 들어갈 수 있는 싱글 페이지들을 가져와 vmalloc 가상 주소 영역의 빈자리를 찾아 연속되도록 한꺼번에 매핑하는 모습을 보여준다.

 


vmalloc 초기화

커널이 사용하는 연속된 가상 주소 메모리를 할당 받는 메커니즘을 위해 초기화를 수행한다.

  • vmalloc() & vfree()
    • 연속된 가상 주소 메모리 할당과 메모리 해제
  • vmap() & vunmap()
    • 가상 주소 공간에 페이지를 매핑과 해제
      • 현재 이용할 수 있는 가상 주소 공간으로 vmalloc 가상 주소 공간과 module 가상 주소 공간이 있다.

 

다음 그림은 vmalloc_init() 함수의 처리 흐름을 보여준다.

vmalloc_init-1

 

vmalloc_init()

mm/vmalloc.c

void __init vmalloc_init(void)
{
        struct vmap_area *va;
        struct vm_struct *tmp;
        int i;

        for_each_possible_cpu(i) {
                struct vmap_block_queue *vbq;
                struct vfree_deferred *p;

                vbq = &per_cpu(vmap_block_queue, i);
                spin_lock_init(&vbq->lock);
                INIT_LIST_HEAD(&vbq->free);
                p = &per_cpu(vfree_deferred, i);
                init_llist_head(&p->list);
                INIT_WORK(&p->wq, free_work);
        }

        /* Import existing vmlist entries. */
        for (tmp = vmlist; tmp; tmp = tmp->next) {
                va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT);
                va->flags = VM_VM_AREA;
                va->va_start = (unsigned long)tmp->addr;
                va->va_end = va->va_start + tmp->size; 
                va->vm = tmp;
                __insert_vmap_area(va);
        }

        vmap_area_pcpu_hole = VMALLOC_END;

        vmap_initialized = true;
}

vmalloc 공간을 이용할 수 있도록 초기화한다.

  • 코드 라인 7~17에서 possible cpu 수 만큼 루프를 돌며 per-cpu vmap_block_queue 및 per-cpu vfree_deferred를 초기화한다.
    • per-cpu vmap_block_queue
      • 할당 및 flushing 목적으로 free 및 dirty vmap block 큐로 사용된다.
    • per-cpu vfree_deferred
      • 인터럽트 처리 중에 vfree() 처리를 지연시킬 목적으로 사용되는 대기 리스크
  • 코드 라인 20~27에서 전역 vmlist에 등록되어 있는 수 만큼 vmap_area 구조체를 할당 받아 초기화하여 vmap_area에 추가한다.
    • vmlist
      • vm_area_register_early() 함수를 통해 vm이 등록되는데 pcpu_page_first_chunk()를 만들 때 사용된다.
        • 그 외 x86에서 xen ops 드라이버에서 호출 될때에도 사용된다.
  • 코드 라인 29~31에서 per-cpu가 vmalloc 공간의 최상단부터 아래 방향으로 사용될 예정이다. vmap을 사용할 준비가 되었음을 알린다.

 


vmalloc 할당

vmalloc()

mm/vmalloc.c

/**
 *      vmalloc  -  allocate virtually contiguous memory
 *      @size:          allocation size
 *      Allocate enough pages to cover @size from the page level
 *      allocator and map them into contiguous kernel virtual space.
 *
 *      For tight control over page level allocator and protection flags
 *      use __vmalloc() instead.
 */
void *vmalloc(unsigned long size)
{
        return __vmalloc_node_flags(size, NUMA_NO_NODE,
                                    GFP_KERNEL);
}
EXPORT_SYMBOL(vmalloc);

커널을 위해 @size 만큼의 연속된 가상 주소 공간을 할당받는다.  __vmalloc_node_flags() 함수를 이어 호출한다.

 

다음 그림은 vmalloc() 함수와 관련 함수들간의 처리 흐름을 보여준다.

 

__vmalloc_node_flags()

mm/vmalloc.c

static inline void *__vmalloc_node_flags(unsigned long size,
                                        int node, gfp_t flags)
{
        return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
                                        node, __builtin_return_address(0));
}

커널을 위해 요청 노드에서 연속된 가상 메모리를 할당한다.

 

__vmalloc_node()

mm/vmalloc.c

/**
 *      __vmalloc_node  -  allocate virtually contiguous memory
 *      @size:          allocation size
 *      @align:         desired alignment
 *      @gfp_mask:      flags for the page level allocator
 *      @prot:          protection mask for the allocated pages
 *      @node:          node to use for allocation or NUMA_NO_NODE
 *      @caller:        caller's return address
 *
 *      Allocate enough pages to cover @size from the page level
 *      allocator with @gfp_mask flags.  Map them into contiguous
 *      kernel virtual space, using a pagetable protection of @prot.
 *
 *      Reclaim modifiers in @gfp_mask - __GFP_NORETRY, __GFP_RETRY_MAYFAIL
 *      and __GFP_NOFAIL are not supported
 *
 *      Any use of gfp flags outside of GFP_KERNEL should be consulted
 *      with mm people.
 *
 */
static void *__vmalloc_node(unsigned long size, unsigned long align,
                            gfp_t gfp_mask, pgprot_t prot,
                            int node, const void *caller)
{
        return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
                                gfp_mask, prot, 0, node, caller);
}

커널을 위해 @node에서 연속된 가상 메모리를 할당하되 가상 주소는 VMALLOC address space의 빈 공간을 사용하여 가상 주소를 매핑하게 한다.

 

__vmalloc_node_range()

mm/vmalloc.c

/**
 *      __vmalloc_node_range  -  allocate virtually contiguous memory
 *      @size:          allocation size
 *      @align:         desired alignment
 *      @start:         vm area range start
 *      @end:           vm area range end
 *      @gfp_mask:      flags for the page level allocator
 *      @prot:          protection mask for the allocated pages
 *      @vm_flags:      additional vm area flags (e.g. %VM_NO_GUARD)
 *      @node:          node to use for allocation or NUMA_NO_NODE
 *      @caller:        caller's return address
 *
 *      Allocate enough pages to cover @size from the page level
 *      allocator with @gfp_mask flags.  Map them into contiguous
 *      kernel virtual space, using a pagetable protection of @prot.
 */
void *__vmalloc_node_range(unsigned long size, unsigned long align,
                        unsigned long start, unsigned long end, gfp_t gfp_mask,
                        pgprot_t prot, unsigned long vm_flags, int node,
                        const void *caller)
{
        struct vm_struct *area;
        void *addr;
        unsigned long real_size = size;

        size = PAGE_ALIGN(size);
        if (!size || (size >> PAGE_SHIFT) > totalram_pages)
                goto fail;

        area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
                                vm_flags, start, end, node, gfp_mask, caller);
        if (!area)
                goto fail;

        addr = __vmalloc_area_node(area, gfp_mask, prot, node);
        if (!addr)
                return NULL;

        /*
         * In this function, newly allocated vm_struct has VM_UNINITIALIZED
         * flag. It means that vm_struct is not fully initialized.
         * Now, it is fully initialized, so remove this flag here.
         */
        clear_vm_uninitialized_flag(area);

        /*
         * A ref_count = 2 is needed because vm_struct allocated in
         * __get_vm_area_node() contains a reference to the virtual address of
         * the vmalloc'ed block.
         */
        kmemleak_vmalloc(area, size, gfp_mask);

        return addr;

fail:
        warn_alloc_failed(gfp_mask, NULL,
                          "vmalloc: allocation failure: %lu bytes\n", real_size);
        return NULL;
}

요청 노드에서 연속된 가상 메모리를 할당하되 가상 주소는 지정된 범위 이내의 빈 공간을 사용한 가상 주소를 매핑하게 한다.

  • 코드 라인 10~12에서 size를 페이지 단위로 정렬하고 그 size가 0이거나 전체 메모리보다 큰 경우 fail 레이블을 경유해 null을 반환한다.
  • 코드 라인 14~17에서 요청 가상 주소 범위에서 요청 size가 들어갈 수 있는 빈 자리를 찾아 그 가상 주소로 vmap_area와 vm_struct 정보를 구성하여 반환한다.
    • 참고: Vmap | 문c
  • 코드 라인 19~21에서 vm_struct 정보가 요청하는 가상 주소 영역 만큼 page descriptor 테이블을 할당받고 order 0 페이지들을 요청 수 만큼 할당하여 연결하고  페이지 테이블에 매핑한다.
  • 코드 라인 28에서 vm에 대해 uninitialized 플래그를 클리어하여 vm이 초기화 되었음을 나타낸다.
  • 코드 라인 37에서 할당한 가상 주소 공간을 반환한다.

 

다음 그림은 vmap_area와 vm_struct가 할당받아 구성되고 필요한 물리 페이지 수 만큼 page descriptor 배열을 할당 받고 그 수 만큼 order 0 페이지들을 할당받아 연결하고 매핑되는 모습을 보여준다.

 

__vmalloc_area_node()

mm/vmalloc.c

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                                 pgprot_t prot, int node)
{
        struct page **pages;
        unsigned int nr_pages, array_size, i;
        const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
        const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;
        const gfp_t highmem_mask = (gfp_mask & (GFP_DMA | GFP_DMA32)) ?
                                        0 :
                                        __GFP_HIGHMEM;

        nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
        array_size = (nr_pages * sizeof(struct page *));

        area->nr_pages = nr_pages;
        /* Please note that the recursion is strictly bounded. */
        if (array_size > PAGE_SIZE) {
                pages = __vmalloc_node(array_size, 1, nested_gfp|highmem_mask,
                                PAGE_KERNEL, node, area->caller);
        } else {
                pages = kmalloc_node(array_size, nested_gfp, node);
        }
        area->pages = pages;
        if (!area->pages) {
                remove_vm_area(area->addr);
                kfree(area);
                return NULL;
        }

        for (i = 0; i < area->nr_pages; i++) {
                struct page *page;

                if (node == NUMA_NO_NODE)
                        page = alloc_page(alloc_mask|highmem_mask);
                else
                        page = alloc_pages_node(node, alloc_mask|highmem_mask, 0);

                if (unlikely(!page)) {
                        /* Successfully allocated i pages, free them in __vunmap() */
                        area->nr_pages = i;
                        goto fail;
                }
                area->pages[i] = page;
                if (gfpflags_allow_blocking(gfp_mask|highmem_mask))
                        cond_resched();
        }

        if (map_vm_area(area, prot, pages))
                goto fail;
        return area->addr;

fail:
        warn_alloc(gfp_mask, NULL,
                          "vmalloc: allocation failure, allocated %ld of %ld bytes",
                          (area->nr_pages*PAGE_SIZE), area->size);
        vfree(area->addr);
        return NULL;
}

vm_struct 정보가 요청하는 가상 주소 영역 만큼 page 디스크립터 배열을 할당받고 싱글 페이지들을 요청 수 만큼 할당하여 연결하고  페이지 테이블에 매핑한다.

  • 코드 라인 6에서 페이지 회수와 관련된 플래그만 남기고 __GFP_ZERO를 추가한다.
  • 코드 라인 7에서 gfp_mask에 __GFP_NOWARN을 추가한다.
  • 코드 라인 8~10에서 dma 및 dma32 존에 대한 요청이 없는 경우 highmem 존 마스크를 준비한다.
  • 코드 라인 12에서 영역이 사용하는 페이지 수를 알아오고 만들 page descriptor들이 사용할 배열의 크기를 구한다.
  • 코드 라인 15에서 vm에 사용될 페이지 수를 기록한다.
  • 코드 라인 17~19에서 페이지 디스크립터 배열 할당을 위한 array_size가 1 페이지를 초과하는 경우 해당 노드의 highmem을 포함한 zone에서 array_size 만큼의 공간을 할당받는다.
    • vmalloc() 함수가 진행 중에 nest되어 호출되는 상황이다.
    • area의 플래그 정보에 VM_VPAGES를 설정하여 할당 받은 page descriptor 배열이 있다는 것을 나타낸다.
  • 코드 라인 20~22에서 array_size가 array_size가 1페이지 이내인 경우 kmalloc_node() 함수를 사용하여 슬랩 object를 할당받아 page 디스크립터 배열을 구성하게 한다.
  • 코드 라인 23~28에서 area가 사용하는 페이지 디스크립터 배열을 가리키게 한다.(vm 전용 mem_map)
  • 코드 라인 30~46에서 페이지 수 만큼 루프를 돌며 버디 시스템을 사용하는 페이지 할당자로 부터 1개의 싱글 페이지를 할당 받고, 페이지 디스크립터 배열에 할당 받은 각 페이지를 연결한다.
  • 코드 라인 48~50에서 area의 매핑을 수행한 후 그 가상 주소를 반환한다. 만일 실패하는 경우 fail 레이블로 이동한다.

 

clear_vm_uninitialized_flag()

mm/vmalloc.c

static void clear_vm_uninitialized_flag(struct vm_struct *vm)
{
        /*
         * Before removing VM_UNINITIALIZED,
         * we should make sure that vm has proper values.
         * Pair with smp_rmb() in show_numa_info().
         */
        smp_wmb();
        vm->flags &= ~VM_UNINITIALIZED;
}

vm 설정이 완료되었음을 나타내기 위해 VM_UNINITIALIZED 플래그를 제거한다.

 


vmalloc 할당 해제

vfree()

mm/vmalloc.c

/**
 *      vfree  -  release memory allocated by vmalloc()
 *      @addr:          memory base address
 *
 *      Free the virtually continuous memory area starting at @addr, as
 *      obtained from vmalloc(), vmalloc_32() or __vmalloc(). If @addr is
 *      NULL, no operation is performed.
 *
 *      Must not be called in NMI context (strictly speaking, only if we don't
 *      have CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG, but making the calling
 *      conventions for vfree() arch-depenedent would be a really bad idea)
 *
 *      May sleep if called *not* from interrupt context.
 *
 *      NOTE: assumes that the object at @addr has a size >= sizeof(llist_node)
 */
void vfree(const void *addr)
{
        BUG_ON(in_nmi());

        kmemleak_free(addr);

        might_sleep_if(!in_interrupt());

        if (!addr)
                return;
        if (unlikely(in_interrupt()))
                __vfree_deferred(addr);
        else
                __vunmap(addr, 1);
}
EXPORT_SYMBOL(vfree);

vmalloc()에 의해 할당되고 매핑된 연속된 가상 주소 메모리를 해제한다.

  • 인터럽트 핸들러를 통해 호출되는 경우 당장 매핑된 연속된 가상 주소 메모리를 해제할 수 없으므로 지연 처리를 위해 per-cpu vfree_deferred에 추가한 후 워크큐에 등록된 free_work() 함수를 스케쥴 한다.

 

다음 그림은 vfree() 함수와 관련 함수의 흐름을 보여준다.

 

free_work()

mm/vmalloc.c

static void free_work(struct work_struct *w)
{
        struct vfree_deferred *p = container_of(w, struct vfree_deferred, wq);
        struct llist_node *t, *llnode;

        llist_for_each_safe(llnode, t, llist_del_all(&p->list))
                __vunmap((void *)llnode, 1);
}

per-cpu vfree_deferred 리스트에 있는 모든 vm(연속된 가상 주소에 매핑된 메모리)들을 해제한다.

 

참고

 

Zoned Allocator -11- (Direct Reclaim)

<kernel v5.0>

Zoned Allocator -11- (Direct Reclaim)

Reclaim 판단

should_continue_reclaim()

mm/vmscan.c

/*
 * Reclaim/compaction is used for high-order allocation requests. It reclaims
 * order-0 pages before compacting the zone. should_continue_reclaim() returns
 * true if more pages should be reclaimed such that when the page allocator
 * calls try_to_compact_zone() that it will have enough free pages to succeed.
 * It will give up earlier than that if there is difficulty reclaiming pages.
 */
static inline bool should_continue_reclaim(struct pglist_data *pgdat,
                                        unsigned long nr_reclaimed,
                                        unsigned long nr_scanned,
                                        struct scan_control *sc)
{
        unsigned long pages_for_compaction;
        unsigned long inactive_lru_pages;
        int z;

        /* If not in reclaim/compaction mode, stop */
        if (!in_reclaim_compaction(sc))
                return false;

        /* Consider stopping depending on scan and reclaim activity */
        if (sc->gfp_mask & __GFP_RETRY_MAYFAIL) {
                /*
                 * For __GFP_RETRY_MAYFAIL allocations, stop reclaiming if the
                 * full LRU list has been scanned and we are still failing
                 * to reclaim pages. This full LRU scan is potentially
                 * expensive but a __GFP_RETRY_MAYFAIL caller really wants to succeed
                 */
                if (!nr_reclaimed && !nr_scanned)
                        return false;
        } else {
                /*
                 * For non-__GFP_RETRY_MAYFAIL allocations which can presumably
                 * fail without consequence, stop if we failed to reclaim
                 * any pages from the last SWAP_CLUSTER_MAX number of
                 * pages that were scanned. This will return to the
                 * caller faster at the risk reclaim/compaction and
                 * the resulting allocation attempt fails
                 */
                if (!nr_reclaimed)
                        return false;
        }

        /*
         * If we have not reclaimed enough pages for compaction and the
         * inactive lists are large enough, continue reclaiming
         */
        pages_for_compaction = compact_gap(sc->order);
        inactive_lru_pages = node_page_state(pgdat, NR_INACTIVE_FILE);
        if (get_nr_swap_pages() > 0)
                inactive_lru_pages += node_page_state(pgdat, NR_INACTIVE_ANON);
        if (sc->nr_reclaimed < pages_for_compaction &&
                        inactive_lru_pages > pages_for_compaction)
                return true;

        /* If compaction would go ahead or the allocation would succeed, stop */
        for (z = 0; z <= sc->reclaim_idx; z++) {
                struct zone *zone = &pgdat->node_zones[z];
                if (!managed_zone(zone))
                        continue;

                switch (compaction_suitable(zone, sc->order, 0, sc->reclaim_idx)) {
                case COMPACT_SUCCESS:
                case COMPACT_CONTINUE:
                        return false;
                default:
                        /* check next zone */
                        ;
                }
        }
        return true;
}

high order 페이지 요청을 처리하는데 reclaim/compaction이 계속되야 하는 경우  true를 반환한다.

  • 코드 라인 11~12에서 reclaim/compaction 모드가 아니면 처리를 중단한다.
  • 코드 라인 15~35에서 __GFP_RETRY_MAYFAIL 플래그가 사용된 경우 reclaimed 페이지와 scanned 페이지가 없는 경우 false를 반환한다. 플래그가 사용되지 않은 경우 reclaimed 페이지가 없는 경우 false를 반환한다.
  • 코드 라인 41~47에서 reclaimed 페이지가 order 페이지의 두 배보다 작아 compaction을 위해 작지만 inactive lru 페이지 수가 order 페이지의 두 배보다는 커 충분한 경우 true를 반환한다.
  • 코드 라인 50~64에서 reclaim_idx만큼 존을 순회하며 compaction이 이미 성공하였거나 계속해야 하는 경우 false를 반환한다.
  • 코드 라인 64에서 순회한 모든 존에서 compaction의 성공이 없는 경우 true를 반환하여 compaction이 계속되어야 함을 알린다.

 

in_reclaim_compaction()

mm/vmscan.c

/* Use reclaim/compaction for costly allocs or under memory pressure */
static bool in_reclaim_compaction(struct scan_control *sc)
{
        if (IS_ENABLED(CONFIG_COMPACTION) && sc->order &&
                        (sc->order > PAGE_ALLOC_COSTLY_ORDER ||
                         sc->priority < DEF_PRIORITY - 2))
                return true;

        return false;
}

reclaim/compaction 모드인 경우 true를 반환한다.

  • 0 order 요청을 제외하고 다음 두 조건을 만족하면 true를 반환한다.
    • 우선 순위를 2번 이상 높여 반복 수행 중이다. (낮은 priority 번호가 높은 우선 순위)
    • costly order 요청이다.(order 4부터)

 


Direct-Reclaim 수행

__alloc_pages_direct_reclaim()

mm/page_alloc.c

/* The really slow allocator path where we enter direct reclaim */
static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
                int alloc_flags, const struct alloc_context *ac,
                unsigned long *did_some_progress)
{
        struct page *page = NULL;
        bool drained = false;

        *did_some_progress = __perform_reclaim(gfp_mask, order, ac);
        if (unlikely(!(*did_some_progress)))
                return NULL;

retry:
        page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

        /*
         * If an allocation failed after direct reclaim, it could be because
         * pages are pinned on the per-cpu lists. Drain them and try again
         */
        if (!page && !drained) {
                unreserve_highatomic_pageblock(ac, false);
                drain_all_pages(NULL);
                drained = true;
                goto retry;
        }

        return page;
}

페이지를 회수한 후 페이지 할당을 시도한다. 만일 처음 실패하는 경우 pcp 캐시를 비워 버디 시스템에 free 페이지를 확보한 후 재시도를 한다.

  • 코드 라인 10~12에서 페이지를 회수하며 작은 확률로 회수한 페이지가 없는 경우 null을 반환한다.
  • 코드 라인 14~15에서 retry: 레이블에서 order 페이지 할당을 시도한다.
  • 코드 라인 21~26에서 페이지 할당이 실패하였고 첫 실패인 경우 highatomic 페이지 블럭을 해제하고,  pcp 캐시를 비워 버디시스템에 free 페이지를 확보한 후 재시도 한다.

 

다음 그림은 direct reclaim을 통해 페이지를 회수하는 과정을 보여준다.

 

__perform_reclaim()

mm/page_alloc.c

/* Perform direct synchronous page reclaim */
static int
__perform_reclaim(gfp_t gfp_mask, unsigned int order,
                                        const struct alloc_context *ac)
{
        struct reclaim_state reclaim_state;
        int progress;
        unsigned int noreclaim_flag;
        unsigned long pflags;

        cond_resched();

        /* We now go into synchronous reclaim */
        cpuset_memory_pressure_bump();
        psi_memstall_enter(&pflags);
        fs_reclaim_acquire(gfp_mask);
        noreclaim_flag = memalloc_noreclaim_save();
        reclaim_state.reclaimed_slab = 0;
        current->reclaim_state = &reclaim_state;

        progress = try_to_free_pages(ac->zonelist, order, gfp_mask,
                                                                ac->nodemask);

        current->reclaim_state = NULL;
        memalloc_noreclaim_restore(noreclaim_flag);
        fs_reclaim_release(gfp_mask);
        psi_memstall_leave(&pflags);

        cond_resched();

        return progress;
}

페이지를 회수한다. 반환되는 값은 회수한 페이지 수이다.

  • 코드 라인 14에서 전역 cpuset_memory_pressure_enabled가 설정된 경우 현재 태스크 cpuset의 frequency meter를 업데이트한다.
    • 루트 cpuset에 있는 memory_pressure_enabled 파일을 1로 설정하여 사용한다.
  •  코드 라인 15에서 메모리 압박이 시작되었음을 psi에 알린다.
  • 코드 라인 17에서 페이지 회수를 목적으로 잠시 페이지 할당이 필요하다. 이 때 다시 페이지 회수 루틴이 재귀 호출되지 않도록 방지하기 위해 reclaim을 하는 동안 잠시 현재 태스크의 플래그에 PF_MEMALLOC를 설정하여 워터 마크 기준을 없앤 후 할당할 수 있도록 한다.
  • 코드 라인 18~19에서 reclaimed_slab 카운터를 0으로 리셋하고, 현재 태스크에 지정한다.
  • 코드 라인 21~22에서 페이지를 회수하고 회수한 페이지 수를 알아온다.
  • 코드 라인 24에서 태스크에 지정한 reclaim_state를 해제한다.
  • 코드 라인 25에서 현재 태스크의 플래그에 reclaim을 하는 동안 잠시 설정해두었던 PF_MEMALLOC을 제거한다.
  • 코드 라인 27에서 메모리 압박이 완료되었음을 psi에 알린다.

 

Scan Control

스캔 컨트롤을 사용하는 루틴들은 다음과 같다.

  • reclaim_clean_pages_from_list()
  • try_to_free_pages()
  • mem_cgroup_shrink_node()
  • try_to_free_mem_cgroup_pages()
  • balance_pgdat()
  • shrink_all_memory()
  • __node_reclaim()

 

페이지 회수로 free 페이지 확보 시도

try_to_free_pages()

mm/vmscan.c

unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
                                gfp_t gfp_mask, nodemask_t *nodemask)
{
        unsigned long nr_reclaimed;
        struct scan_control sc = {
                .nr_to_reclaim = SWAP_CLUSTER_MAX,
                .gfp_mask = current_gfp_context(gfp_mask),
                .reclaim_idx = gfp_zone(gfp_mask),
                .order = order,
                .nodemask = nodemask,
                .priority = DEF_PRIORITY,
                .may_writepage = !laptop_mode,
                .may_unmap = 1,
                .may_swap = 1,
                .may_shrinkslab = 1,
        };

        /*
         * scan_control uses s8 fields for order, priority, and reclaim_idx.
         * Confirm they are large enough for max values.
         */
        BUILD_BUG_ON(MAX_ORDER > S8_MAX);
        BUILD_BUG_ON(DEF_PRIORITY > S8_MAX);
        BUILD_BUG_ON(MAX_NR_ZONES > S8_MAX);

        /*
         * Do not enter reclaim if fatal signal was delivered while throttled.
         * 1 is returned so that the page allocator does not OOM kill at this
         * point.
         */
        if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
                return 1;

        trace_mm_vmscan_direct_reclaim_begin(order,
                                sc.may_writepage,
                                sc.gfp_mask,
                                sc.reclaim_idx);

        nr_reclaimed = do_try_to_free_pages(zonelist, &sc);

        trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);

        return nr_reclaimed;
}

페이지 회수(Reclaim)를 시도하고 회수된 페이지 수를 반환한다. 유저 요청 시 free page가 normal 존 이하에서 min 워터마크 기준의 절반 이상을 확보할 때까지 태스크가 스로틀링(sleep)될 수 있다.

  • 코드 라인 5~16에서 페이지 회수를 위한 scan_control 구조체를 준비한다.
  • 코드 라인 31~32에서 direct-reclaim을 위해 일정 기준 이상 스로틀링 중 fatal 시그널을 전달 받은 경우 즉각 루틴을 빠져나간다. 단 1을 반환하므로 OOM kill 루틴을 수행하지 못하게 방지한다.
  • 코드 라인 39에서 페이지 회수를 시도한다.

 

유저 요청 시 스로틀링

throttle_direct_reclaim()

mm/vmscan.c

/*
 * Throttle direct reclaimers if backing storage is backed by the network
 * and the PFMEMALLOC reserve for the preferred node is getting dangerously
 * depleted. kswapd will continue to make progress and wake the processes
 * when the low watermark is reached.
 *
 * Returns true if a fatal signal was delivered during throttling. If this
 * happens, the page allocator should not consider triggering the OOM killer.
 */
static bool throttle_direct_reclaim(gfp_t gfp_mask, struct zonelist *zonelist,
                                        nodemask_t *nodemask)
{
        struct zoneref *z;
        struct zone *zone;
        pg_data_t *pgdat = NULL;

        /*
         * Kernel threads should not be throttled as they may be indirectly
         * responsible for cleaning pages necessary for reclaim to make forward
         * progress. kjournald for example may enter direct reclaim while
         * committing a transaction where throttling it could forcing other
         * processes to block on log_wait_commit().
         */
        if (current->flags & PF_KTHREAD)
                goto out;

        /*
         * If a fatal signal is pending, this process should not throttle.
         * It should return quickly so it can exit and free its memory
         */
        if (fatal_signal_pending(current))
                goto out;

        /*
         * Check if the pfmemalloc reserves are ok by finding the first node
         * with a usable ZONE_NORMAL or lower zone. The expectation is that
         * GFP_KERNEL will be required for allocating network buffers when
         * swapping over the network so ZONE_HIGHMEM is unusable.
         *
         * Throttling is based on the first usable node and throttled processes
         * wait on a queue until kswapd makes progress and wakes them. There
         * is an affinity then between processes waking up and where reclaim
         * progress has been made assuming the process wakes on the same node.
         * More importantly, processes running on remote nodes will not compete
         * for remote pfmemalloc reserves and processes on different nodes
         * should make reasonable progress.
         */
        for_each_zone_zonelist_nodemask(zone, z, zonelist,
                                        gfp_zone(gfp_mask), nodemask) {
                if (zone_idx(zone) > ZONE_NORMAL)
                        continue;

                /* Throttle based on the first usable node */
                pgdat = zone->zone_pgdat;
                if (allow_direct_reclaim(pgdat))
                        goto out;
                break;
        }

        /* If no zone was usable by the allocation flags then do not throttle */
        if (!pgdat)
                goto out;

        /* Account for the throttling */
        count_vm_event(PGSCAN_DIRECT_THROTTLE);

        /*
         * If the caller cannot enter the filesystem, it's possible that it
         * is due to the caller holding an FS lock or performing a journal
         * transaction in the case of a filesystem like ext[3|4]. In this case,
         * it is not safe to block on pfmemalloc_wait as kswapd could be
         * blocked waiting on the same lock. Instead, throttle for up to a
         * second before continuing.
         */
        if (!(gfp_mask & __GFP_FS)) {
                wait_event_interruptible_timeout(pgdat->pfmemalloc_wait,
                        allow_direct_reclaim(pgdat), HZ);

                goto check_pending;
        }

        /* Throttle until kswapd wakes the process */
        wait_event_killable(zone->zone_pgdat->pfmemalloc_wait,
                allow_direct_reclaim(pgdat));

check_pending:
        if (fatal_signal_pending(current))
                return true;

out:
        return false;
}

유저 태스크에서 direct-reclaim 요청 시 필요한 만큼 스로틀링한다. 파일 시스템을 사용하지 않는(nofs) direct-reclaim 요청인 경우 스로틀링은 1초로 제한된다. 스로틀링 중 sigkill 시그널 수신 여부를 반환한다.

  • 코드 라인 15~16에서 커널 스레드에서 요청한 경우 스로틀링을 하지 않기 위해 처리를 중단하고 false를 반환한다.
  • 코드 라인 22~23에서 SIGKILL 시그널이 처리되고 있는 태스크의 경우도 역시 처리를 중단하고 false를 반환한다.
  • 코드 라인 39~49에서 요청한 노드의 lowmem 존들의 direct-reclaim이 허용 기준 이상인 경우 스로틀링을 포기한다.
  • 코드 라인 52~53에서 사용할 수 있는 노드가 없는 경우 처리를 포기한다.
  • 코드 라인 56에서 스로틀링이 시작되는 구간이다. PGSCAN_DIRECT_THROTTLE stat을 증가시킨다.
  • 코드 라인 66~71에서 파일 시스템을 사용하지 않는 direct-reclaim 요청인 경우 direct-reclaim을 허락할 때까지 최대 1초간 스로틀링 후 check_pending 레이블로 이동한다.
  • 코드 라인74~75에서 파일 시스템을 사용하는 direct-reclaim의 경우 kswapd를 깨워 free page를 확보하며 direct-reclaim을 허락할 때까지 슬립한다.
  • 코드 라인 77~82에서 현재 태스크에 SIGKILL 시그널이 요청된 경우 true를 반환하고 그렇지 않은 경우 false를 반환한다.

 

다음 그림은 유저 요청 direct-reclaim 시 파일 시스템 사용 여부에 따라 direct-reclaim을 사용하기 위해 스로틀링하는 과정을 보여준다.

 

direct-reclaim 허락 여부

allow_direct_reclaim()

mm/vmscan.c

static bool allow_direct_reclaim(pg_data_t *pgdat)
{
        struct zone *zone;
        unsigned long pfmemalloc_reserve = 0;
        unsigned long free_pages = 0;
        int i;
        bool wmark_ok;

        if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
                return true;

        for (i = 0; i <= ZONE_NORMAL; i++) {
                zone = &pgdat->node_zones[i];
                if (!managed_zone(zone))
                        continue;

                if (!zone_reclaimable_pages(zone))
                        continue;

                pfmemalloc_reserve += min_wmark_pages(zone);
                free_pages += zone_page_state(zone, NR_FREE_PAGES);
        }

        /* If there are no reserves (unexpected config) then do not throttle */
        if (!pfmemalloc_reserve)
                return true;

        wmark_ok = free_pages > pfmemalloc_reserve / 2;

        /* kswapd must be awake if processes are being throttled */
        if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) {
                pgdat->kswapd_classzone_idx = min(pgdat->kswapd_classzone_idx,
                                                (enum zone_type)ZONE_NORMAL);
                wake_up_interruptible(&pgdat->kswapd_wait);
        }

        return wmark_ok;
}

요청한 노드에서 direct-reclaim을 허락하는지 여부를 반환한다. 만일 lowmem 존들의 free 페이지가 min 워터마크 50% 이하인 경우 현재 태스크를 슬립하고, kswapd를 깨운 뒤 false를 반환한다. 그리고 그 이상인 경우 direct-reclaim을 시도해도 좋다고 판단하여 true를 반환한다.

  • 코드 라인 9~10에서 reclaim 실패 횟수가 MAX_RECLAIM_RETRIES(16)번 이상일 때 스로틀을 하지못하게 곧바로 true를 반환한다.
  • 코드 라인 12~22에서 lowmem 존들의 min 워터마크를 합산한 pfmemalloc_reserve 값 및 free 페이지 수의 합산 값을 구한다.
  • 코드 라인 25~26에서 pfmemalloc_reserve 값이 0인 경우 스로틀을 하지 못하게 곧바로 true를 반환한다.
  • 코드 라인 28~35에서 free 페이지 합산 수가 lowmem 존들의 min 워터마크 합산 값의 50% 이하이면 현재 태스크를 슬립시키고 kswapd를 깨운 뒤 false를 반환한다.

 

다음 그림은 direct-reclaim 허락 여부를 알아오는 과정을 보여준다.

 

do_try_to_free_pages()

mm/vmscan.c

/*
 * This is the main entry point to direct page reclaim.
 *
 * If a full scan of the inactive list fails to free enough memory then we
 * are "out of memory" and something needs to be killed.
 *
 * If the caller is !__GFP_FS then the probability of a failure is reasonably
 * high - the zone may be full of dirty or under-writeback pages, which this
 * caller can't do much about.  We kick the writeback threads and take explicit
 * naps in the hope that some of these pages can be written.  But if the
 * allocating task holds filesystem locks which prevent writeout this might not
 * work, and the allocation attempt will fail.
 *
 * returns:     0, if no pages reclaimed
 *              else, the number of pages reclaimed
 */
static unsigned long do_try_to_free_pages(struct zonelist *zonelist,
                                          struct scan_control *sc)
{
        int initial_priority = sc->priority;
        pg_data_t *last_pgdat;
        struct zoneref *z;
        struct zone *zone;
retry:
        delayacct_freepages_start();

        if (global_reclaim(sc))
                __count_zid_vm_events(ALLOCSTALL, sc->reclaim_idx, 1);

        do {
                vmpressure_prio(sc->gfp_mask, sc->target_mem_cgroup,
                                sc->priority);
                sc->nr_scanned = 0;
                shrink_zones(zonelist, sc);

                if (sc->nr_reclaimed >= sc->nr_to_reclaim)
                        break;

                if (sc->compaction_ready)
                        break;

                /*
                 * If we're getting trouble reclaiming, start doing
                 * writepage even in laptop mode.
                 */
                if (sc->priority < DEF_PRIORITY - 2)
                        sc->may_writepage = 1;
        } while (--sc->priority >= 0);

        last_pgdat = NULL;
        for_each_zone_zonelist_nodemask(zone, z, zonelist, sc->reclaim_idx,
                                        sc->nodemask) {
                if (zone->zone_pgdat == last_pgdat)
                        continue;
                last_pgdat = zone->zone_pgdat;
                snapshot_refaults(sc->target_mem_cgroup, zone->zone_pgdat);
                set_memcg_congestion(last_pgdat, sc->target_mem_cgroup, false);
        }

        delayacct_freepages_end();

        if (sc->nr_reclaimed)
                return sc->nr_reclaimed;

        /* Aborted reclaim to try compaction? don't OOM, then */
        if (sc->compaction_ready)
                return 1;

        /* Untapped cgroup reserves?  Don't OOM, retry. */
        if (sc->memcg_low_skipped) {
                sc->priority = initial_priority;
                sc->memcg_low_reclaim = 1;
                sc->memcg_low_skipped = 0;
                goto retry;
        }

        return 0;
}

direct-reclaim 요청을 통해 페이지를 회수하여 free 페이지를 확보를 시도한다.

  • 코드 라인 8~9에서 retry: 레이블이다. 페이지 회수에 소요되는 시간을 계량하기 위해 시작한다.
  • 코드 라인 11~12에서 global reclaim을 사용해야하는 경우 ALLOCSTALL stat을 증가시킨다.
  • 코드 라인 14~16에서 루프를 돌며 우선 순위가 높아져 스캔 depth가 깊어지는 경우 vmpressure 정보를 갱신한다.
  • 코드 라인 17~18에서 스캔 건 수를 리셋시키고 페이지를 회수하고 회수한 건 수를 알아온다.
  • 코드 라인 20~21에서 회수 건 수가 회수해야 할 건 수보다 큰 경우 처리를 위해 루프에서 벗어난다.
  • 코드 라인 23~24에서 compaction이 준비된 경우 처리를 위해 루프에서 벗어난다.
  • 코드 라인 30~31에서 우선 순위를 2 단계 더 높여 처리하는 경우 writepage 기능을 설정한다.
  • 코드 라인 32에서 우선 순위를 최고까지 높여가며(0으로 갈수록 높아진다) 루프를 돈다.
  • 코드 라인 34~42에서 zonelist를 순회하며 노드에 대해 노드 또는 memcg lru의 refaults를 갱신하고 memcg 노드의 congested를 false로 리셋한다.
  • 코드 라인 44에서 페이지 회수에 소요되는 시간을 계량한다.
  • 코드 라인 46~47네거 회수한 적이 있는 경우 그 값을 반환한다.
  • 코드 라인 50~51에서 compaction이 준비된 경우 1을 반환한다.
  • 코드 라인 54~59에서 sc->memcg_low_skipped가 설정된 경우 처음 재시도에 한해 priority를 다시 원래 요청 priority로 바꾸고 재시도한다.

 

global_reclaim()

mm/vmscan.c

#ifdef CONFIG_MEMCG
static bool global_reclaim(struct scan_control *sc)
{
        return !sc->target_mem_cgroup;
}
#else
static bool global_reclaim(struct scan_control *sc)
{
        return true;
}
#endif

CONFIG_MEMCG 커널 옵션을 사용하여 Memory Control Group을 사용하는 경우 scan_control의 target_mem_cgroup이 정해진 경우 false를 반환한다. 그렇지 않은 경우 global reclaim을 위해 true를 반환한다. CONFIG_MEMCG를 사용하지 않는 경우 항상 true이다.

 


Memory Pressure (per-cpuset reclaims)

cpuset_memory_pressure_bump()

include/linux/cpuset.h

#define cpuset_memory_pressure_bump()                           \
        do {                                                    \
                if (cpuset_memory_pressure_enabled)             \
                        __cpuset_memory_pressure_bump();        \
        } while (0)

현재 태스크 cpuset의 frequency meter를 업데이트한다.

 

__cpuset_memory_pressure_bump()

kernel/cpuset.c

/**
 * cpuset_memory_pressure_bump - keep stats of per-cpuset reclaims.
 *
 * Keep a running average of the rate of synchronous (direct)
 * page reclaim efforts initiated by tasks in each cpuset.
 *
 * This represents the rate at which some task in the cpuset
 * ran low on memory on all nodes it was allowed to use, and
 * had to enter the kernels page reclaim code in an effort to
 * create more free memory by tossing clean pages or swapping
 * or writing dirty pages.
 *
 * Display to user space in the per-cpuset read-only file
 * "memory_pressure".  Value displayed is an integer
 * representing the recent rate of entry into the synchronous
 * (direct) page reclaim by any task attached to the cpuset.
 **/
void __cpuset_memory_pressure_bump(void)
{
        rcu_read_lock();
        fmeter_markevent(&task_cs(current)->fmeter);
        rcu_read_unlock();
}

현재 태스크 cpuset의 frequency meter를 업데이트한다.

 

fmeter_markevent()

kernel/cpuset.c

/* Process any previous ticks, then bump cnt by one (times scale). */
static void fmeter_markevent(struct fmeter *fmp)
{
        spin_lock(&fmp->lock);
        fmeter_update(fmp);
        fmp->cnt = min(FM_MAXCNT, fmp->cnt + FM_SCALE);
        spin_unlock(&fmp->lock);
}

요청한 frequency meter를 업데이트하고 다음 계산을 위해 이벤트 수에 1,000을 대입하되 최대 1,000,000을 넘기지 않게 한다.

 

fmeter_update()

kernel/cpuset.c

/* Internal meter update - process cnt events and update value */
static void fmeter_update(struct fmeter *fmp)
{
        time_t now = get_seconds();
        time_t ticks = now - fmp->time;

        if (ticks == 0)
                return;

        ticks = min(FM_MAXTICKS, ticks);
        while (ticks-- > 0)
                fmp->val = (FM_COEF * fmp->val) / FM_SCALE;
        fmp->time = now;

        fmp->val += ((FM_SCALE - FM_COEF) * fmp->cnt) / FM_SCALE;
        fmp->cnt = 0;
}

요청한 frequency meter로 val 값을 계산하고 이벤트 수를 0으로 리셋한다.

  • 코드 라인 4~8에서 fmeter에 기록된 초(second)로부터 경과한 초를 알아온다.
  • 코드 라인 10~12에서 ticks는 최대 99까지로 제한하고, ticks 만큼 fmp->val *= 93.3%를 반복한다.
  • 코드 라인 13에서 다음 계산을 위해 현재 초로 갱신한다.
  • 코드 라인 15~16에서 fmp->val에 fmp->cnt x 6.7%를 더한 후 이벤트 수를 0으로 리셋한다.

 

fmeter 구조체

kernel/cgroup/cpuset.c

struct fmeter {
        int cnt;                /* unprocessed events count */
        int val;                /* most recent output value */
        time_t time;            /* clock (secs) when val computed */
        spinlock_t lock;        /* guards read or write of above */
};
  • cnt
    • 처리되지 않은 이벤트 수
  • val
    • 최근 fmeter 업데이트 시 계산된 값
  • time
    • val 값이 계산될 때의 clock(secs)

 

참고

 

Zoned Allocator -13- (Direct Reclaim-Shrink-2)

<kernel v5.0>

Zoned Allocator -13- (Direct Reclaim-Shrink-2)

 

shrink_page_list()

mm/vmscan.c -1/6-

/*
 * shrink_page_list() returns the number of reclaimed pages
 */
static unsigned long shrink_page_list(struct list_head *page_list,
                                      struct pglist_data *pgdat,
                                      struct scan_control *sc,
                                      enum ttu_flags ttu_flags,
                                      struct reclaim_stat *stat,
                                      bool force_reclaim)
{
        LIST_HEAD(ret_pages);
        LIST_HEAD(free_pages);
        int pgactivate = 0;
        unsigned nr_unqueued_dirty = 0;
        unsigned nr_dirty = 0;
        unsigned nr_congested = 0;
        unsigned nr_reclaimed = 0;
        unsigned nr_writeback = 0;
        unsigned nr_immediate = 0;
        unsigned nr_ref_keep = 0;
        unsigned nr_unmap_fail = 0;

        cond_resched();

        while (!list_empty(page_list)) {
                struct address_space *mapping;
                struct page *page;
                int may_enter_fs;
                enum page_references references = PAGEREF_RECLAIM_CLEAN;
                bool dirty, writeback;

                cond_resched();

                page = lru_to_page(page_list);
                list_del(&page->lru);

                if (!trylock_page(page))
                        goto keep;

                VM_BUG_ON_PAGE(PageActive(page), page);

                sc->nr_scanned++;

                if (unlikely(!page_evictable(page)))
                        goto activate_locked;

                if (!sc->may_unmap && page_mapped(page))
                        goto keep_locked;

                /* Double the slab pressure for mapped and swapcache pages */
                if ((page_mapped(page) || PageSwapCache(page)) &&
                    !(PageAnon(page) && !PageSwapBacked(page)))
                        sc->nr_scanned++;

                may_enter_fs = (sc->gfp_mask & __GFP_FS) ||
                        (PageSwapCache(page) && (sc->gfp_mask & __GFP_IO));

                /*
                 * The number of dirty pages determines if a node is marked
                 * reclaim_congested which affects wait_iff_congested. kswapd
                 * will stall and start writing pages if the tail of the LRU
                 * is all dirty unqueued pages.
                 */
                page_check_dirty_writeback(page, &dirty, &writeback);
                if (dirty || writeback)
                        nr_dirty++;

                if (dirty && !writeback)
                        nr_unqueued_dirty++;

                /*
                 * Treat this page as congested if the underlying BDI is or if
                 * pages are cycling through the LRU so quickly that the
                 * pages marked for immediate reclaim are making it to the
                 * end of the LRU a second time.
                 */
                mapping = page_mapping(page);
                if (((dirty || writeback) && mapping &&
                     inode_write_congested(mapping->host)) ||
                    (writeback && PageReclaim(page)))
                        nr_congested++;

isolation 후 전달받은 @page_list의 페이지들에 대해 shrink를 수행하고 회수된 페이지의 수를 반환한다.

  • 코드 라인 8에서 회수되지 않고 남은 페이지들을 담기위해 임시로 사용되는 ret_pages 리스트를 초기화한다.
  • 코드 라인 9에서 회수를 위해 임시로 사용되는 free_pages 리스트를 초기화한다.
  • 코드 라인 22~32에서 page_list의 페이지 수 만큼 순회하며 페이지를 가져온다.
  • 코드 라인 34~35에서 페이지 lock 획득이 실패하는 경우 다음에 처리하도록 lru로 되돌리기 위해 keep 레이블로 이동한다.
  • 코드 라인 41~42에서 작은 확률로 페이지가 evictable 페이지 상태가 아닌 경우 active lru로 되돌리기 위해 activate_locked 레이블로 이동한다.
  • 코드 라인 44~45에서 sc->may_unmap 요청인 경우 매핑된 페이지는 처리하지 않고 lru로 되돌리기 위해 keep 레이블로 이동한다.
  • 코드 라인 48~50에서 pte 매핑된 페이지 또는 swap 캐시인 경우 nr_scanned를 증가시킨다. 단 swap 영역을 자지지 않는 clean anon 페이지는 제외한다.
  • 코드 라인 51~52에서 이 페이지의 처리에 fs 사용 가능 여부를 알아온다. fs 허용하였거나 swap 캐시이면서 IO 사용 가능한 상태도 fs 사용 가능한 상태이다.
  • 코드 라인 61~63에서 dirty 및  writeback 페이지인지 여부를 알아오고 nr_dirty 카운터를 증가시킨다.
  • 코드 라인 65~66에서 writeback 큐잉되지 않은 dirty 페이지인 경우 nr_unqueued_dirty 카운터를 증가시킨다.
  • 코드 라인 74~78에서 write가 혼잡한 상태이거나 페이지가 writeback을 통해 회수가 진행되는 페이지인 경우 nr_congested를 증가시킨다.

 

mm/vmscan.c -2/6-

.               /*
                 * If a page at the tail of the LRU is under writeback, there
                 * are three cases to consider.
                 *
                 * 1) If reclaim is encountering an excessive number of pages
                 *    under writeback and this page is both under writeback and
                 *    PageReclaim then it indicates that pages are being queued
                 *    for IO but are being recycled through the LRU before the
                 *    IO can complete. Waiting on the page itself risks an
                 *    indefinite stall if it is impossible to writeback the
                 *    page due to IO error or disconnected storage so instead
                 *    note that the LRU is being scanned too quickly and the
                 *    caller can stall after page list has been processed.
                 *
                 * 2) Global or new memcg reclaim encounters a page that is
                 *    not marked for immediate reclaim, or the caller does not
                 *    have __GFP_FS (or __GFP_IO if it's simply going to swap,
                 *    not to fs). In this case mark the page for immediate
                 *    reclaim and continue scanning.
                 *
                 *    Require may_enter_fs because we would wait on fs, which
                 *    may not have submitted IO yet. And the loop driver might
                 *    enter reclaim, and deadlock if it waits on a page for
                 *    which it is needed to do the write (loop masks off
                 *    __GFP_IO|__GFP_FS for this reason); but more thought
                 *    would probably show more reasons.
                 *
                 * 3) Legacy memcg encounters a page that is already marked
                 *    PageReclaim. memcg does not have any dirty pages
                 *    throttling so we could easily OOM just because too many
                 *    pages are in writeback and there is nothing else to
                 *    reclaim. Wait for the writeback to complete.
                 *
                 * In cases 1) and 2) we activate the pages to get them out of
                 * the way while we continue scanning for clean pages on the
                 * inactive list and refilling from the active list. The
                 * observation here is that waiting for disk writes is more
                 * expensive than potentially causing reloads down the line.
                 * Since they're marked for immediate reclaim, they won't put
                 * memory pressure on the cache working set any longer than it
                 * takes to write them to disk.
                 */
                if (PageWriteback(page)) {
                        /* Case 1 above */
                        if (current_is_kswapd() &&
                            PageReclaim(page) &&
                            test_bit(PGDAT_WRITEBACK, &pgdat->flags)) {
                                nr_immediate++;
                                goto activate_locked;

                        /* Case 2 above */
                        } else if (sane_reclaim(sc) ||
                            !PageReclaim(page) || !may_enter_fs) {
                                /*
                                 * This is slightly racy - end_page_writeback()
                                 * might have just cleared PageReclaim, then
                                 * setting PageReclaim here end up interpreted
                                 * as PageReadahead - but that does not matter
                                 * enough to care.  What we do want is for this
                                 * page to have PageReclaim set next time memcg
                                 * reclaim reaches the tests above, so it will
                                 * then wait_on_page_writeback() to avoid OOM;
                                 * and it's also appropriate in global reclaim.
                                 */
                                SetPageReclaim(page);
                                nr_writeback++;
                                goto activate_locked;

                        /* Case 3 above */
                        } else {
                                unlock_page(page);
                                wait_on_page_writeback(page);
                                /* then go back and try same page again */
                                list_add_tail(&page->lru, page_list);
                                continue;
                        }
                }
  • 코드 라인 43에서 writeback 페이지에 대한 처리이다.
  • 코드 라인 45~49에서 첫 번째 writeback 케이스: kswapd에서 회수 중인 페이지가 다시 돌아온 경우이다. 이러한 경우 처리 시간을 좀 더 주기위해 nr_immediate 카운터를 증가시키고, activate 처리 후 lru로 되돌리기 위해 activate_locked 레이블로 이동한다.
  • 코드 라인 52~67에서 두 번째 writeback 케이스: memcg를 통해 writeback을 하거나 아직 회수 중인 페이지가 아니거나 fs 사용 불가능한 상태인 경우 즉각 회수를 위해 reclaim 플래그를 설정하고, nr_writeback 카운터를 증가시킨다. 그런 후 activate 처리 후 lru로 되돌리기 위해 activate_locked 레이블로 이동한다.
  • 코드 라인 70~76에서 세 번째 writeback 케이스: 해당 페이지의 writeback이 완료될 때 까지 기다린 후 page_list에 추가하고 계속한다.

 

mm/vmscan.c -3/6-

.               if (!force_reclaim)
                        references = page_check_references(page, sc);

                switch (references) {
                case PAGEREF_ACTIVATE:
                        goto activate_locked;
                case PAGEREF_KEEP:
                        nr_ref_keep++;
                        goto keep_locked;
                case PAGEREF_RECLAIM:
                case PAGEREF_RECLAIM_CLEAN:
                        ; /* try to reclaim the page below */
                }

                /*
                 * Anonymous process memory has backing store?
                 * Try to allocate it some swap space here.
                 * Lazyfree page could be freed directly
                 */
                if (PageAnon(page) && PageSwapBacked(page)) {
                        if (!PageSwapCache(page)) {
                                if (!(sc->gfp_mask & __GFP_IO))
                                        goto keep_locked;
                                if (PageTransHuge(page)) {
                                        /* cannot split THP, skip it */
                                        if (!can_split_huge_page(page, NULL))
                                                goto activate_locked;
                                        /*
                                         * Split pages without a PMD map right
                                         * away. Chances are some or all of the
                                         * tail pages can be freed without IO.
                                         */
                                        if (!compound_mapcount(page) &&
                                            split_huge_page_to_list(page,
                                                                    page_list))
                                                goto activate_locked;
                                }
                                if (!add_to_swap(page)) {
                                        if (!PageTransHuge(page))
                                                goto activate_locked;
                                        /* Fallback to swap normal pages */
                                        if (split_huge_page_to_list(page,
                                                                    page_list))
                                                goto activate_locked;
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
                                        count_vm_event(THP_SWPOUT_FALLBACK);
#endif
                                        if (!add_to_swap(page))
                                                goto activate_locked;
                                }

                                may_enter_fs = 1;

                                /* Adding to swap updated mapping */
                                mapping = page_mapping(page);
                        }
                } else if (unlikely(PageTransHuge(page))) {
                        /* Split file THP */
                        if (split_huge_page_to_list(page, page_list))
                                goto keep_locked;
                }

                /*
                 * The page is mapped into the page tables of one or more
                 * processes. Try to unmap it here.
                 */
                if (page_mapped(page)) {
                        enum ttu_flags flags = ttu_flags | TTU_BATCH_FLUSH;

                        if (unlikely(PageTransHuge(page)))
                                flags |= TTU_SPLIT_HUGE_PMD;
                        if (!try_to_unmap(page, flags)) {
                                nr_unmap_fail++;
                                goto activate_locked;
                        }
                }
  • 코드 라인 1~13에서 @force_reclaim이 true인 경우 reclaim을 강제하고, 그렇지 않은 경우 페이지 참조 체크 결과에 따라 다음 중 하나로 진행한다.
    • swapback 중인 페이지, 2 번 이상 참조된 페이지, 실행 파일 페이지가 참조된 경우 activate lru로 되돌리기 위해 activate_locked 레이블로 이동한다.
    • 기타 페이지가 참조된 경우 inactivate lru로 되돌리기 위해 keep_locked 레이블로 이동한다.
    • 그 외의 경우 페이지 회수를 진행하기 위해 코드를 계속 진행한다.
  • 코드 라인 20~56에서 swap 영역 사용 가능한 normal anon 페이지를 처리한다. 만일 swap 캐시가 아직 없을 때에 다음과 같이 수행한다.
    • io 처리 금지된 상태라면 keep_locked 레이블로 이동시킨다.
    • thp인 경우 split이 불가능하면 activate_locked 레이블로 이동시키고, 페이지를 split한 order 0 페이지들을 page_list에 추가하고 activate_locked 레이블로 이동한다.
    • 페이지를 swap 큐에 추가한다. 만일 추가할 수 없는 경우 thp이면 split한 order 0페이지를 page_list에 추가한다.
  • 코드 라인 57~61에서 swap 영역에 지정된 anon 페이지가 아니면서 thp인 경우 페이지를 split한 order 0 페이지들을 page_list에 추가하고 keep_locked 레이블로 이동한다.
  • 코드 라인 67~76에서 매핑된 페이지에 대해 언매핑을 수행한다. 언매핑이 실패한 경우 activate lru로 되돌리기 위해 activate_locked로 이동한다.

 

mm/vmscan.c -4/6-

.               if (PageDirty(page)) {
                        /*
                         * Only kswapd can writeback filesystem pages
                         * to avoid risk of stack overflow. But avoid
                         * injecting inefficient single-page IO into
                         * flusher writeback as much as possible: only
                         * write pages when we've encountered many
                         * dirty pages, and when we've already scanned
                         * the rest of the LRU for clean pages and see
                         * the same dirty pages again (PageReclaim).
                         */
                        if (page_is_file_cache(page) &&
                            (!current_is_kswapd() || !PageReclaim(page) ||
                             !test_bit(PGDAT_DIRTY, &pgdat->flags))) {
                                /*
                                 * Immediately reclaim when written back.
                                 * Similar in principal to deactivate_page()
                                 * except we already have the page isolated
                                 * and know it's dirty
                                 */
                                inc_node_page_state(page, NR_VMSCAN_IMMEDIATE);
                                SetPageReclaim(page);

                                goto activate_locked;
                        }

                        if (references == PAGEREF_RECLAIM_CLEAN)
                                goto keep_locked;
                        if (!may_enter_fs)
                                goto keep_locked;
                        if (!sc->may_writepage)
                                goto keep_locked;

                        /*
                         * Page is dirty. Flush the TLB if a writable entry
                         * potentially exists to avoid CPU writes after IO
                         * starts and then write it out here.
                         */
                        try_to_unmap_flush_dirty();
                        switch (pageout(page, mapping, sc)) {
                        case PAGE_KEEP:
                                goto keep_locked;
                        case PAGE_ACTIVATE:
                                goto activate_locked;
                        case PAGE_SUCCESS:
                                if (PageWriteback(page))
                                        goto keep;
                                if (PageDirty(page))
                                        goto keep;

                                /*
                                 * A synchronous write - probably a ramdisk.  Go
                                 * ahead and try to reclaim the page.
                                 */
                                if (!trylock_page(page))
                                        goto keep;
                                if (PageDirty(page) || PageWriteback(page))
                                        goto keep_locked;
                                mapping = page_mapping(page);
                        case PAGE_CLEAN:
                                ; /* try to free the page below */
                        }
                }
  • 코드 라인 1에서 dirty 페이지인 경우의 처리이다.
  • 코드 라인 12~25에서 dirty된 file 캐시는 kswapd에서만 pageout()을 사용할 예정이다. 따라서 file 캐시 페이지이면서 kswapd가 아닌 경우에는 reclaim 플래그를 설정한 후 activate lru로 되돌리기 위해 activate_locked 레이블로 이동한다.
  • 코드 라인 27~32에서 fs를 사용하지 못하거나, write 금지 상황이거나, 페이지 참조 체크가 clean 상태인 경우 lru로 되돌리기 위해 keep_locked 레이블로 이동한다.
  • 코드 라인 39에서 writable 페이지의 경우 TLB를 플러시하여 IO 시작 후 cpu가 기록하는 일이 없도록 방지한다.
  • 코드 라인 40~62에서 dirty 페이지를 pageout() 함수를 통해 파일 시스템에 기록하도록 요청한다. clean 결과를 얻으면 페이지를 free 하기위해 아래 코드를 계속 진행하고, 나머지는 페이지 상황에 따라 active 또는 inactive lru로 되돌린다.

 

mm/vmscan.c -5/6-

.               /*
                 * If the page has buffers, try to free the buffer mappings
                 * associated with this page. If we succeed we try to free
                 * the page as well.
                 *
                 * We do this even if the page is PageDirty().
                 * try_to_release_page() does not perform I/O, but it is
                 * possible for a page to have PageDirty set, but it is actually
                 * clean (all its buffers are clean).  This happens if the
                 * buffers were written out directly, with submit_bh(). ext3
                 * will do this, as well as the blockdev mapping.
                 * try_to_release_page() will discover that cleanness and will
                 * drop the buffers and mark the page clean - it can be freed.
                 *
                 * Rarely, pages can have buffers and no ->mapping.  These are
                 * the pages which were not successfully invalidated in
                 * truncate_complete_page().  We try to drop those buffers here
                 * and if that worked, and the page is no longer mapped into
                 * process address space (page_count == 1) it can be freed.
                 * Otherwise, leave the page on the LRU so it is swappable.
                 */
                if (page_has_private(page)) {
                        if (!try_to_release_page(page, sc->gfp_mask))
                                goto activate_locked;
                        if (!mapping && page_count(page) == 1) {
                                unlock_page(page);
                                if (put_page_testzero(page))
                                        goto free_it;
                                else {
                                        /*
                                         * rare race with speculative reference.
                                         * the speculative reference will free
                                         * this page shortly, so we may
                                         * increment nr_reclaimed here (and
                                         * leave it off the LRU).
                                         */
                                        nr_reclaimed++;
                                        continue;
                                }
                        }
                }

                if (PageAnon(page) && !PageSwapBacked(page)) {
                        /* follow __remove_mapping for reference */
                        if (!page_ref_freeze(page, 1))
                                goto keep_locked;
                        if (PageDirty(page)) {
                                page_ref_unfreeze(page, 1);
                                goto keep_locked;
                        }

                        count_vm_event(PGLAZYFREED);
                        count_memcg_page_event(page, PGLAZYFREED);
                } else if (!mapping || !__remove_mapping(mapping, page, true))
                        goto keep_locked;

                unlock_page(page);
  • 코드 라인 22~41에서 파일 시스템에 별도의 버퍼를 가진 private 페이지인 경우 버퍼를 해제한다. 만일 버퍼 해제가 실패한 경우 다시 active lru로 되돌리기 위해 activate_locked 레이블로 이동한다. 매핑되지 않았거나 사용되지 않으면 페이지를 회수하기 위해 free_it 레이블로 이동한다. 경쟁 상황에서 드물게 이미 free되고 있는 페이지인 경우 nr_reclaimed 카운트를 증가하고 다음 페이지를 처리하도록 한다.
  • 코드 라인 43~53에서 swap 영역을 사용하지 못하는 clean anon 페이지인 경우이다. 다음 순서대로 처리한다.
    • 사용자가 없으면 참조 카운터를 0으로 변경한다. 만일 아직 사용자가 있으면 keep_locked 레이블로 이동한다.
    • dirty 페이지인 경우 참조 카운터를 1로 변경하고, lru로 되돌리기 위해 keep_locked 레이블로 이동한다.
    • 마지막으로 PGLAZYFREED 카운터를 증가시킨다. 다음 free_it: 레이블로 이어지는 코드를 통해 회수될 예정이다.
  • 코드 라인 54~55에서 swapbacked 되지 않은 anon 페이지도 아니면서 매핑이 없거나 매핑을 제거할 수 없으면 lru로 되돌리기 위해 keep_locked 레이블로 이동한다.

 

mm/vmscan.c -6/6-

free_it:
                nr_reclaimed++;

                /*
                 * Is there need to periodically free_page_list? It would
                 * appear not as the counts should be low
                 */
                if (unlikely(PageTransHuge(page))) {
                        mem_cgroup_uncharge(page);
                        (*get_compound_page_dtor(page))(page);
                } else
                        list_add(&page->lru, &free_pages);
                continue;

activate_locked:
                /* Not a candidate for swapping, so reclaim swap space. */
                if (PageSwapCache(page) && (mem_cgroup_swap_full(page) ||
                                                PageMlocked(page)))
                        try_to_free_swap(page);
                VM_BUG_ON_PAGE(PageActive(page), page);
                if (!PageMlocked(page)) {
                        SetPageActive(page);
                        pgactivate++;
                        count_memcg_page_event(page, PGACTIVATE);
                }
keep_locked:
                unlock_page(page);
keep:
                list_add(&page->lru, &ret_pages);
                VM_BUG_ON_PAGE(PageLRU(page) || PageUnevictable(page), page);
        }

        mem_cgroup_uncharge_list(&free_pages);
        try_to_unmap_flush();
        free_unref_page_list(&free_pages);

        list_splice(&ret_pages, page_list);
        count_vm_events(PGACTIVATE, pgactivate);

        if (stat) {
                stat->nr_dirty = nr_dirty;
                stat->nr_congested = nr_congested;
                stat->nr_unqueued_dirty = nr_unqueued_dirty;
                stat->nr_writeback = nr_writeback;
                stat->nr_immediate = nr_immediate;
                stat->nr_activate = pgactivate;
                stat->nr_ref_keep = nr_ref_keep;
                stat->nr_unmap_fail = nr_unmap_fail;
        }
        return nr_reclaimed;
}
  • 코드 라인 1~13에서 free_it: 레이블이다.  이 곳에서는 회수될 페이지를 free_pages 리스트에 추가하고 다음 페이지를 반복한다. 만일 thp의 경우 memcg에도 보고하고 free_transhuge_page() 함수를 호출하여 order 0 페이지로 분해한다.
  • 코드 라인 15~25에서 activate_locked: 레이블이다. 이 곳에서는 페이지를 active 설정한다.  만일 swap 캐시 페이지가 memcg swap 공간이 full 상태이거나 mlocked 페이지인 상태인 경우 swap 영역을 비우도록 한다. 그리고 mlocked 페이지가 아닌 경우 actvie 설정하고 아래 keep_locked: 레이블을 계속 진행한다.
  • 코드 라인 26~27에서 keep_locked: 레이블이다.  페이지를 unlock 하고 아래 keep: 레이블을 계속 진행한다.
  • 코드 라인 28~31에서 keep: 레이블이다. 이 곳에서는 페이지를 ret_pages 리스트에 추가하고 다음 페이지를 반복한다.
  • 코드 라인 33~35에서 루프를 모두 완료하면 free_pages 리스트들의 페이지를 memcg에 uncharge 보고하고, pcp에 회수시킨다.
  • 코드 라인 37~38에서 ret_pages 리스트는 @page_list의 선두로 다시 되돌리고(rotate) PGACTIVATE 카운트를 증가시킨다.
  • 코드 라인 40~50에서 회수와 관련된 카운터들을 갱신하고, 회수된 페이지 수를 반환한다.

 

다음 그림은 스캔하여 isolation한 page_list를 대상으로 페이지를 회수하여 free page를 확보하는 흐름을 보여준다.

  • activate 화살표에서는 페이지에 PG_active 플래그를 설정한 후 lru로 되돌린다.
  • keep 화살표에서는 lru로 되돌린다.
  • free 화살표에서는 free 페이지를 버디 시스템으로 회수한다.

 


페이지의 dirty & writeback 상태 체크

page_check_dirty_writeback()

mm/vmscan.c

/* Check if a page is dirty or under writeback */
static void page_check_dirty_writeback(struct page *page,
                                       bool *dirty, bool *writeback)
{
        struct address_space *mapping;

        /*
         * Anonymous pages are not handled by flushers and must be written
         * from reclaim context. Do not stall reclaim based on them
         */
        if (!page_is_file_cache(page) ||
            (PageAnon(page) && !PageSwapBacked(page))) {
                *dirty = false;
                *writeback = false;
                return;
        }

        /* By default assume that the page flags are accurate */
        *dirty = PageDirty(page);
        *writeback = PageWriteback(page);

        /* Verify dirty/writeback state if the filesystem supports it */
        if (!page_has_private(page))
                return;

        mapping = page_mapping(page);
        if (mapping && mapping->a_ops->is_dirty_writeback)
                mapping->a_ops->is_dirty_writeback(page, dirty, writeback);
}

페이지의 dirty 및 writeback 여부를 알아온다.

  • 코드 라인 11~16에서 file 캐시가 아니거나, swap 영역을 사용할 수 없는 anon 페이지인 경우 출력 인수 dirty와 writeback에 false를 담고 함수를 종료한다.
    • swapbacked 여부와 상관없는 anon 페이지
  • 코드 라인 19~20에서 dirty 및 writeback 플래그 상태를 저장한다.
  • 코드 라인23~24에서별도의 버퍼를 갖는 private 페이지가 아닌 경우 함수를 빠져나간다.
  • 코드 라인26~28에서 mapping 페이지의 경우 is_dirty_writeback() 핸들러 함수를 통해 dirty 및 writeback 여부를 알아온다.

 

페이지의 참조 상태 체크

page_check_references()

mm/vmscan.c

static enum page_references page_check_references(struct page *page,
                                                  struct scan_control *sc)
{
        int referenced_ptes, referenced_page;
        unsigned long vm_flags;

        referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup,
                                          &vm_flags);
        referenced_page = TestClearPageReferenced(page);

        /*
         * Mlock lost the isolation race with us.  Let try_to_unmap()
         * move the page to the unevictable list.
         */
        if (vm_flags & VM_LOCKED)
                return PAGEREF_RECLAIM;

        if (referenced_ptes) {
                if (PageSwapBacked(page))
                        return PAGEREF_ACTIVATE;
                /*
                 * All mapped pages start out with page table
                 * references from the instantiating fault, so we need
                 * to look twice if a mapped file page is used more
                 * than once.
                 *
                 * Mark it and spare it for another trip around the
                 * inactive list.  Another page table reference will
                 * lead to its activation.
                 *
                 * Note: the mark is set for activated pages as well
                 * so that recently deactivated but used pages are
                 * quickly recovered.
                 */
                SetPageReferenced(page);

                if (referenced_page || referenced_ptes > 1)
                        return PAGEREF_ACTIVATE;

                /*
                 * Activate file-backed executable pages after first usage.
                 */
                if (vm_flags & VM_EXEC)
                        return PAGEREF_ACTIVATE;

                return PAGEREF_KEEP;
        }

        /* Reclaim if clean, defer dirty pages to writeback */
        if (referenced_page && !PageSwapBacked(page))
                return PAGEREF_RECLAIM_CLEAN;

        return PAGEREF_RECLAIM;
}

페이지 참조를 확인하여 그 상태를 다음과 같이 4 가지로 알아온다.

  • PAGEREF_RECLAIM
    • 페이지 회수 시작
  • PAGEREF_RECLAIM_CLEAN
    • 페이지 회수 완료되어 free 하여도 되는 상태
  • PAGEREF_KEEP
    • 다음에 처리하게 유보
  • PAGEREF_ACTIVATE
    • 페이지가 active 중이므로 다음에 처리하게 유보

 

  • 코드 라인 7~8에서 pte 매핑된 횟수를 알아온다.
  • 코드 라인 9에서 페이지의 reference 플래그를 알아오고 클리어한다.
  • 코드 라인 15~16에서 참조된 vma 영역이 VM_LOCKED 상태인 경우 PAGEREF_RECLAIM을 반환하여 페이지 회수를 시작하게 한다.
  • 코드 라인 18~47에서 pte 참조 중인 페이지인 경우 PAGEREF_KEEP을 반환하여 해당 lru로 되돌리게 한다. 단 다음 조건인 경우에는 PAGEREF_ACTIVATE를 반환하여 activae lru로 되돌리게 한다.
    • swap 영역을 사용할 수 있는 anon 페이지인 경우
    • 참조 플래그를 설정하고, 그 전에 참조 플래그가 설정되었었거나, 2 군데 이상에서 pte 참조된 경우
    • 실행 파일인 경우
  • 코드 라인 50~51에서 기존에 참조 플래그가 설정되었고 swap 영역을 사용할 수 없는 clean anon 페이지인 경우 RECLAIM_CLEAN 상태로 반환하여 곧바로 페이지를 free하게 한다.
  • 코드 라인 53에서 그 외의 경우 PAGEREF_RECLAIM을 반환하여 페이지를 회수 시작하도록 한다.

 


Shrinker

슬랩 캐시를 사용하는  스캔하여 사용되지 않는 슬랩 오브젝트를 제거하여 free 페이지를 확보할 수 있도록 shrinker를 구성할 수 있다. 이러한 shrinker는 캐시를 많이 사용하는 파일 시스템 등에서 주로 많이 사용되며 이들의 등록과 삭제는 다음 api를 통해서 할 수 있다.

  • register_shrinker()
  • unregister_shrinker()

 

이를 사용하는 대표적인 서브시스템 및 드라이버등은 다음과 같다.

  • zsmalloc
  • huge_memory
  • kvm
  • ubifs
  • ext4
  • f2fs
  • xfs
  • nfs
  • gpu
  • ion
  • bcache
  • raid5
  • virtio_balloon

 

shrinker 구조체

include/linux/shrinker.h

/*
 * A callback you can register to apply pressure to ageable caches.
 *
 * @count_objects should return the number of freeable items in the cache. If
 * there are no objects to free, it should return SHRINK_EMPTY, while 0 is
 * returned in cases of the number of freeable items cannot be determined
 * or shrinker should skip this cache for this time (e.g., their number
 * is below shrinkable limit). No deadlock checks should be done during the
 * count callback - the shrinker relies on aggregating scan counts that couldn't
 * be executed due to potential deadlocks to be run at a later call when the
 * deadlock condition is no longer pending.
 *
 * @scan_objects will only be called if @count_objects returned a non-zero
 * value for the number of freeable objects. The callout should scan the cache
 * and attempt to free items from the cache. It should then return the number
 * of objects freed during the scan, or SHRINK_STOP if progress cannot be made
 * due to potential deadlocks. If SHRINK_STOP is returned, then no further
 * attempts to call the @scan_objects will be made from the current reclaim
 * context.
 *
 * @flags determine the shrinker abilities, like numa awareness
 */
struct shrinker {
        unsigned long (*count_objects)(struct shrinker *,
                                       struct shrink_control *sc);
        unsigned long (*scan_objects)(struct shrinker *,
                                      struct shrink_control *sc);

        long batch;     /* reclaim batch size, 0 = default */
        int seeks;      /* seeks to recreate an obj */
        unsigned flags;

        /* These are for internal use */
        struct list_head list;
#ifdef CONFIG_MEMCG_KMEM
        /* ID in shrinker_idr */
        int id;
#endif
        /* objs pending delete, per node */
        atomic_long_t *nr_deferred;
};
  • (*count_objects)
    • 캐시안에서 free 가능한 오브젝트 수를 반환한다. 없는 경우 SHRINK_EMPTY를 반환한다.
  • (*scan_objects)
    • 캐시안에서 free 가능한 오브젝트들을 대상으로 reclaim을 수행한다. 반환되는 수는 할당 해제한 오브젝트 수이다.
  • batch
    • 배치 수 만큼 reclaim을 수행한다.
    • 지정하지 않는 경우 SHRINK_BATCH(128) 만큼 처리한다.
  • seeks
    • 지정되지 않으면 항상 free 가능한 오브젝트 수의 절반씩 recalim 한다.
    • 지정되는 경우 free 가능한 오브젝트 >> priority를 한 후 4/seeks를 곱한 수 만큼 reclaim 한다.
  • flags
    • 다음과 같은 플래그가 사용된다.
      • SHRINKER_NUMA_AWARE
        • node 별로 shrink를 할 수 있도록 구성한다.
      • SHRINKER_MEMCG_AWARE
        • memcg 별로 shrink를 할 수 있도록 구성한다.
  • list
    • shrinker_list에 등록될 때 사용될 노드이다.
  • id
    • memcg에서 shrinker_idr에 등록할 때 사용되는 id 값이다.
  • *nr_deferred
    • 이 포인터는 노드 수(per-node)만큼 할당된 정수 배열에 연결되며, 노드별 삭제 지연된 오브젝트 수를 나타낸다.

 

shrinker 등록

register_shrinker()

mm/vmscan.c

int register_shrinker(struct shrinker *shrinker)
{
        int err = prealloc_shrinker(shrinker);

        if (err)
                return err;
        register_shrinker_prepared(shrinker);
        return 0;
}
EXPORT_SYMBOL(register_shrinker);

shrinker를 등록한다.

  • 코드 라인 3~6에서 shrinker를 등록하기 전에 할당할 항목들을 준비한다.
  • 코드 라인 7에서 shrinker를 등록한다.

 

다음 그림은 register_shrinker() 함수를 통해 shrinker가 등록되는 모습을 보여준다.

 

prealloc_shrinker()

mm/vmscan.c

/*
 * Add a shrinker callback to be called from the vm.
 */
int prealloc_shrinker(struct shrinker *shrinker)
{
        size_t size = sizeof(*shrinker->nr_deferred);

        if (shrinker->flags & SHRINKER_NUMA_AWARE)
                size *= nr_node_ids;

        shrinker->nr_deferred = kzalloc(size, GFP_KERNEL);
        if (!shrinker->nr_deferred)
                return -ENOMEM;

        if (shrinker->flags & SHRINKER_MEMCG_AWARE) {
                if (prealloc_memcg_shrinker(shrinker))
                        goto free_deferred;
        }

        return 0;

free_deferred:
        kfree(shrinker->nr_deferred);
        shrinker->nr_deferred = NULL;
        return -ENOMEM;
}

shrinker를 등록하기 전에 할당할 항목들을 준비한다.

  • 코드 라인 3~10에서 SHRINKER_NUMA_AWARE 플래그를 사용한 경우에 노드 수 만큼 size 배열을 할당하여 shrinker->nr_deferred에 연결한다. 그렇지 않은 경우 1 개의 size 배열을 사용한다.
  • 코드 라인 12~15에서 SHRINKER_MEMCG_AWARE 플래그를 사용한 경우에 shrinker를 memcg에 등록하도록 idr을 준비한다.

 

prealloc_memcg_shrinker()

mm/vmscan.c

static int prealloc_memcg_shrinker(struct shrinker *shrinker)
{
        int id, ret = -ENOMEM;

        down_write(&shrinker_rwsem);
        /* This may call shrinker, so it must use down_read_trylock() */
        id = idr_alloc(&shrinker_idr, SHRINKER_REGISTERING, 0, 0, GFP_KERNEL);
        if (id < 0)
                goto unlock;

        if (id >= shrinker_nr_max) {
                if (memcg_expand_shrinker_maps(id)) {
                        idr_remove(&shrinker_idr, id);
                        goto unlock;
                }

                shrinker_nr_max = id + 1;
        }
        shrinker->id = id;
        ret = 0;
unlock:
        up_write(&shrinker_rwsem);
        return ret;
}

shrinker를 memcg에 등록하도록 idr을 준비한다.

  • 코드 라인 7~9에서 shrinker_idr에서 id를 발급받는다.
  • 코드 라인 11~18에서 새로 발급 받은 id 값이 shrinker_nr_max 보다 크면 shrinker_nr_max를 id + 1 값으로 갱신하고, shrinker 비트맵도 확장한다.
  • 코드 라인 19에서 shrinker에 id를 지정한다.

 

register_shrinker_prepared()

mm/vmscan.c

void register_shrinker_prepared(struct shrinker *shrinker)
{
        down_write(&shrinker_rwsem);
        list_add_tail(&shrinker->list, &shrinker_list);
#ifdef CONFIG_MEMCG_KMEM
        if (shrinker->flags & SHRINKER_MEMCG_AWARE)
                idr_replace(&shrinker_idr, shrinker, shrinker->id);
#endif
        up_write(&shrinker_rwsem);
}

shrinker를 등록한다.

  • 코드 라인 4에서 전역 shrinker_list에 @shrinker를 등록한다.
  • 코드 라인 6~7에서 SHRINKER_MEMCG_AWARE 플래그를 사용하는 shrinker인 경우 발급받은 id 자리에 @shrinker 포인터를 저정한다.

 

shrinker 등록 해제

unregister_shrinker()

mm/vmscan.c

/*
 * Remove one
 */
void unregister_shrinker(struct shrinker *shrinker)
{
        if (!shrinker->nr_deferred)
                return;
        if (shrinker->flags & SHRINKER_MEMCG_AWARE)
                unregister_memcg_shrinker(shrinker);
        down_write(&shrinker_rwsem);
        list_del(&shrinker->list);
        up_write(&shrinker_rwsem);
        kfree(shrinker->nr_deferred);
        shrinker->nr_deferred = NULL;
}
EXPORT_SYMBOL(unregister_shrinker);

shrinker의 등록을 해제한다.

 

등록된 shrinker들을 대상으로 슬랩 캐시를 shrink

shrink_slab()

mm/vmscan.c

/**
 * shrink_slab - shrink slab caches
 * @gfp_mask: allocation context
 * @nid: node whose slab caches to target
 * @memcg: memory cgroup whose slab caches to target
 * @priority: the reclaim priority
 *
 * Call the shrink functions to age shrinkable caches.
 *
 * @nid is passed along to shrinkers with SHRINKER_NUMA_AWARE set,
 * unaware shrinkers will receive a node id of 0 instead.
 *
 * @memcg specifies the memory cgroup to target. Unaware shrinkers
 * are called only if it is the root cgroup.
 *
 * @priority is sc->priority, we take the number of objects and >> by priority
 * in order to get the scan target.
 *
 * Returns the number of reclaimed slab objects.
 */
tatic unsigned long shrink_slab(gfp_t gfp_mask, int nid,
                                 struct mem_cgroup *memcg,
                                 int priority)
{
        unsigned long ret, freed = 0;
        struct shrinker *shrinker;

        if (!mem_cgroup_is_root(memcg))
                return shrink_slab_memcg(gfp_mask, nid, memcg, priority);

        if (!down_read_trylock(&shrinker_rwsem))
                goto out;

        list_for_each_entry(shrinker, &shrinker_list, list) {
                struct shrink_control sc = {
                        .gfp_mask = gfp_mask,
                        .nid = nid,
                        .memcg = memcg,
                };

                ret = do_shrink_slab(&sc, shrinker, priority);
                if (ret == SHRINK_EMPTY)
                        ret = 0;
                freed += ret;
                /*
                 * Bail out if someone want to register a new shrinker to
                 * prevent the regsitration from being stalled for long periods
                 * by parallel ongoing shrinking.
                 */
                if (rwsem_is_contended(&shrinker_rwsem)) {
                        freed = freed ? : 1;
                        break;
                }
        }

        up_read(&shrinker_rwsem);
out:
        cond_resched();
        return freed;
}

등록된 shrinker를 대상으로 슬랩 캐시를 shirnk 하여 free 페이지를 확보하게 할 수 있다.

  • 코드 라인 8~9에서 root memcg가 아닌 경우 다른 memcg를 사용하는 경우 슬랩 캐시를 shrink 한다.
  • 코드 라인 14~34에서 등록된 shrinker 리스트를 대상으로 루프를 돌며 슬랩 캐시를 shrink 한다.
    • shrinker는 register_shrinker_prepared() 함수를 통해 등록된다.
  • 코드 라인 37~39에서 out: 레이블에서는 free된 슬랩 오브젝트의 수를 반환한다.

 

shrink_slab_memcg()

mm/vmscan.c

static unsigned long shrink_slab_memcg(gfp_t gfp_mask, int nid,
                        struct mem_cgroup *memcg, int priority)
{
        struct memcg_shrinker_map *map;
        unsigned long ret, freed = 0;
        int i;

        if (!memcg_kmem_enabled() || !mem_cgroup_online(memcg))
                return 0;

        if (!down_read_trylock(&shrinker_rwsem))
                return 0;

        map = rcu_dereference_protected(memcg->nodeinfo[nid]->shrinker_map,
                                        true);
        if (unlikely(!map))
                goto unlock;

        for_each_set_bit(i, map->map, shrinker_nr_max) {
                struct shrink_control sc = {
                        .gfp_mask = gfp_mask,
                        .nid = nid,
                        .memcg = memcg,
                };
                struct shrinker *shrinker;

                shrinker = idr_find(&shrinker_idr, i);
                if (unlikely(!shrinker || shrinker == SHRINKER_REGISTERING)) {
                        if (!shrinker)
                                clear_bit(i, map->map);
                        continue;
                }

                ret = do_shrink_slab(&sc, shrinker, priority);
                if (ret == SHRINK_EMPTY) {
                        clear_bit(i, map->map);
                        /*
                         * After the shrinker reported that it had no objects to
                         * free, but before we cleared the corresponding bit in
                         * the memcg shrinker map, a new object might have been
                         * added. To make sure, we have the bit set in this
                         * case, we invoke the shrinker one more time and reset
                         * the bit if it reports that it is not empty anymore.
                         * The memory barrier here pairs with the barrier in
                         * memcg_set_shrinker_bit():
                         *
                         * list_lru_add()     shrink_slab_memcg()
                         *   list_add_tail()    clear_bit()
                         *   <MB>               <MB>
                         *   set_bit()          do_shrink_slab()
                         */
                        smp_mb__after_atomic();
                        ret = do_shrink_slab(&sc, shrinker, priority);
                        if (ret == SHRINK_EMPTY)
                                ret = 0;
                        else
                                memcg_set_shrinker_bit(memcg, nid, i);
                }
                freed += ret;

                if (rwsem_is_contended(&shrinker_rwsem)) {
                        freed = freed ? : 1;
                        break;
                }
        }
unlock:
        up_read(&shrinker_rwsem);
        return freed;
}

memcg에 등록된 shrinker를 대상으로 슬랩 캐시를 shirnk 하여 free 페이지를 확보하게 할 수 있다.

  • 코드 라인 8~9에서 요청한 memcg가 online 상태가 아니면 처리를 포기하고 0을 반환한다.
  • 코드 라인 14~17에서 lock-less rcu 방식을 사용하여 shrinker_map을 수정하기 위해 준비한다.
  • 코드 라인 19~34에서 비트맵인 shrinker_map에서 비트가 설정된 항목만큼 순회하며 해당 비트 인덱스를 키로 shrinker_idr 에서 등록된 shrinker를 대상으로 슬랩 캐시를 shrink한다.
  • 코드 라인 35~58에서 shrink 결과가 empty인 경우 shrinker_map의 해당 비트를 클리어한다. 그 후 다시 한번 슬랩 캐시를 shrink 하고, 두 번째 수행 결과가 empty가 아닌 경우에는 shrinker_map의 해당 비트를 다시 설정한다.
  • 코드 라인 59~64에서 shirnk된 슬랩 캐시 오브젝트 수를 더한 후 계속 반복한다.
  • 코드 라인 66~68에서 unlock: 레이블에서는 free된 슬랩 오브젝트의 수를 반환한다.

 

do_shrink_slab()

mm/vmscan.c -1/2-

static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
                                    struct shrinker *shrinker, int priority)
{
        unsigned long freed = 0;
        unsigned long long delta;
        long total_scan;
        long freeable;
        long nr;
        long new_nr;
        int nid = shrinkctl->nid;
        long batch_size = shrinker->batch ? shrinker->batch
                                          : SHRINK_BATCH;
        long scanned = 0, next_deferred;

        if (!(shrinker->flags & SHRINKER_NUMA_AWARE))
                nid = 0;

        freeable = shrinker->count_objects(shrinker, shrinkctl);
        if (freeable == 0 || freeable == SHRINK_EMPTY)
                return freeable;

        /*
         * copy the current shrinker scan count into a local variable
         * and zero it so that other concurrent shrinker invocations
         * don't also do this scanning work.
         */
        nr = atomic_long_xchg(&shrinker->nr_deferred[nid], 0);

        total_scan = nr;
        if (shrinker->seeks) {
                delta = freeable >> priority;
                delta *= 4;
                do_div(delta, shrinker->seeks);
        } else {
                /*
                 * These objects don't require any IO to create. Trim
                 * them aggressively under memory pressure to keep
                 * them from causing refetches in the IO caches.
                 */
                delta = freeable / 2;
        }

        total_scan += delta;
        if (total_scan < 0) {
                pr_err("shrink_slab: %pF negative objects to delete nr=%ld\n",
                       shrinker->scan_objects, total_scan);
                total_scan = freeable;
                next_deferred = nr;
        } else
                next_deferred = total_scan;

        /*
         * We need to avoid excessive windup on filesystem shrinkers
         * due to large numbers of GFP_NOFS allocations causing the
         * shrinkers to return -1 all the time. This results in a large
         * nr being built up so when a shrink that can do some work
         * comes along it empties the entire cache due to nr >>>
         * freeable. This is bad for sustaining a working set in
         * memory.
         *
         * Hence only allow the shrinker to scan the entire cache when
         * a large delta change is calculated directly.
         */
        if (delta < freeable / 4)
                total_scan = min(total_scan, freeable / 2);

        /*
         * Avoid risking looping forever due to too large nr value:
         * never try to free more than twice the estimate number of
         * freeable entries.
         */
        if (total_scan > freeable * 2)
                total_scan = freeable * 2;

        trace_mm_shrink_slab_start(shrinker, shrinkctl, nr,
                                   freeable, delta, total_scan, priority);

shrink_control을 통해 요청한 글로벌 또는 memcg에 등록된 shrinker를 대상으로 슬랩 캐시를 shirnk 하여 free 페이지를 확보 한다.

  • 코드 라인 11~12에서 shrinker에 한 번에 처리할 슬랩 오브젝트 수가 지정되지 않는 경우 SHRINK_BATCH(128) 개를 대입한다.
  • 코드 라인 15~16에서 SHRINKER_NUMA_AWARE 플래그가 사용된 경우가 아니면 nid를 0으로 고정한다.
  • 코드 라인 18~20에서 shrinker에서 free 가능한 object 수를 알아오기 위해 (*count_object)의 결과 값을 알아와freeable에 대입한다. 처리할 free 가능한 오브젝트가 없는 경우 함수를 빠져나간다.
  • 코드 라인 27~29에서 삭제 지연 중인 오브젝트 수(nr_deferred)를 이번에 처리하기 위해 total_scan에 대입하고, 기존 값은 atomic하게 0으로 리셋하여 다른 곳과 동시에 호출되는 것을 막는다.
  • 코드 라인 30~50에서  freeable 값에 seeks와 priority를 적용하여 산출한 delta를 total_scan에 더한다. 만일 total_scan 값이 0 미만인 경우 freeable 값을 모두 적용한다.
    • shrinker에 seeks가 지정된 경우
      • delta = (freeable >> priority) * 4 / shrinker->seeks
    • shrinker에 seeks가 지정되지 않은 경우
      • delta = freeable / 2
  • 코드 라인 64~65에서 delta가 freeable의 25% 미만인 경우 total_scan 수가 freeable의 절반을 초과하지 않도록 제한한다.
  • 코드 라인 72~73에서 total_scan이 freeable의 두 배를 초과하지 않도록 제한한다.

 

mm/vmscan.c -2/2-

        /*
         * Normally, we should not scan less than batch_size objects in one
         * pass to avoid too frequent shrinker calls, but if the slab has less
         * than batch_size objects in total and we are really tight on memory,
         * we will try to reclaim all available objects, otherwise we can end
         * up failing allocations although there are plenty of reclaimable
         * objects spread over several slabs with usage less than the
         * batch_size.
         *
         * We detect the "tight on memory" situations by looking at the total
         * number of objects we want to scan (total_scan). If it is greater
         * than the total number of objects on slab (freeable), we must be
         * scanning at high prio and therefore should try to reclaim as much as
         * possible.
         */
        while (total_scan >= batch_size ||
               total_scan >= freeable) {
                unsigned long ret;
                unsigned long nr_to_scan = min(batch_size, total_scan);

                shrinkctl->nr_to_scan = nr_to_scan;
                shrinkctl->nr_scanned = nr_to_scan;
                ret = shrinker->scan_objects(shrinker, shrinkctl);
                if (ret == SHRINK_STOP)
                        break;
                freed += ret;

                count_vm_events(SLABS_SCANNED, shrinkctl->nr_scanned);
                total_scan -= shrinkctl->nr_scanned;
                scanned += shrinkctl->nr_scanned;

                cond_resched();
        }

        if (next_deferred >= scanned)
                next_deferred -= scanned;
        else
                next_deferred = 0;
        /*
         * move the unused scan count back into the shrinker in a
         * manner that handles concurrent updates. If we exhausted the
         * scan, there is no need to do an update.
         */
        if (next_deferred > 0)
                new_nr = atomic_long_add_return(next_deferred,
                                                &shrinker->nr_deferred[nid]);
        else
                new_nr = atomic_long_read(&shrinker->nr_deferred[nid]);

        trace_mm_shrink_slab_end(shrinker, nid, freed, nr, new_nr, total_scan);
        return freed;
}
  • 코드 라인 16~33에서 batch_size 또는 freeable 만큼 반복하며 shrinker에 등록한 scan_objects() 핸들러 함수를 호출하여 free 가능한 오브젝트 들을 reclaim한다. free된 object 수를 알아와서 freed에 추가한다. 만일 결과가 SHRINK_STOP인 경우 루프를 벗어난다.
  • 코드 라인 35~48에서 삭제 처리되지 않고 남은 오브젝트 수를 다음에 처리하기 위해 다시 shrinker->nr_deferred[nid]에 대입한다.
  • 코드 라인 51에서 정상 삭제 처리된 오브젝트 수를 반환한다.

 

다음 그림은 do_shrink_slab()을 통하여 memcg(없으면 글로벌)에 등록된 등록된 shrinker들을 대상으로 각각 산출된 total_scan만큼씩 shrink 하는 과정을 보여준다.

 

참고