Vmap

 

Vmap

비 연속으로 구성된 여러 개의 order 0 물리 페이지 프레임들을 page descriptor 배열 정보로 요청하는 경우 VMALLOC address space 공간의 빈 자리를 찾아 매핑하고 매핑된 가상 시작 주소를 반환한다.

  • 내부적으로는 VMALLOC address space 범위의 빈 공간 검색을 위해 RB 트리와 리스트를 사용한다.
    • vmap_area_root RB 트리
    • vmap_area_list 리스트
    • free_vmap_cache
      • 빈 공간에 대한 검색을 빠르게 하기 위해 가장 최근에 등록하여 사용한 vma 또는 가장 최근에 free 한 vma의 이전(prev) vma 이 보관된다.
      • 관련 전역 변수 사용
        • cached_hole_size
        • cached_vstart
        • cached_align
        • vmap_area_pcpu_hole (초기값은 VMALLOC_END)

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

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

 

Lazy TLB Flushing(Free)

  • vunmap()을 수행하면 다음과 같은 처리항목이 수행되어야 하는데 즉각 처리되는 항목과 나중에 모아 처리할 항목을 분류한다.
    • 즉각 처리
      • 페이지 테이블에서 매핑 해제
      • 캐시 flush
    • 지연 처리
      • RB tree 및 리스트에서 vmap_area의 제거
      • 모든 cpu의 TLB flush
        • cpu가 많은 시스템에서 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)
      • 할당 상태

 

Vmap() 대체 API

  • vmap() 대체 api로 per-cpu map 기반의 lazy TLB flushing을 적용한 vm_map_ram() & vm_unmap_ram()이 준비되어 있다.
  • 이 API는 아직 많은 드라이버에 적용되어 사용하지는 않고 일부 드라이버들 에서만 사용하고 있다.

 

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

vmap-2

 

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;

        might_sleep();

        if (count > totalram_pages)
                return NULL;

        area = get_vm_area_caller((count << PAGE_SHIFT), 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을 반환한다.

  • might_sleep();
    • CONFIG_PREEMPT_VOLUNTARY 커널 옵션을 사용하는 경우 preempt point로 긴급히 리스케쥴링 요청한 태스크가 있는 경우 sleep 한다.
  • if (count > totalram_pages) return NULL;
    • 전체 메모리 페이지보다 더 많은 페이지를 요구하는 경우 처리를 포기하고 null을 반환한다.
  • area = get_vm_area_caller((count << PAGE_SHIFT), flags, __builtin_return_address(0)); if (!area) return NULL;
    • vmap_area 및 vm_struct 정보를 구성한다. 실패하는 경우 null을 반환한다.
  • if (map_vm_area(area, prot, pages)) { vunmap(area->addr); return NULL; }
    • vm_struct 정보로 페이지들의 매핑을 시도하고 실패한 경우 해제 후 null을 반환한다.

 

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

vmap-1b

 

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 address space에서 빈 공간을 찾아 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());
        if (flags & VM_IOREMAP)
                align = 1ul << clamp(fls(size), PAGE_SHIFT, IOREMAP_MAX_ORDER);

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

        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;
}

요청 가상 주소 범위에서 요청 size가 들어갈 수 있는 빈 자리를 찾아 그 가상 주소로 vmap_area와 vm_struct 정보를 구성하여 반환한다.

  • if (flags & VM_IOREMAP) align = 1ul << clamp(fls(size), PAGE_SHIFT, IOREMAP_MAX_ORDER);
    • io 리매핑이 요청된 경우 size를 가까운 2의 차수 단위로 align 값을 정하되, 그 값은 2 ^ (PAGE_SHIFT(12) ~ 24) 범위내에서 정한다.
      • arm: 4K ~ 16M
  • size = PAGE_ALIGN(size); if (unlikely(!size)) return NULL;
    • size를 페이지 단위로 round up하고 size가 지정되지 않은 경우 null을 반환한다.
  • area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); if (unlikely(!area)) return NULL;
    • vm_struct 구조체를 구성하기 위해 그 크기만큼 할당받는다.
  • if (!(flags & VM_NO_GUARD)) size += PAGE_SIZE;
    • no guard 플래그를 요청하지 않은 경우 가드용으로 1 페이지를 확보하기 위해 size에 1 페이지를 추가한다.
  • va = alloc_vmap_area(size, align, start, end, node, gfp_mask); if (IS_ERR(va)) { kfree(area); return NULL; }
    • 요청한 가상 주소 범위에서 빈 매핑 공간을 찾아 vmap_area를 할당 구성하고 RB트리 및 리스트에 insert한 후 엔트리 정보를 반환한다.
  • setup_vmalloc_vm(area, va, flags, caller);
    • vm_struct 및 vmap_area에 정보를 설정한다.

 

vmap_area 할당

alloc_vmap_area()

mm/vmalloc.c

/*
 * 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(size & ~PAGE_MASK);
        BUG_ON(!is_power_of_2(align));

        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;

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

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

 

  • va = kmalloc_node(sizeof(struct vmap_area), gfp_mask & GFP_RECLAIM_MASK, node); if (unlikely(!va)) return ERR_PTR(-ENOMEM);
    • vmap_area 구조체를 구성하기 위해 reclaim 관련 플래그만 사용하여 할당을 받고 할당 에러인 경우 -ENOMEM을 반환한다.
  • retry: spin_lock(&vmap_area_lock); if (!free_vmap_cache || size < cached_hole_size || vstart < cached_vstart || align < cached_align) {
    • spin-lock을 얻고 캐시된 노드 위치를 사용할 수  없는 조건인 경우 이 번 검색에 캐시를 사용하지 못하게 한다. 조건은 다음과 같다.
      • 캐시 바로 이전(prev) 공간에 있는 hole이 새로 요청하는 size를 커버할 수 있는 경우
      • 시작 요청 범위가 캐시 사용 시의 요청 범위보다 작은 경우
      • 요청 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;
        }

        /* 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_entry(first->list.next,
                                struct vmap_area, list);
        }

전역 free_vmap_cache가 가리키는 rb 노드를 first에 대입하고, 그 노드의 끝 주소를 addr에 대입한다. (여기서 부터 검색)

  • if (free_vmap_cache) { first = rb_entry(free_vmap_cache, struct vmap_area, rb_node);
    • 최종 등록하였거나 최근 free 시킨 va(vmap_area) 이전(prev) va를 보관한 free_vmap_cache에서 vm 엔트리를 가져온다.
  • addr = ALIGN(first->va_end, align); if (addr < vstart) goto nocache; if (addr + size < addr) goto overflow;
    • 만일 first 엔트리의 끝 주소가 요청 범위를 벗어난 경우 캐시를 사용하지 않게 하기 위해 nocache 레이블로 이동한다.
    • 또한 first 엔트리의 끝 주소에 size를 더해서 범위를 초과한 경우 overflow 레이블로 이동한다.

캐시에 없는 경우 전역 vmap_area_root RB 트리를 통해 요청 범위에서 가장 첫 va를 first에 대입한다.

  • } else { addr = ALIGN(vstart, align); if (addr + size < addr) goto overflow;
    • free_vmap_cache를 사용할 수 없는 경우 first 엔트리의 끝 주소에 size를 더해서 범위를 초과한 경우 overflow 레이블로 이동한다.
  • n = vmap_area_root.rb_node; first = NULL;
    • n에 전역 vmap_area_root의 첫 루트 노드를 대입한다.
  • 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; }

    • leaf 노드를 찾는다.
  • if (!first) goto found;
    • leaf 노드를 찾은 경우 found 레이블로 이동한다.

전역 first va(vmap_area)부터 리스트의 끝까지 요청 범위 내에서 size가 들어갈 수 있는 빈 공간을 찾는다.

  • while (addr + size > first->va_start && addr + size <= vend) {
    • 범위가 끝 날때까지 루프를 돈다.
  • if (addr + cached_hole_size < first->va_start) cached_hole_size = first->va_start – addr;
    • 현재 엔트리 이전에 공간이 비게되는 경우 cached_hole_size에 그 공간을 기억해둔다.
  • addr = ALIGN(first->va_end, align); if (addr + size < addr) goto overflow;
    • 현재 엔트리의 끝 주소 + size가 범위를 초과한 경우 overflow 레이블로 이동한다.
  • if (list_is_last(&first->list, &vmap_area_list)) goto found;
    • 현재 엔트리가 마지막 엔트리인 경우 found 레이블로 이동하고 그렇지 않은 경우 계속 루프를 수행한다.

 

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(va->va_start & (align-1));
        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 (printk_ratelimit())
                pr_warn("vmap allocation for size %lu failed: "
                        "use vmalloc=<size> to increase size.\n", size);
        kfree(va);
        return ERR_PTR(-EBUSY);
}

적절한 공간을 찾은 경우 RB 트리 및 리스트에 insert 하고 만일 요청 범위내에서 빈 공간을 찾을 수 없는 경우 lazy되어 캐시되어 있는 free 엔트리들을 삭제한 후 한 번만 다시 시도한다.

  • found: if (addr + size > vend) goto overflow;
    • 범위를 벗어난 경우 overflow 레이블로 이동한다.
  • va->va_start = addr; va->va_end = addr + size; va->flags = 0; __insert_vmap_area(va);
    • vmap area 엔트리를 구성하고 전역 vmap_area_list RB 트리 및 vmap_area_list에 끼워 넣는다.
  • free_vmap_cache = &va->rb_node; spin_unlock(&vmap_area_lock);
    • 방금 처리한 엔트리를 free_vmap_cache에 대입하고 사용한 lock을 풀고 vmap area 엔트리를 반환한다.
  • overflow: spin_unlock(&vmap_area_lock); if (!purged) { purge_vmap_area_lazy(); purged = 1; goto retry; }
    • 범위 내에서 빈 공간을 발견하지 못한 경우 캐시되어 있는 lazy free 엔트리들을 해제하고 다시 한 번만 시도한다.
  • kfree(va); return ERR_PTR(-EBUSY);
    • 매핑 공간을 찾지 못하면 할당 받은 영역을 해제하고 -EBUSY를 반환한다.

 

다음 그림은 매핑을 위해 요청 가상 주소 범위내에서 빈 공간을 찾을 때 먼저 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 한다.

  • while() 문을 통해 vmap_area_root RB 트리에서 insert 할 leaf 노드를 찾는다.
  • rb_link_node() 함수를 사용하여 leaf 노드에 엔트리를 연결하고
  • rb_insert_color()를 통해 RB 트리의 밸런스를 균형있게 맞춘다.
  • 마지막으로 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_pud_range(pgd, addr, next, prot, pages, &nr);
                if (err)
                        return err;
        } while (pgd++, addr = next, addr != end);

        return nr;
}

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

 

flush_cache_vmap()

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를 할 필요가 없어서 성능이 크게 개선된다.

 

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에 정보를 설정한다.

 

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 address space에 매핑한 가상 주소 영역의 매핑을 해제한다. 다만 물리 페이지는 해제 시키지 않는다.

 

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

vunmap-2

 

다음 그림은 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 = remove_vm_area(addr);
        if (unlikely(!area)) {
                WARN(1, KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
                                addr);
                return;
        }

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

        if (deallocate_pages) {
                int i;

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

                        BUG_ON(!page);
                        __free_page(page);
                }

                if (area->flags & VM_VPAGES)
                        vfree(area->pages);
                else 
                        kfree(area->pages);
        }

        kfree(area);
        return;
}

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

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

 

  • area = remove_vm_area(addr);
    • 요청 가상 주소를 RB 트리 vmap_area_root에 등록되어 있는 vmap_area 정보에서 검색하여 매치된 vmap_area 정보를 제거한 후 매핑을 해제하고 vm_struct 정보를 알아온다.
  • if (deallocate_pages) { int i; for (i = 0; i < area->nr_pages; i++) { struct page *page = area->pages[i]; __free_page(page); }
    • deallocate_pages 인수 요청이 설정된 경우 등록된 모든 페이지들을 해제하여 버디 시스템으로 돌려준다.
  • if (area->flags & VM_VPAGES) vfree(area->pages); else kfree(area->pages);
    • **pages가 vmalloc으로 할당된 경우는 vfree() 함수로 해제하고 그렇지 않은 경우 kfree() 함수로 해제한다.
  • kfree(area);
    • vm_struct 정보를 해제한다.

 

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;

        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;
                spin_unlock(&vmap_area_lock);

                vmap_debug_free_range(va->va_start, va->va_end);
                kasan_free_shadow(vm);
                free_unmap_vmap_area(va);
                vm->size -= PAGE_SIZE;

                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이라 부른다.

 

  • va = find_vmap_area((unsigned long)addr);
    • 요청 가상 주소를 RB 트리 vmap_area_root에 등록되어 있는 vmap_area 정보에서 검색하여 매치된 vmap_area 정보를 찾아온다.
  • if (va && va->flags & VM_VM_AREA) { struct vm_struct *vm = va->vm;
    • 찾은 vmap_area 정보의 플래그가 VM_VM_AREA로 설정된 경우 vm_struct 정보를 참조한다.
  • spin_lock(&vmap_area_lock); va->vm = NULL; va->flags &= ~VM_VM_AREA; spin_unlock(&vmap_area_lock)
    • vmap_area 정보에서 vm_struct의 연결을 제거하고 플래그도 VM을 제거한다.
  • free_unmap_vmap_area(va);
    • vmap_area 정보를 사용하여 매핑을 해제한다.
  • vm->size -= PAGE_SIZE;
    • vm의 사이에서 1페이지를 감소시킨다.

 

 

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 -1-

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);
        free_unmap_vmap_area_noflush(va);
}

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

 

flush_cache_vunmap()

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 타입인 경우 캐시를 비우지 않아도 되므로 성능이 향상된다.

 

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_pud_range(pgd, addr, next);
        } while (pgd++, addr = next, addr != end);
}

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

 

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)
{
        va->flags |= VM_LAZY_FREE;
        atomic_add((va->va_end - va->va_start) >> PAGE_SHIFT, &vmap_lazy_nr);
        if (unlikely(atomic_read(&vmap_lazy_nr) > lazy_max_pages()))
                try_purge_vmap_area_lazy();
}

vmap_area를 곧바로 삭제하지 않고 VM_LAZY_FREE 플래그를 설정한다. 페이지 수가 lazy_max_pages()를 초과하는 경우 pruge를 수행한다.

  • 매핑 해제할 페이지 수를 vmap_lazy_nr에 저장하고 작은 확률로 lazy_max_pages()를 초과하는 경우 purge를 수행한다.

 

vmap_area free -2- 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);
}

32M에 해당하는 페이지 수 x online cpu 수를 표현하는데 필요한 비트 수를 반환한다.

  • cpu가 많아지는 경우 TLB flush는 시스템의 전체적인 성능을 떨어뜨리므로 cpu 수가 많아질 수록 lazy_max_pages 수는 더 커져야 한다.

 

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)
{
        unsigned long start = ULONG_MAX, end = 0;

        __purge_vmap_area_lazy(&start, &end, 0, 0);
}

가상 주소 범위에 포함된 vmap_area_list의 엔트리들을모두 purge_list에 옮기고 해당하는 페이지 수를 vmap_lazy_nr에서 감소시킨다. 가상 주소 범위의 TLB도 flush한다.

 

__purge_vmap_area_lazy()

mm/vmalloc.c

/*
 * Purges all lazily-freed vmap areas.
 *
 * If sync is 0 then don't purge if there is already a purge in progress.
 * If force_flush is 1, then flush kernel TLBs between *start and *end even
 * if we found no lazy vmap areas to unmap (callers can use this to optimise
 * their own TLB flushing).
 * Returns with *start = min(*start, lowest purged address)
 *              *end = max(*end, highest purged address)
 */
static void __purge_vmap_area_lazy(unsigned long *start, unsigned long *end,
                                        int sync, int force_flush)
{
        static DEFINE_SPINLOCK(purge_lock);
        LIST_HEAD(valist);
        struct vmap_area *va;
        struct vmap_area *n_va;
        int nr = 0;

        /*
         * If sync is 0 but force_flush is 1, we'll go sync anyway but callers
         * should not expect such behaviour. This just simplifies locking for
         * the case that isn't actually used at the moment anyway.
         */
        if (!sync && !force_flush) {
                if (!spin_trylock(&purge_lock))
                        return;
        } else
                spin_lock(&purge_lock);

        if (sync)
                purge_fragmented_blocks_allcpus();

        rcu_read_lock();
        list_for_each_entry_rcu(va, &vmap_area_list, list) {
                if (va->flags & VM_LAZY_FREE) {
                        if (va->va_start < *start)
                                *start = va->va_start;
                        if (va->va_end > *end)
                                *end = va->va_end;
                        nr += (va->va_end - va->va_start) >> PAGE_SHIFT;
                        list_add_tail(&va->purge_list, &valist);
                        va->flags |= VM_LAZY_FREEING;
                        va->flags &= ~VM_LAZY_FREE;
                }
        }
        rcu_read_unlock();

        if (nr)
                atomic_sub(nr, &vmap_lazy_nr);

        if (nr || force_flush)
                flush_tlb_kernel_range(*start, *end);

        if (nr) {
                spin_lock(&vmap_area_lock);
                list_for_each_entry_safe(va, n_va, &valist, purge_list)
                        __free_vmap_area(va);
                spin_unlock(&vmap_area_lock);
        }
        spin_unlock(&purge_lock);
}

요청 가상 주소 범위에 있는 모든 lazy free되어 있는 vmap_area 들을 찾아서 해제한다.

  • if (!sync && !force_flush) { if (!spin_trylock(&purge_lock)) return; } else spin_lock(&purge_lock);
    • purge용 lock을 획득 한다. sync와 force_flush가 설정되지 않은 경우 lock 획득이 실패하면 purge 처리를 포기한다.
  • if (sync) purge_fragmented_blocks_allcpus();
    • possible cpu 수 만큼 루프를 돌며 각 cpu에서 lazy 처리를 위해 삭제되지 않고 대기하는 vmap_block들을 모두 삭제한다.
  • list_for_each_entry_rcu(va, &vmap_area_list, list) {
    • vmap_area_list에 등록된 vmap_area 정보들 수 만큼 루프를 돈다.
  • if (va->flags & VM_LAZY_FREE) { if (va->va_start < *start) *start = va->va_start; if (va->va_end > *end) *end = va->va_end; nr += (va->va_end – va->va_start) >> PAGE_SHIFT; list_add_tail(&va->purge_list, &valist); va->flags |= VM_LAZY_FREEING; va->flags &= ~VM_LAZY_FREE; }
    • VM_LAZY_FREE가 설정된 엔트리들에서 요청 범위에 있는 엔트리들을 일단 임시 리스트인 valist에 옮기고 VM_LAZY_FREE 를 제거하고 VM_LAZY_FREEING으로 설정한다.
  • if (nr) atomic_sub(nr, &vmap_lazy_nr);
    • 처리할 페이지 수만큼을 vmap_lazy_nr에서 감소시킨다.
  • if (nr || force_flush) flush_tlb_kernel_range(*start, *end);
    • force_flush가 설정되었거나 처리할 페이지 수가 있는 경우 해당 범위의 TLB 엔트리들을 flush한다.
  • if (nr) { spin_lock(&vmap_area_lock); list_for_each_entry_safe(va, n_va, &valist, purge_list) __free_vmap_area(va); spin_unlock(&vmap_area_lock); }
    • valist에 연결되어 있는 엔트리들을 모두 해제한다.

 

vmap_area free -3-

__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에서 제거한 후 해제한다.

  • if (free_vmap_cache) { if (va->va_end < cached_vstart) { free_vmap_cache = NULL;
    • free_vmap_cache가 지정된 경우 그 영역 아래에 요청 영역이 위치하는 경우 free_vmap_cache에 null을 대입한다.
    • free_vmap_cache는 vmap_area_lock에 의해 보호된다.
  • } 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); } }
    • free_vmap_cache에 등록된 엔트리의 시작 주소 이하에 요청 영역이 있는 경우 free_vmap_cache에 그 이전 노드를 대입한다.
  • rb_erase(&va->rb_node, &vmap_area_root); RB_CLEAR_NODE(&va->rb_node); list_del_rcu(&va->list);
    • RB 트리 vmap_area_root와 vmap_area_list에서 해당 vmap_area를 제거한다.
  • if (va->va_end > VMALLOC_START && va->va_end <= VMALLOC_END) vmap_area_pcpu_hole = max(vmap_area_pcpu_hole, va->va_end);
    • 요청 가상 주소 범위가 VMALLOC 영역 이내에 있는 경우 vmap_area_pcpu_hole 보다 요청 가상 끝 주소가 요청 가상 끝 주소를 대입한다.
    • vmalloc address space의 최상단 부분은 per-cpu의 dynamic chunk가 생성되는 부분이고 최상단 hole의 끝 주소를 기억하기 위해 vmap_area_pcpu_hole을 사용한다.
  • kfree_rcu(va, rcu_head);
    • rcu의 grace period 이후에 kfree를 통해 object가 해제되도록 한다.

 

purge_fragmented_blocks_allcpus()

mm/vmalloc.c

static void purge_fragmented_blocks_allcpus(void)
{
        int cpu;

        for_each_possible_cpu(cpu)
                purge_fragmented_blocks(cpu);
}

possible cpu 수 만큼 루프를 돌며 각 cpu에서 lazy 처리를 위해 삭제되지 않고 대기하는 vmap_block들을 모두 삭제한다.

 

purge_fragmented_blocks()

mm/vmalloc.c

static void purge_fragmented_blocks(int cpu)
{
        LIST_HEAD(purge);
        struct vmap_block *vb;
        struct vmap_block *n_vb;
        struct vmap_block_queue *vbq = &per_cpu(vmap_block_queue, cpu);

        rcu_read_lock();
        list_for_each_entry_rcu(vb, &vbq->free, free_list) {

                if (!(vb->free + vb->dirty == VMAP_BBMAP_BITS && vb->dirty != VMAP_BBMAP_BITS))
                        continue;

                spin_lock(&vb->lock);
                if (vb->free + vb->dirty == VMAP_BBMAP_BITS && vb->dirty != VMAP_BBMAP_BITS) {
                        vb->free = 0; /* prevent further allocs after releasing lock */
                        vb->dirty = VMAP_BBMAP_BITS; /* prevent purging it again */
                        bitmap_fill(vb->dirty_map, VMAP_BBMAP_BITS);
                        spin_lock(&vbq->lock);
                        list_del_rcu(&vb->free_list);
                        spin_unlock(&vbq->lock);
                        spin_unlock(&vb->lock);
                        list_add_tail(&vb->purge, &purge);
                } else
                        spin_unlock(&vb->lock);
        }
        rcu_read_unlock();

        list_for_each_entry_safe(vb, n_vb, &purge, purge) {
                list_del(&vb->purge);
                free_vmap_block(vb);
        }
}

요청한 cpu에서 lazy 처리를 위해 삭제되지 않고 대기하는 vmap_block들을 모두 삭제한다.

  • vmap() 함수 대신 vm_map_ram()을 사용한 경우 per-cpu vmaps 기반의 lazy-TLB flush를 구현하였는데 이를 통해 더 빠른 처리를 가능하게 하였다.
  • vmap()과 vunmap()을 확장하여 대체하는 새로운 인터페이스는 vm_map_ram()과 vm_unmap_ram() 함수이다.
  • 현재 커널에서 vmalloc() 함수는 vm_map_ram()을  사용하지 않고 vmap()을 사용하고 있다.
    • 따라서 처리 속도는 빠르나 아직 자주 이용되지 않는 상황이다.

 

  • struct vmap_block_queue *vbq = &per_cpu(vmap_block_queue, cpu);
    • 현재 cpu의 vmap_block_queue 정보
  • list_for_each_entry_rcu(vb, &vbq->free, free_list) {
    • vmap_block_queue에 있는 의 free_list의 엔트리 수 만큼 루프를 돈다.
  • if (!(vb->free + vb->dirty == VMAP_BBMAP_BITS && vb->dirty != VMAP_BBMAP_BITS)) continue;
    • vmap_block의 free + dirty가 VMAP_BBMAP_BITS와 다르거나 dirty가 VMAP_BBMAP_BITS와 같은 경우 skip 한다.
    • VMALLOC_SPACE
      • VMALLOC_END – VMALLOC_START와 같이 계산하지 않고 32bit 시스템의 경우 대략 128M로 계산하고  64bit 시스템에서는 128G로 계산한다.
    • VMALLOC_PAGES
      • VMALLOC 페이지 수
        • rpi2: 0x8000
    • VMAP_BBMAP_BITS
      • VMALLOC 페이지 수 / NR_CPUS를 2의 차수 단위로 round up한 수 / 16하여 32 ~ 1024 범위로 제한한 값
        • rpi2: 0x200
  • spin_lock(&vb->lock); if (vb->free + vb->dirty == VMAP_BBMAP_BITS && vb->dirty != VMAP_BBMAP_BITS) {
    • 이 번엔 lock을 걸고 다시 한 번 위에서 한 조건의 반대인 경우
  • vb->free = 0; vb->dirty = VMAP_BBMAP_BITS;
    • vmap_block의 free에 0을 대입하고 dirty에 VMAP_BBMAP_BITS를 대입하여 purging이 다시 처리되지 않도록 막는다.
  • bitmap_fill(vb->dirty_map, VMAP_BBMAP_BITS); spin_lock(&vbq->lock); list_del_rcu(&vb->free_list); spin_unlock(&vbq->lock); spin_unlock(&vb->lock); list_add_tail(&vb->purge, &purge);
    • vmap_block의 dirty_map에 VMAP_BBMAP_BITS만큼 설정하고 free_list에서 제거한 후 임시 리스트인 purge로 옮긴다.
  • list_for_each_entry_safe(vb, n_vb, &purge, purge) { list_del(&vb->purge); free_vmap_block(vb); }
    • 마지막으로 로컬 리스트인 purge에 담긴 vmap_block을 purge 리스트에서 제거하고 free 시킨다.

 

free_vmap_block()

mm/vmalloc.c

static void free_vmap_block(struct vmap_block *vb)
{
        struct vmap_block *tmp;
        unsigned long vb_idx;

        vb_idx = addr_to_vb_idx(vb->va->va_start);
        spin_lock(&vmap_block_tree_lock);
        tmp = radix_tree_delete(&vmap_block_tree, vb_idx);
        spin_unlock(&vmap_block_tree_lock);
        BUG_ON(tmp != vb);

        free_vmap_area_noflush(vb->va);
        kfree_rcu(vb, rcu_head);
}

vmap_block을 해제한다.

  • radix 트리 vmap_block_tree에서 vb_idx를 제거하고 vmap_block의 vmap_area 및 vmap_block을 해제한다.

 

addr_to_vb_idx()

mm/vmalloc.c

/*
 * We should probably have a fallback mechanism to allocate virtual memory
 * out of partially filled vmap blocks. However vmap block sizing should be
 * fairly reasonable according to the vmalloc size, so it shouldn't be a
 * big problem.
 */

static unsigned long addr_to_vb_idx(unsigned long addr)
{
        addr -= VMALLOC_START & ~(VMAP_BLOCK_SIZE-1);
        addr /= VMAP_BLOCK_SIZE;
        return addr;
}

가상 주소를 vmap_block의 인덱스 값으로 변환한다.

  • 가상 주소를 32M 단위로 round down 한 후 32M 단위로 나눈 수를 반환한다.
    • VMAP_BLOCK_SIZE
      • rpi2: 0x200000 (32M)
  • 예) VMALLOC_START가 0xf000_0000인 경우
    • 0~3까지의 인덱스 번호를 리턴한다.

 

참고

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 | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);

커널을 위해 연속된 가상 메모리를 할당한다. 커널노드 구분 없이 highmem의 이용 가능하도록 __vmalloc_node_flags() 함수를 이어 호출한다.

 

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

vmalloc-1

 

__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.
 */
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);
}

커널을 위해 요청 노드에서 연속된 가상 메모리를 할당하되 가상 주소는 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_alloc(addr, real_size, 2, gfp_mask);

        return addr;

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

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

  • size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > totalram_pages) goto fail;
    • size를 페이지 단위로 정렬하고 그 size가 0이거나 전체 메모리보다 큰 경우 fail 레이블을 경유해 null을 반환한다.
  • area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED | vm_flags, start, end, node, gfp_mask, caller); if (!area) goto fail;
    • 요청 가상 주소 범위에서 요청 size가 들어갈 수 있는 빈 자리를 찾아 그 가상 주소로 vmap_area와 vm_struct 정보를 구성하여 반환한다.
  • addr = __vmalloc_area_node(area, gfp_mask, prot, node); if (!addr) return NULL;
    • vm_struct 정보가 요청하는 가상 주소 영역 만큼 page descriptor 테이블을 할당받고 order 0 페이지들을 요청 수 만큼 할당하여 연결하고  페이지 테이블에 매핑한다.
  • clear_vm_uninitialized_flag(area);
    • vm에 대해 uninitialized 플래그를 클리어하여 vm이 초기화 되었음을 나타낸다.

 

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

__vmalloc_node_range-1

 

__vmalloc_area_node()

mm/vmalloc.c

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
                                 pgprot_t prot, int node)
{
        const int order = 0;
        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;

        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|__GFP_HIGHMEM,
                                PAGE_KERNEL, node, area->caller);
                area->flags |= VM_VPAGES;
        } 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);
                else
                        page = alloc_pages_node(node, alloc_mask, order);

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

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

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

vm_struct 정보가 요청하는 가상 주소 영역 만큼 page descriptor 테이블을 할당받고 order 0 페이지들을 요청 수 만큼 할당하여 연결하고  페이지 테이블에 매핑한다.

  • const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
    • 페이지 회수와 관련된 플래그만 남기고 __GFP_ZERO를 추가한다.
  • const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;
    • gfp_mask에 __GFP_NOWARN을 추가한다.
  • nr_pages = get_vm_area_size(area) >> PAGE_SHIFT; array_size = (nr_pages * sizeof(struct page *));
    • 영역이 사용하는 페이지 수를 알아오고 만들 page descriptor들이 사용할 배열의 크기를 구한다.
  • area->nr_pages = nr_pages;
    • area에서 사용하는 페이지 수를 기록한다.
  • if (array_size > PAGE_SIZE) { pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM, PAGE_KERNEL, node, area->caller); area->flags |= VM_VPAGES;
    • array_size가 1 페이지를 초과하는 경우 해당 노드의 highmem을 포함한 zone에서 array_size 만큼의 공간을 할당받는다.
    • vmalloc() 함수가 진행 중에 nest되어 호출되는 상황이다.
    • area의 플래그 정보에 VM_VPAGES를 설정하여 할당 받은 page descriptor 배열이 있다는 것을 나타낸다.
  • } else {  pages = kmalloc_node(array_size, nested_gfp, node); }
    • array_size가 1페이지 이내인 경우 kmalloc_node() 함수를 사용하여 slub object를 할당받아 page descriptor 배열을 구성하게 한다.
  • area->pages = pages;
    • area가 사용하는 page descriptor 배열을 가리키게 한다.(vm 전용 mem_map)
  • if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; }
    • 페이지 할당이 안된 경우 vmalloc을 포기하고 해제하고  null을 반환한다.
  • for (i = 0; i < area->nr_pages; i++) { struct page *page; if (node == NUMA_NO_NODE) page = alloc_page(alloc_mask); else page = alloc_pages_node(node, alloc_mask, order);
    • 페이지 수 만큼 루프를 돌며 1개의 물리 페이지를 할당 받는다.
  • if (unlikely(!page)) { area->nr_pages = i; goto fail; }
    • 페이지의 할당이 실패한 경우 할당 받은 페이지 수 만큼을 area->nr_pages에 기록하고 fail 레이블로 이동한다.
  • area->pages[i] = page; if (gfp_mask & __GFP_WAIT) cond_resched();
    • page descriptor 배열에 할당 받은 각 페이지를 연결한다. __GFP_WAIT 플래그를 사용한 경우 현재 태스크보다 높은 우선순위의 리스케쥴링 요청이 발생된 경우 sleep한다.
  • if (map_vm_area(area, prot, pages)) goto fail; return area->addr;
    • 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 플래그를 제거한다.

 

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)
 *
 *      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);

        if (!addr)
                return;
        if (unlikely(in_interrupt())) {
                struct vfree_deferred *p = this_cpu_ptr(&vfree_deferred);
                if (llist_add((struct llist_node *)addr, &p->list))
                        schedule_work(&p->wq);
        } else
                __vunmap(addr, 1);
}
EXPORT_SYMBOL(vfree);

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

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

 

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

vfree-1

 

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 *llnode = llist_del_all(&p->list);
        while (llnode) {
                void *p = llnode;
                llnode = llist_next(llnode);
                __vunmap(p, 1);
        }
}

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

 

참고

vmalloc_init()

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

  • vmalloc() & vfree()
    • 연속된 가상 주소 메모리 할당과 메모리 해제
  • vmap() & vunmap()
    • 가상 주소의 매핑과 해제
      • 현재 vmalloc address space 또는 module address space를 사용

 

다음 그림은 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;
}

possible cpu 수 만큼 루프를 돌며 per-cpu vmap_block_queue 및 per-cpu vfree_deferred 구조체 변수를 초기화하고 전역 vmlist에 등록되어 있는 수 만큼 vmap_area 구조체를 할당 받아 초기화하여 vmap_area에 추가한다.

  • per-cpu vmap_block_queue
    • 할당 및 flushing 목적으로 free 및 dirty vmap block 큐로 사용된다.
  • per-cpu vfree_deferred
    • 인터럽트 처리 중에 vfree() 처리를 지연시킬 목적으로 사용되는 대기 리스크
  • vmlist
    • vm_area_register_early() 함수를 통해 vm이 등록되는데 pcpu_page_first_chunk()를 만들 때 사용된다.
      • 그 외 x86에서 xen ops 드라이버에서 호출 될때에도 사용된다.

 

참고

Zoned Allocator -7- (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 zone *zone,
                                        unsigned long nr_reclaimed,
                                        unsigned long nr_scanned,
                                        struct scan_control *sc)
{
        unsigned long pages_for_compaction;
        unsigned long inactive_lru_pages;

        /* 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_REPEAT) {
                /*
                 * For __GFP_REPEAT 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_REPEAT caller really wants to succeed
                 */
                if (!nr_reclaimed && !nr_scanned)
                        return false;
        } else {
                /*
                 * For non-__GFP_REPEAT 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 = (2UL << sc->order);
        inactive_lru_pages = zone_page_state(zone, NR_INACTIVE_FILE);
        if (get_nr_swap_pages() > 0)
                inactive_lru_pages += zone_page_state(zone, 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 */
        switch (compaction_suitable(zone, sc->order, 0, 0)) {
        case COMPACT_PARTIAL:
        case COMPACT_CONTINUE:
                return false;
        default:
                return true;
        }
}

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

  • if (!in_reclaim_compaction(sc)) return false;
    • reclaim/compaction 모드가 아니면 처리를 중단한다.
  • if (sc->gfp_mask & __GFP_REPEAT) { if (!nr_reclaimed && !nr_scanned) return false;
    • __GFP_REPEAT 옵션이더라도 reclaimed 페이지와 scanned 페이지가 없는 경우 false를 반환한다.
  • } else { if (!nr_reclaimed) return false; }
    • __GFP_REPEAT 옵션이 없고 reclaimed 페이지가 없는 경우 false를 반환한다.
  • pages_for_compaction = (2UL << sc->order);
    • order 페이지의 두 배만큼
  • if (get_nr_swap_pages() > 0) inactive_lru_pages += zone_page_state(zone, NR_INACTIVE_ANON);
    • swap 페이지가 있는 경우 inactive_lru_pages에 inactive anon 페이지 수를 더한다.
  • if (sc->nr_reclaimed < pages_for_compaction && inactive_lru_pages > pages_for_compaction) return true;
    • order 페이지의 2배보다 회수된 페이지가  작으면서 처리할 inactive_lru_pages가 그 보다 많은 경우 true를 반환한다.
  • switch (compaction_suitable(zone, sc->order, 0, 0)) { case COMPACT_PARTIAL: case COMPACT_CONTINUE: return false; default: return true; }
    • compaction이 필요한 경우 true를 반환한다.
      • COMPACT_PARTIAL 또는 COMPACT_CONTINUE가 아닌 경우 true를 반환한다.

 

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를 반환한다.

  • 우선 순위를 2번 이상 높여 반복 수행 중이거나 order가 PAGE_ALLOC_COSTLY_ORDER(3)보다 높은 경우 true를 반환한다.

 

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;

        /* After successful reclaim, reconsider all zones for allocation */
        if (IS_ENABLED(CONFIG_NUMA))
                zlc_clear_zones_full(ac->zonelist);

retry:
        page = get_page_from_freelist(gfp_mask, order,
                                        alloc_flags & ~ALLOC_NO_WATERMARKS, 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) {
                drain_all_pages(NULL);
                drained = true;
                goto retry;
        }

        return page;
}

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

  • *did_some_progress = __perform_reclaim(gfp_mask, order, ac); if (unlikely(!(*did_some_progress))) return NULL;
    • 페이지를 회수하며 작은 확률로 회수한 페이지가 없는 경우 null을 반환한다.
  • if (IS_ENABLED(CONFIG_NUMA)) zlc_clear_zones_full(ac->zonelist);
    • reclaim이 성공적이면 모든 zone의 할당 허용을 고려한다.
  • retry: page = get_page_from_freelist(gfp_mask, order, alloc_flags & ~ALLOC_NO_WATERMARKS, ac);
    • ALLOC_NO_WATERMARKS를 제거한 채로 order 페이지 할당을 요청한다.
  • if (!page && !drained) { drain_all_pages(NULL); drained = true; goto retry;  }
    • 페이지 할당이 실패하였고 첫 실패인 경우 pcp 캐시를 비워 버디시스템에 free 페이지를 확보한 후 재시도를 한다.

 

다음 그림은 direct reclaim을 통해 페이지를 회수하는 과정을 보여준다. 실제 회수 과정인 shrink_zones()는 별도의 그림에서 자세히 표현한다.

__alloc_pages_direct_reclaim-1

 

__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;

        cond_resched();

        /* We now go into synchronous reclaim */
        cpuset_memory_pressure_bump();
        current->flags |= PF_MEMALLOC;
        lockdep_set_current_reclaim_state(gfp_mask);
        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;
        lockdep_clear_current_reclaim_state();
        current->flags &= ~PF_MEMALLOC;

        cond_resched();

        return progress;
}

페이지를 회수한다.

  •  cpuset_memory_pressure_bump();
    • 전역 cpuset_memory_pressure_enabled가 설정된 경우 현재 태스크 cpuset의 frequency meter를 업데이트한다.
    • 루트 cpuset에 있는 memory_pressure_enabled 파일을 1로 설정하여 사용한다.
  •  current->flags |= PF_MEMALLOC;
    • reclaim을 하는 동안 잠시 현재 태스크의 플래그에 PF_MEMALLOC를 설정한다.
  • reclaim_state.reclaimed_slab = 0;
    • reclaimed_slab 카운터를 0으로 리셋한다.
  • progress = try_to_free_pages(ac->zonelist, order, gfp_mask, ac->nodemask);
    • 페이지를 회수하고 회수한 페이지 수를 알아온다.
  • current->reclaim_state = NULL;
    • 현재 태스크의 reclaim_state에 null을 대입한다.
  • current->flags &= ~PF_MEMALLOC;
    • 현재 태스크의 플래그에 reclaim을 하는 동안 잠시 설정해두었던 PF_MEMALLOC을 제거한다.

 

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 = (gfp_mask = memalloc_noio_flags(gfp_mask)),
                .order = order,
                .nodemask = nodemask,           
                .priority = DEF_PRIORITY,
                .may_writepage = !laptop_mode,
                .may_unmap = 1,
                .may_swap = 1,
        }; 

        /*
         * 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(gfp_mask, zonelist, nodemask))
                return 1;

        trace_mm_vmscan_direct_reclaim_begin(order,
                                sc.may_writepage,
                                gfp_mask);

        nr_reclaimed = do_try_to_free_pages(zonelist, &sc);

        trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);

        return nr_reclaimed;
}

페이지 회수(Reclaim)를 시도하고 회수된 페이지 수를 반환한다. free page가pfmemalloc reserve의 절반 이상 확보될 때까지 태스크가 최대 1초 동안 스로틀링(sleep)될 수 있다.

  • .gfp_mask = (gfp_mask = memalloc_noio_flags(gfp_mask)),
    • gfp_mask에서 __GFP_IO 및 __GFP_FS를 제거한다.
  • if (throttle_direct_reclaim(gfp_mask, zonelist, nodemask)) return 1;
    • free 페이지가 pfmemalloc reserve의 절반에 도달할 때까지 태스크를 스로틀링(noio를 사용하는 경우 최대 1초 sleep)시킨다. 스로틀링 중에 fatal 시그널을 전달 받은 경우 reclaim에 진입하지 않게 한다. 이러한 경우 OOM은 발생하지 않는다.
  •  nr_reclaimed = do_try_to_free_pages(zonelist, &sc);
    • 페이지를 회수해온다.

 

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 (pfmemalloc_watermark_ok(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,
                        pfmemalloc_watermark_ok(pgdat), HZ);

                goto check_pending;
        }

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

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

out:
        return false;
}

유저 스레드인 경우 free 페이지가 pfmemalloc reserve의 절반 이상에 도달할 때까지 태스크를 스로틀링(대기)시킨다. SIGKILL 시그널이 처리 지연된 경우 true를 반환하고 그렇지 않은 경우 false를 반환한다.

  • if (current->flags & PF_KTHREAD) goto out;
    • 커널 스레드인 경우 처리를 중단한다.
  • if (fatal_signal_pending(current)) goto out;
    • SIGKILL 시그널이 처리 지연된 경우 처리를 중단한다.
  • for_each_zone_zonelist_nodemask(zone, z, zonelist, gfp_zone(gfp_mask), nodemask) { if (zone_idx(zone) > ZONE_NORMAL) continue;
    • 요청한 노드와 zone 이하의 zonelist에서 ZONE_NORMAL 이상의 zone은 skip
  • pgdat = zone->zone_pgdat; if (pfmemalloc_watermark_ok(pgdat)) goto out; break;
    • zonelist에서 사용 가능한 첫 zone이 있는 노드에서 pfmemalloc reserve가 ok 되지 않는 경우 처리를 포기한다.
      • ZONE_NORMAL 또는 그 이하의 zone에서 free page 수가 min 워터마크 2배 만큼이 안되면 처리를 포기한다.
  •  if (!pgdat) goto out;
    • 사용할 수 있는 zone이 없는 경우 처리를 포기한다.
  • count_vm_event(PGSCAN_DIRECT_THROTTLE);
    • PGSCAN_DIRECT_THROTTLE stat을 증가시킨다.
  • if (!(gfp_mask & __GFP_FS)) { wait_event_interruptible_timeout(pgdat->pfmemalloc_wait, pfmemalloc_watermark_ok(pgdat), HZ); goto check_pending; }
    • __GFP_FS가 포함되지 않은 경우 free 페이지가 pfmemalloc reserve의 절반 이상이 될 때까지 최대 1초간 기다린 후 check_pending 레이블로 이동한다.
    • 파일 시스템을 이용하여 swap 하지 않는 경우에는 최대 1초 까지만 메모리가 확보될 때까지 기다리도록 스로틀링(sleep)한다..
  • wait_event_killable(zone->zone_pgdat->pfmemalloc_wait, pfmemalloc_watermark_ok(pgdat));
    • kswapd가 free page를 확보할 때까지 sleep하고 있는다.
      • zone이 있는 노드의 pfmemalloc_wait에 이벤트가 올 때마다 pfmemalloc reserve가 ok될 때까지 sleep 한다.
      • 현재 태스크를 zone이 있는 노드의 pfmemalloc_wait 큐에 추가한 후 sleep에 들어간다. 추후 kswapd가 free page를 확보해 pfmemalloc reserve ok 되는 경우 pfmemalloc_wait에서 sleep하고 있는 현재 태스크를 깨운다. 이 때 태스크가 깨어나서 pfmemalloc reserve가 ok 되었는지 다시 확인하고 ok되지 않은 경우 다시 반복 sleep한다.
    • 파일 시스템을 사용하여 swap 하는데 네트워크 기반의 파일 시스템을 사용하는 경우도 있기 때문에 메모리가 확보될 때까지 무한 기다리는데 태스크의 종료 시그널은 허용된다.
  • check_pending: if (fatal_signal_pending(current)) return true;
    • SIGKILL 시그널이 처리 지연되는 경우 true를 반환하고 그렇지 않은 경우 false를 반환한다.

 

pfmemalloc_watermark_ok()

mm/vmscan.c

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

        for (i = 0; i <= ZONE_NORMAL; i++) {
                zone = &pgdat->node_zones[i];
                if (!populated_zone(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->classzone_idx = min(pgdat->classzone_idx,
                                                (enum zone_type)ZONE_NORMAL);
                wake_up_interruptible(&pgdat->kswapd_wait);
        }

        return wmark_ok;
}

free 페이지가 pfmemalloc reserve의 절반 이상에 도달한 경우 true를 반환하고, 그렇지 않은 경우 kswapd를 깨워 페이지 회수 프로세스가 백그라운드에서 동작하도록 한다.

  • for (i = 0; i <= ZONE_NORMAL; i++) { zone = &pgdat->node_zones[i];  if (!populated_zone(zone)) continue; pfmemalloc_reserve += min_wmark_pages(zone); free_pages += zone_page_state(zone, NR_FREE_PAGES);
    • 지정된 노드의 ZONE_NORMAL 이하의 활성화된 zone의 min 워터마크 페이지 수의 합으로 pfmemealloc_reserve를 구하고, free page의 합으로 free_pages를 구한다.
  • if (!pfmemalloc_reserve) return true;
    • pfmemalloc_reserve가 0인 경우 true를 반환하여 throttle을 하지 않도록 한다.
  • wmark_ok = free_pages > pfmemalloc_reserve / 2;
    • free_pages가 pfmemalloc_reserve의 절반보다 큰 경우 throttle을 하지 않도록 한다.
  • if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) { pgdat->classzone_idx = min(pgdat->classzone_idx, (enum zone_type)ZONE_NORMAL); wake_up_interruptible(&pgdat->kswapd_wait); }
    • wmark_ok가 false이면서 kswapd가 대기하는 경우 노드의 classzone_idx가 최대 ZONE_NORMAL이 넘지 않도록 조정하고 노드에서 대기하는 kswapd를 깨운다.

 

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;
        unsigned long total_scanned = 0;
        unsigned long writeback_threshold;
        bool zones_reclaimable;
retry:
        delayacct_freepages_start();

        if (global_reclaim(sc))
                count_vm_event(ALLOCSTALL);

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

                total_scanned += sc->nr_scanned;
                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;

                /*
                 * Try to write back as many pages as we just scanned.  This
                 * tends to cause slow streaming writers to write data to the
                 * disk smoothly, at the dirtying rate, which is nice.   But
                 * that's undesirable in laptop mode, where we *want* lumpy
                 * writeout.  So in laptop mode, write out the whole world.
                 */
                writeback_threshold = sc->nr_to_reclaim + sc->nr_to_reclaim / 2;
                if (total_scanned > writeback_threshold) { 
                        wakeup_flusher_threads(laptop_mode ? 0 : total_scanned,
                                                WB_REASON_TRY_TO_FREE_PAGES);
                        sc->may_writepage = 1;
                }
        } while (--sc->priority >= 0);
  • retry: delayacct_freepages_start();
    • 페이지 회수에 소요되는 시간을 계량하기 위해 시작한다.
    • 참고: delayacct_init() | 문c
  • if (global_reclaim(sc)) count_vm_event(ALLOCSTALL);
    • global reclaim을 사용해야하는 경우 ALLOCSTALL stat을 증가시킨다.
  • do { vmpressure_prio(sc->gfp_mask, sc->target_mem_cgroup, sc->priority);
    • 우선 순위가 높아져 스캔 depth가 깊어지는 경우 vmpressure 정보를 갱신한다.
  • sc->nr_scanned = 0;  zones_reclaimable = shrink_zones(zonelist, sc);
    • 스캔 건 수를 리셋시키고 페이지를 회수하고 회수한 건 수를 알아온다.
  • total_scanned += sc->nr_scanned; if (sc->nr_reclaimed >= sc->nr_to_reclaim) break;
    • 스캔 건 수를 누적시키고 회수 건 수가 회수해야 할 건 수보다 큰 경우 처리를 위해 루프에서 벗어난다.
  • if (sc->compaction_ready) break;
    • compaction이 준비된 경우 처리를 위해 루프에서 벗어난다.
  • if (sc->priority < DEF_PRIORITY – 2) sc->may_writepage = 1;
    • priority를 내려서 반복하여 처리하는 경우 writepage 기능을 설정한다.
  • writeback_threshold = sc->nr_to_reclaim + sc->nr_to_reclaim / 2;
    • writeback_threshold에 회수해야 할 건 수의 1.5배를 대입한다.
  • if (total_scanned > writeback_threshold) { wakeup_flusher_threads(laptop_mode ? 0 : total_scanned, WB_REASON_TRY_TO_FREE_PAGES); sc->may_writepage = 1; }
    • 스캔 건 수가 writeback_threshold보다 큰 경우
  • } while (–sc->priority >= 0);
    • 우선 순위를 최고까지 높여가며(0으로 갈수록 높아진다) 루프를 돈다.

 

        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->may_thrash) {
                sc->priority = initial_priority;
                sc->may_thrash = 1;
                goto retry;
        }

        /* Any of the zones still reclaimable?  Don't OOM. */
        if (zones_reclaimable)
                return 1;

        return 0;
}
  • delayacct_freepages_end();
    • 페이지 회수에 소요되는 시간을 계량한다.
  • if (sc->nr_reclaimed) return sc->nr_reclaimed;
    • 회수한 적이 있는 경우 그 값을 반환한다.
  • if (sc->compaction_ready) return 1;
    • compaction이 준비된 경우 1을 반환한다.
  • if (!sc->may_thrash) { sc->priority = initial_priority; sc->may_thrash = 1; goto retry; }
    • 재시도가 처음인 경우 priority를 다시 원래 요청 priority로 바꾸고 재시도한다.
  • if (zones_reclaimable) return 1;
    • zones_reclaimable이 있는 경우 1을 반환하고 그렇지 않은 경우 0을 반환한다.

 

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

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으로 리셋한다.

  • time_t now = get_seconds(); time_t ticks = now – fmp->time; if (ticks == 0) return;
    • fmeter에 기록된 초(second)로부터 경과한 초를 알아온다.
  • ticks = min(FM_MAXTICKS, ticks);
    • ticks는 최대 99까지로 제한한다.
  • while (ticks– > 0) fmp->val = (FM_COEF * fmp->val) / FM_SCALE;
    • ticks 만큼 fmp->val *= 93.3%를 반복한다.
  • fmp->time = now;
    • 다음 계산을 위해 현재 초로 갱신한다.
  • fmp->val += ((FM_SCALE – FM_COEF) * fmp->cnt) / FM_SCALE;
    • fmp->val에 fmp->cnt x 6.7%를 더한다.
  • fmp->cnt = 0;
    • 이벤트 수를 0으로 리셋한다.

 

fmeter 구조체

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)

 

VM Pressure

 

/*
 * The window size (vmpressure_win) is the number of scanned pages before
 * we try to analyze scanned/reclaimed ratio. So the window is used as a
 * rate-limit tunable for the "low" level notification, and also for
 * averaging the ratio for medium/critical levels. Using small window
 * sizes can cause lot of false positives, but too big window size will
 * delay the notifications.
 *
 * As the vmscan reclaimer logic works with chunks which are multiple of
 * SWAP_CLUSTER_MAX, it makes sense to use it for the window size as well.
 *
 * TODO: Make the window size depend on machine size, as we do for vmstat
 * thresholds. Currently we set it to 512 pages (2MB for 4KB pages).
 */
static const unsigned long vmpressure_win = SWAP_CLUSTER_MAX * 16;

/*
 * These thresholds are used when we account memory pressure through
 * scanned/reclaimed ratio. The current values were chosen empirically. In
 * essence, they are percents: the higher the value, the more number
 * unsuccessful reclaims there were.
 */
static const unsigned int vmpressure_level_med = 60;
static const unsigned int vmpressure_level_critical = 95;
  • vmpressure_win
    • SWAP_CLUSTER_MAX(32) * 16 = 256 페이지로 설정되어 있다.
    • 이 윈도우 사이즈는 scanned/reclaim 비율을 분석을 시도하기 전에 사용하는 scanned 페이지 수이다.
    •  low 레벨 notification에 사용되고 medium/critical 레벨의 평균 비율을 위해서도 사용된다.
  • vmpressure_level_med
    • scanned/reclaimed 비율로 메모리 pressure 계량시 사용되는 medium 레벨의 스레졸드 값
  • vmpressure_level_critical
    • scanned/reclaimed 비율로 메모리 pressure 계량시 사용되는 critical 레벨의 스레졸드 값

 

vmpressure_prio()

mm/vmpressure.c

/**
 * vmpressure_prio() - Account memory pressure through reclaimer priority level
 * @gfp:        reclaimer's gfp mask
 * @memcg:      cgroup memory controller handle
 * @prio:       reclaimer's priority
 *
 * This function should be called from the reclaim path every time when
 * the vmscan's reclaiming priority (scanning depth) changes.
 *
 * This function does not return any value.
 */
void vmpressure_prio(gfp_t gfp, struct mem_cgroup *memcg, int prio)
{
        /*
         * We only use prio for accounting critical level. For more info
         * see comment for vmpressure_level_critical_prio variable above.
         */
        if (prio > vmpressure_level_critical_prio)
                return;

        /*
         * OK, the prio is below the threshold, updating vmpressure
         * information before shrinker dives into long shrinking of long
         * range vmscan. Passing scanned = vmpressure_win, reclaimed = 0
         * to the vmpressure() basically means that we signal 'critical'
         * level.
         */
        vmpressure(gfp, memcg, vmpressure_win, 0);
}

우선 순위가 높아져 스캔 depth가 깊어지는 경우 vmpressure 정보를 갱신한다.

  • if (prio > vmpressure_level_critical_prio) return;
    • 요청 우선 순위가 vmpressure_level_critical_prio(3)보다 낮아 함수를 빠져나간다.
      • prio는 낮을 수록 우선 순위가 높다.
  • vmpressure(gfp, memcg, vmpressure_win, 0);
    • 스레졸드 이하로 prio가 떨어진 경우, 즉 우선 순위가 높아진 경우 shrinker가 오랫 동안 스캔하기 전에 vmpressure 정보를 업데이트한다.

 

다음 그림은 vmpressure_prio() 함수가 처리되는 과정을 보여준다.

vmpressure_prio-1

 

vmpressure()

mm/vmpressure.c

/**
 * vmpressure() - Account memory pressure through scanned/reclaimed ratio
 * @gfp:        reclaimer's gfp mask
 * @memcg:      cgroup memory controller handle
 * @scanned:    number of pages scanned
 * @reclaimed:  number of pages reclaimed
 *
 * This function should be called from the vmscan reclaim path to account
 * "instantaneous" memory pressure (scanned/reclaimed ratio). The raw
 * pressure index is then further refined and averaged over time.
 *
 * This function does not return any value.
 */
void vmpressure(gfp_t gfp, struct mem_cgroup *memcg,
                unsigned long scanned, unsigned long reclaimed)
{
        struct vmpressure *vmpr = memcg_to_vmpressure(memcg);

        /*
         * Here we only want to account pressure that userland is able to
         * help us with. For example, suppose that DMA zone is under
         * pressure; if we notify userland about that kind of pressure,
         * then it will be mostly a waste as it will trigger unnecessary
         * freeing of memory by userland (since userland is more likely to
         * have HIGHMEM/MOVABLE pages instead of the DMA fallback). That
         * is why we include only movable, highmem and FS/IO pages.
         * Indirect reclaim (kswapd) sets sc->gfp_mask to GFP_KERNEL, so
         * we account it too.
         */
        if (!(gfp & (__GFP_HIGHMEM | __GFP_MOVABLE | __GFP_IO | __GFP_FS)))
                return;

        /*
         * If we got here with no pages scanned, then that is an indicator
         * that reclaimer was unable to find any shrinkable LRUs at the
         * current scanning depth. But it does not mean that we should
         * report the critical pressure, yet. If the scanning priority
         * (scanning depth) goes too high (deep), we will be notified
         * through vmpressure_prio(). But so far, keep calm.
         */
        if (!scanned)
                return;

        spin_lock(&vmpr->sr_lock);
        vmpr->scanned += scanned;
        vmpr->reclaimed += reclaimed;
        scanned = vmpr->scanned;
        spin_unlock(&vmpr->sr_lock);

        if (scanned < vmpressure_win)
                return;
        schedule_work(&vmpr->work);
}

scaned 및 reclaimed 비율로 메모리 pressure를 계량한다.

  • struct vmpressure *vmpr = memcg_to_vmpressure(memcg);
    • 요청한 memcg의 vmpressure 정보를 반환한다.
  • if (!(gfp & (__GFP_HIGHMEM | __GFP_MOVABLE | __GFP_IO | __GFP_FS))) return;
    • highmem, movable, FS/IO 페이지가 설정되지 않으면 pressure 계량을 하지 않는다.
    • userland만 계측을 하려한다.
  • if (!scanned) return;
    • 인수 scanned가 0인 경우 함수를 중단한다.
  • vmpr->scanned += scanned; vmpr->reclaimed += reclaimed;
    • scanned와 reclaimed 각각 그 만큼 증가시킨다.
  • if (scanned < vmpressure_win) return;
    • vmpr->scanned가 vmpressure_win 보다 작은 경우 함수를 중단한다.
  • schedule_work(&vmpr->work);
    • vmpr->work에 등록한 작업을 실행시킨다.
      • vmpressure_work_fn()

 

memcg_to_vmpressure()

mm/memcontrol.c

/* Some nice accessors for the vmpressure. */
struct vmpressure *memcg_to_vmpressure(struct mem_cgroup *memcg)
{
        if (!memcg)
                memcg = root_mem_cgroup;
        return &memcg->vmpressure;   
}

요청한 memcg의 vmpressure 정보를 반환한다. memcg가 지정되지 않은 경우 root memcg의 vmpressure를 반환한다.

 

참고

 

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

 

isolate_lru_pages()

mm/vmscan.c

/*
 * zone->lru_lock is heavily contended.  Some of the functions that
 * shrink the lists perform better by taking out a batch of pages
 * and working on them outside the LRU lock.
 *
 * For pagecache intensive workloads, this function is the hottest
 * spot in the kernel (apart from copy_*_user functions).
 *
 * Appropriate locks must be held before calling this function.
 *
 * @nr_to_scan: The number of pages to look through on the list.
 * @lruvec:     The LRU vector to pull pages from.
 * @dst:        The temp list to put pages on to.
 * @nr_scanned: The number of pages that were scanned.
 * @sc:         The scan_control struct for this reclaim session
 * @mode:       One of the LRU isolation modes
 * @lru:        LRU list id for isolating
 *
 * returns how many pages were moved onto *@dst.
 */
static unsigned long isolate_lru_pages(unsigned long nr_to_scan,
                struct lruvec *lruvec, struct list_head *dst,
                unsigned long *nr_scanned, struct scan_control *sc,
                isolate_mode_t mode, enum lru_list lru)
{
        struct list_head *src = &lruvec->lists[lru];
        unsigned long nr_taken = 0;
        unsigned long scan;

        for (scan = 0; scan < nr_to_scan && !list_empty(src); scan++) {
                struct page *page;
                int nr_pages;

                page = lru_to_page(src);
                prefetchw_prev_lru_page(page, src, flags);

                VM_BUG_ON_PAGE(!PageLRU(page), page);

                switch (__isolate_lru_page(page, mode)) {
                case 0:
                        nr_pages = hpage_nr_pages(page);
                        mem_cgroup_update_lru_size(lruvec, lru, -nr_pages);
                        list_move(&page->lru, dst);
                        nr_taken += nr_pages;
                        break;

                case -EBUSY:
                        /* else it is being freed elsewhere */
                        list_move(&page->lru, src);
                        continue;

                default:
                        BUG();
                }
        }

        *nr_scanned = scan;
        trace_mm_vmscan_lru_isolate(sc->order, nr_to_scan, scan,
                                    nr_taken, mode, is_file_lru(lru));
        return nr_taken;
}

지정한 lru 벡터 리스트로부터 nr_to_scan 만큼 스캔을 시도하여 분리된 페이지는 dst 리스트에 담고 분리된 페이지 수를 반환한다.

  • struct list_head *src = &lruvec->lists[lru];
    • 작업할 lru 벡터 리스트
  • for (scan = 0; scan < nr_to_scan && !list_empty(src); scan++) {
    • lru 리스트에 엔트리가 있는 한 nr_to_scan 까지 스캔을 반복한다.
  • page = lru_to_page(src); prefetchw_prev_lru_page(page, src, flags);
    • 이전 페이지의 플래그 값을 캐시에 미리 로드한다.
  • switch (__isolate_lru_page(page, mode)) {
    • 한 페이지의 분리를 시도한다.
      • 성공 시 0,
      • 관련 없는 페이지의 분리를 시도하는 경우 -EINVAL
      • 모드 옵션으로 인해 당장 분리할 수 없는 경우 -EBUSY
  • case 0: nr_pages = hpage_nr_pages(page); mem_cgroup_update_lru_size(lruvec, lru, -nr_pages); list_move(&page->lru, dst); nr_taken += nr_pages; break;
    • 페이지 분리가 성공인 경우 memcg용 lru 벡터의 사이즈(갯수)를 갱신하고 페이지를 dst 리스트에 옮기고 nr_taken 만큼 페이지 수를 추가한다.
      • 1개의 trans huge 페이지로 구성된 페이지는 HPAGE_PMD_NR(아키텍처에 따라 다르다) 수로 구성된다.
  • case -EBUSY: list_move(&page->lru, src); continue;
    • 곧 free될 예정으로 잠시 보류하기 위해 현재 작업중인 lru 벡터 리스트의 선두(hot)로 다시 되돌린다.
  • *nr_scanned = scan; return nr_taken;
    • scan 건 수를 출력 인수 nr_scanned에 대입하고 isolation된 페이지 수를 반환한다.

 

__isolate_lru_page()

mm/vmscan.c

/*
 * Attempt to remove the specified page from its LRU.  Only take this page
 * if it is of the appropriate PageActive status.  Pages which are being
 * freed elsewhere are also ignored.
 *
 * page:        page to consider
 * mode:        one of the LRU isolation modes defined above
 *
 * returns 0 on success, -ve errno on failure.
 */
int __isolate_lru_page(struct page *page, isolate_mode_t mode)
{
        int ret = -EINVAL;

        /* Only take pages on the LRU. */
        if (!PageLRU(page))
                return ret;

        /* Compaction should not handle unevictable pages but CMA can do so */
        if (PageUnevictable(page) && !(mode & ISOLATE_UNEVICTABLE))
                return ret;

        ret = -EBUSY;

        /*
         * To minimise LRU disruption, the caller can indicate that it only
         * wants to isolate pages it will be able to operate on without
         * blocking - clean pages for the most part.
         *
         * ISOLATE_CLEAN means that only clean pages should be isolated. This
         * is used by reclaim when it is cannot write to backing storage
         *
         * ISOLATE_ASYNC_MIGRATE is used to indicate that it only wants to pages
         * that it is possible to migrate without blocking
         */
        if (mode & (ISOLATE_CLEAN|ISOLATE_ASYNC_MIGRATE)) {
                /* All the caller can do on PageWriteback is block */
                if (PageWriteback(page))
                        return ret;

                if (PageDirty(page)) {
                        struct address_space *mapping;

                        /* ISOLATE_CLEAN means only clean pages */
                        if (mode & ISOLATE_CLEAN)
                                return ret;

                        /*
                         * Only pages without mappings or that have a
                         * ->migratepage callback are possible to migrate
                         * without blocking
                         */
                        mapping = page_mapping(page);
                        if (mapping && !mapping->a_ops->migratepage)
                                return ret;
                }
        }

        if ((mode & ISOLATE_UNMAPPED) && page_mapped(page))
                return ret;

        if (likely(get_page_unless_zero(page))) {
                /*
                 * Be careful not to clear PageLRU until after we're
                 * sure the page is not being freed elsewhere -- the
                 * page release code relies on it.
                 */
                ClearPageLRU(page);
                ret = 0;
        }

        return ret;
}

요청 페이지를 분리를 시도하고 성공 시 0을 반환한다. 만일 관련 없는 페이지의 분리를 시도하는 경우 -EINVAL을 반환하고, 모드 옵션으로 인해 당장 분리할 수 없는 경우 -EBUSY를 반환한다.

  • if (!PageLRU(page)) return ret;
    • lru 페이지가 아닌 경우 분리를 포기한다.
  • if (PageUnevictable(page) && !(mode & ISOLATE_UNEVICTABLE)) return ret;
    • unevictable 페이지이면서 모드에 unevictable의 분리를 허용하지 않은 경우 분리를 포기한다.
  • if (mode & (ISOLATE_CLEAN|ISOLATE_ASYNC_MIGRATE)) { if (PageWriteback(page)) return ret;
    • 모드에 clean 또는 비동기 migration이 있는 경우 writeback 페이지는 분리를 포기한다.
  • if (PageDirty(page)) { struct address_space *mapping; if (mode & ISOLATE_CLEAN) return ret;
    • dirty 페이지인 경우 모드에 clean 요청이 있는 경우 분리를 포기한다.
  • mapping = page_mapping(page); if (mapping && !mapping->a_ops->migratepage) return ret;
    • mapping을 알아오고 해당 핸들러 함수 migratepage()를 수행한 결과가 실패하는 경우 분리를 포기한다.
  • if ((mode & ISOLATE_UNMAPPED) && page_mapped(page)) return ret;
    • 모드에 unmapped를 요청한 경우에는 mapped 페이지는 분리를 포기한다.
  • if (likely(get_page_unless_zero(page))) { ClearPageLRU(page); ret = 0; }
    • 높은 확률로 참조카운터를 증가시키며 이 전에 사용하지 않은 경우 페이지의 lru 플래그 비트를 클리어하고 성공적으로 리턴한다.

 

mem_cgroup_update_lru_size()

mm/memcontrol.c

/**
 * mem_cgroup_update_lru_size - account for adding or removing an lru page
 * @lruvec: mem_cgroup per zone lru vector
 * @lru: index of lru list the page is sitting on
 * @nr_pages: positive when adding or negative when removing
 *
 * This function must be called when a page is added to or removed from an
 * lru list.
 */
void mem_cgroup_update_lru_size(struct lruvec *lruvec, enum lru_list lru,
                                int nr_pages)
{
        struct mem_cgroup_per_zone *mz;
        unsigned long *lru_size;

        if (mem_cgroup_disabled())
                return;

        mz = container_of(lruvec, struct mem_cgroup_per_zone, lruvec);
        lru_size = mz->lru_size + lru;
        *lru_size += nr_pages;
        VM_BUG_ON((long)(*lru_size) < 0);
}

memcg용 lru 벡터의 사이즈에 요청한 nr_pages를 더한 값으로 udpate한다.

 

move_active_pages_to_lru()

mm/vmscan.c

/*
 * This moves pages from the active list to the inactive list.
 *
 * We move them the other way if the page is referenced by one or more
 * processes, from rmap.
 *
 * If the pages are mostly unmapped, the processing is fast and it is
 * appropriate to hold zone->lru_lock across the whole operation.  But if
 * the pages are mapped, the processing is slow (page_referenced()) so we
 * should drop zone->lru_lock around each page.  It's impossible to balance
 * this, so instead we remove the pages from the LRU while processing them.
 * It is safe to rely on PG_active against the non-LRU pages in here because
 * nobody will play with that bit on a non-LRU page.
 *
 * The downside is that we have to touch page->_count against each page.
 * But we had to alter page->flags anyway.
 */

static void move_active_pages_to_lru(struct lruvec *lruvec,
                                     struct list_head *list,
                                     struct list_head *pages_to_free,
                                     enum lru_list lru)
{
        struct zone *zone = lruvec_zone(lruvec);
        unsigned long pgmoved = 0;
        struct page *page;
        int nr_pages;

        while (!list_empty(list)) {
                page = lru_to_page(list);
                lruvec = mem_cgroup_page_lruvec(page, zone);

                VM_BUG_ON_PAGE(PageLRU(page), page);
                SetPageLRU(page);

                nr_pages = hpage_nr_pages(page);
                mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
                list_move(&page->lru, &lruvec->lists[lru]);
                pgmoved += nr_pages;

                if (put_page_testzero(page)) {
                        __ClearPageLRU(page);
                        __ClearPageActive(page);
                        del_page_from_lru_list(page, lruvec, lru);

                        if (unlikely(PageCompound(page))) {
                                spin_unlock_irq(&zone->lru_lock);
                                mem_cgroup_uncharge(page);
                                (*get_compound_page_dtor(page))(page);
                                spin_lock_irq(&zone->lru_lock);
                        } else
                                list_add(&page->lru, pages_to_free);
                }
        }
        __mod_zone_page_state(zone, NR_LRU_BASE + lru, pgmoved);
        if (!is_active_lru(lru))
                __count_vm_events(PGDEACTIVATE, pgmoved);
}

active 리스트에 있는 옮길 페이지를 inactive 리스트로 옮긴다. 만일 사용자가 없는 페이지인 경우 pages_to_free에 옮긴다.

  • while (!list_empty(list)) {
    • 리스트의 모든 엔트리 수 만큼 루프를 돈다.
  • lruvec = mem_cgroup_page_lruvec(page, zone);
    • 해당 페이지의 memcg용 lruvec를 알아온다.
  • SetPageLRU(page);
    • 페이지의 lru 플래그 비트를 설정한다.
  • nr_pages = hpage_nr_pages(page);
    • transparent huge page인 경우 그 페이지 수를 알아온다. 그렇지 않은 경우 1이다.
  •  mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
    • memcg용 lru 사이즈(갯수)를 갱신한다.
  • list_move(&page->lru, &lruvec->lists[lru]);
    • 페이지를 lru 벡터로 옮긴다. (hot 방향)
  • pgmoved += nr_pages;
    • pgmoved 카운터에 페이지  수 만큼 추가한다.
  • if (put_page_testzero(page)) { __ClearPageLRU(page); __ClearPageActive(page); del_page_from_lru_list(page, lruvec, lru);
    • 참조 카운터를 감소시켜 0이된 경우, 즉 사용자가 없어져 free해도 될 경우 lru 및 active 플래그 비트를 클리어하고 다시 lru 벡터 리스트에서 제거한다.
  • if (unlikely(PageCompound(page))) { spin_unlock_irq(&zone->lru_lock); mem_cgroup_uncharge(page); (*get_compound_page_dtor(page))(page); spin_lock_irq(&zone->lru_lock); } else list_add(&page->lru, pages_to_free);
    • 작은 확률로 compound 페이지인 경우 memcg에 uncharge 정보를 전달하고 compound 페이지의 파괴자를 호출한다. compound 페이지가 아닌 경우 pages_to_free 리스트에 추가하여 함수 종료 후 free를 시도한다.
  • __mod_zone_page_state(zone, NR_LRU_BASE + lru, pgmoved);
    • 지정 lru 벡터 stat에 pgmoved 만큼 추가한다.
  • if (!is_active_lru(lru)) __count_vm_events(PGDEACTIVATE, pgmoved);
    • active lru가 아닌 경우 PGDEACTIVATE stat에 pgmoved 만큼 추가한다.

 

shrink_page_list()

mm/vmscan.c

/*
 * shrink_page_list() returns the number of reclaimed pages
 */
static unsigned long shrink_page_list(struct list_head *page_list,
                                      struct zone *zone,
                                      struct scan_control *sc,
                                      enum ttu_flags ttu_flags,
                                      unsigned long *ret_nr_dirty,
                                      unsigned long *ret_nr_unqueued_dirty,
                                      unsigned long *ret_nr_congested,
                                      unsigned long *ret_nr_writeback,
                                      unsigned long *ret_nr_immediate,
                                      bool force_reclaim)
{
        LIST_HEAD(ret_pages);
        LIST_HEAD(free_pages);
        int pgactivate = 0;
        unsigned long nr_unqueued_dirty = 0;
        unsigned long nr_dirty = 0;
        unsigned long nr_congested = 0;
        unsigned long nr_reclaimed = 0;
        unsigned long nr_writeback = 0;
        unsigned long nr_immediate = 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);
                VM_BUG_ON_PAGE(page_zone(page) != zone, page);

                sc->nr_scanned++;

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

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

전달받은 페이지 리스트에 대해 shrink를 수행하고 회수된 페이지의 수를 반환한다.

 

  • while (!list_empty(page_list)) {
    • page_list의 페이지 수 만큼 루프를 돈다.
  • page = lru_to_page(page_list); list_del(&page->lru);
    • 페이지를 리스트에서 page_list에서 분리한다.
  • if (!trylock_page(page)) goto keep;
    • page lock 획득을 시도해보고 실패하는 경우 다음에 처리하도록 page_list에 다시 추가한다.
  • if (unlikely(!page_evictable(page))) goto cull_mlocked;
    • 작은 확률로 페이지가 unevictable인 경우 lruvec.lists[LRU_UNEVICTABLE]로 되돌린다.
  • if (!sc->may_unmap && page_mapped(page)) goto keep_locked;
    • mapped 페이지이지만 may_unmap 요청이 없는 경우 다음에 처리하도록 page_list에 다시 추가한다.

 

                /* Double the slab pressure for mapped and swapcache pages */
                if (page_mapped(page) || PageSwapCache(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 zone 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 &&
                     bdi_write_congested(inode_to_bdi(mapping->host))) ||
                    (writeback && PageReclaim(page)))
                        nr_congested++;
  • if (page_mapped(page) || PageSwapCache(page)) sc->nr_scanned++;
    • mapped 페이지 이거나 swapcache 페이지인 경우 nr_scanned 카운터를 증가시킨다.
  • may_enter_fs = (sc->gfp_mask & __GFP_FS) || (PageSwapCache(page) && (sc->gfp_mask & __GFP_IO));
    • 파일시스템의 이용이 허용되었거나 swapcache 페이지이면서 io가 허용된 경우 may_enter_fs에 true가 대입된다.
  • page_check_dirty_writeback(page, &dirty, &writeback); if (dirty || writeback) nr_dirty++;
    • dirty 또는 writeback 페이지인 경우 nr_dirty 카운터를 증가한다.
  • if (dirty && !writeback) nr_unqueued_dirty++;
    • dirty이면서 writeback은 없는 경우 nr_unqueued_dirty 카운터를 증가한다.
  • mapping = page_mapping(page); if (((dirty || writeback) && mapping && bdi_write_congested(inode_to_bdi(mapping->host))) || (writeback && PageReclaim(page))) nr_congested++;
    • 다음 두 가지의 경우 nr_congested 카운터를 증가한다.
      • dirty 또는 writeback이고 mapping->host inode를 담당하는 backing device가 write 혼잡 상태인 경우
      • writeback 및 reclaim 페이지인 경우

 

                /*
                 * 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 reclaim encounters a page, memcg encounters a
                 *    page that is not marked for immediate reclaim or
                 *    the caller does not have __GFP_IO. In this case mark
                 *    the page for immediate reclaim and continue scanning.
                 *
                 *    __GFP_IO is checked  because a loop driver thread 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.
                 *
                 *    Don't require __GFP_FS, since we're not going into the
                 *    FS, just waiting on its writeback completion. Worryingly,
                 *    ext4 gfs2 and xfs allocate pages with
                 *    grab_cache_page_write_begin(,,AOP_FLAG_NOFS), so testing
                 *    may_enter_fs here is liable to OOM on them.
                 *
                 * 3) memcg encounters a page that is not 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.
                 */
                if (PageWriteback(page)) {
                        /* Case 1 above */
                        if (current_is_kswapd() &&
                            PageReclaim(page) &&
                            test_bit(ZONE_WRITEBACK, &zone->flags)) {
                                nr_immediate++;
                                goto keep_locked;

                        /* Case 2 above */
                        } else if (global_reclaim(sc) ||
                            !PageReclaim(page) || !(sc->gfp_mask & __GFP_IO)) {
                                /*
                                 * 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 keep_locked;

                        /* Case 3 above */
                        } else {
                                wait_on_page_writeback(page);
                        }
                }
  • if (PageWriteback(page)) {
    • writeback 페이지인 경우
  • if (current_is_kswapd() && PageReclaim(page) && test_bit(ZONE_WRITEBACK, &zone->flags)) { nr_immediate++; goto keep_locked;
    • 현재 태스크가 kswapd이면서 writeback이 허용된 zone에서 reclaim 페이지를 만나면 nr_immediate 카운터를 증가시키고 다음에 처리하도록 page_list에 다시 추가한다.
  • } else if (global_reclaim(sc) || !PageReclaim(page) || !(sc->gfp_mask & __GFP_IO)) { SetPageReclaim(page); nr_writeback++; goto keep_locked;
    • global reclaim이거나 reclaim 페이지가 아니거나 io가 허용되지 않은 경우 페이지에 reclaim 플래그를 설정하고 nr_writeback을 증가시키고 다음에 처리하도록 page_list에 다시 추가한다.
  • } else { wait_on_page_writeback(page); }
    • 해당 페이지의 writeback이 완료될 때까지 대기한다.

 

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

                switch (references) {
                case PAGEREF_ACTIVATE:
                        goto activate_locked;
                case PAGEREF_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.
                 */
                if (PageAnon(page) && !PageSwapCache(page)) {
                        if (!(sc->gfp_mask & __GFP_IO))
                                goto keep_locked;
                        if (!add_to_swap(page, page_list))
                                goto activate_locked;
                        may_enter_fs = 1;

                        /* Adding to swap updated mapping */
                        mapping = page_mapping(page);
                }

                /*
                 * The page is mapped into the page tables of one or more
                 * processes. Try to unmap it here.
                 */
                if (page_mapped(page) && mapping) {
                        switch (try_to_unmap(page, ttu_flags)) {
                        case SWAP_FAIL:
                                goto activate_locked;
                        case SWAP_AGAIN:
                                goto keep_locked;
                        case SWAP_MLOCK:
                                goto cull_mlocked;
                        case SWAP_SUCCESS:
                                ; /* try to free the page below */
                        }
                }
  • if (!force_reclaim) references = page_check_references(page, sc);
    • force_reclaim이 아닌 경우 페이지 참조 상태를 알아온다.
  • switch (references) { case PAGEREF_ACTIVATE: goto activate_locked;
    • 활동중인 경우 페이지에 Active 비트를 설정하고 다음에 처리하도록 page_list에 다시 추가한다.
  • case PAGEREF_KEEP: goto keep_locked;
    • 다음에 처리하도록 page_list에 다시 추가한다.
  • case PAGEREF_RECLAIM: case PAGEREF_RECLAIM_CLEAN: }
    • 페이지 회수를 위해 아래 루틴을 계속 수행한다.
  • if (PageAnon(page) && !PageSwapCache(page)) {
    • anon 페이지이면서 swapcache 되지 않은 경우
  • if (!(sc->gfp_mask & __GFP_IO)) goto keep_locked;
    • io 처리를 하지 못하게한 경우 다음에 처리하도록 page_list에 다시 추가한다.
  • if (!add_to_swap(page, page_list)) goto activate_locked;
    • swap 영역에 추가(기록)하되 실패한 경우 page의 Active 플래그 비트를 설정하고 다음에 처리하도록 page_list에 다시 추가한다.
  • may_enter_fs = 1; mapping = page_mapping(page); }
    • may_enter_fs를 설정하고 매핑을 알아온다.
  • if (page_mapped(page) && mapping) {
    • mapped 페이지이면서 매핑이 있는 경우
  • switch (try_to_unmap(page, ttu_flags)) { case SWAP_FAIL: goto activate_locked;
    • 페이지 언매핑을 시도하고 실패한 경우 page의 Active 플래그 비트를 설정하고 다음에 처리하도록 page_list에 다시 추가한다.
  • case SWAP_AGAIN: goto keep_locked;
    • 다음에 처리하도록 page_list에 다시 추가한다.
  • case SWAP_MLOCK: goto cull_mlocked;
    • lruvec.lists[]로 되돌린다.
  • case SWAP_SUCCESS: }
    • 페이지가 정상적으로 unmap되었고 아래 루틴을 통해 페이지를 free 시킨다.

 

                if (PageDirty(page)) {
                        /*
                         * Only kswapd can writeback filesystem pages to
                         * avoid risk of stack overflow but only writeback
                         * if many dirty pages have been encountered.
                         */
                        if (page_is_file_cache(page) &&
                                        (!current_is_kswapd() ||
                                         !test_bit(ZONE_DIRTY, &zone->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_zone_page_state(page, NR_VMSCAN_IMMEDIATE);
                                SetPageReclaim(page);

                                goto keep_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, try to write it out here */
                        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 */
                        }
                }
  • if (PageDirty(page)) {
    • dirty 페이지인 경우
  • if (page_is_file_cache(page) && (!current_is_kswapd() || !test_bit(ZONE_DIRTY, &zone->flags))) { inc_zone_page_state(page, NR_VMSCAN_IMMEDIATE); SetPageReclaim(page); goto keep_locked; }
    • file 캐시 페이지이면서 direct reclaim 또는 zone이 dirty를 허용하지 않는 경우 NR_VMSCAN_IMMEDIATE stat을 증가시키고 페이지를 reclaim 비트 플래그를 설정한 후 다음에 처리하도록 page_list에 다시 추가한다.
  • if (references == PAGEREF_RECLAIM_CLEAN) goto keep_locked;
    • 다음에 처리하도록 page_list에 다시 추가한다.
  • if (!may_enter_fs) goto keep_locked;
    • 파일 시스템을 이용할 수 없으면 다음에 처리하도록 page_list에 다시 추가한다.
  • if (!sc->may_writepage) goto keep_locked;
    • 파일에 기록을 할 수 없으면 다음에 처리하도록 page_list에 다시 추가한다.
  • switch (pageout(page, mapping, sc)) {
    • 페이지를 파일에 기록한다.
  • case PAGE_KEEP: goto keep_locked;
    • 다음에 처리하도록 page_list에 다시 추가한다.
  • case PAGE_ACTIVATE: goto activate_locked;
    • page의 Active 플래그 비트를 설정하고 다음에 처리하도록 page_list에 다시 추가한다.
  • case PAGE_SUCCESS: if (PageWriteback(page)) goto keep;
    • 페이지를 파일에 기록을 성공적으로 완료시켰고 여전히 writeback 플래그 상태인 경우 다음에 처리하도록 page_list에 다시 추가한다.
  • if (PageDirty(page)) goto keep;
    • 여전히 dirty 플래그 상태인 경우 다음에 처리하도록 page_list에 다시 추가한다.
  • if (!trylock_page(page)) goto keep;
    • 페이지의 락 획득을 시도하여 실패한 경우 다음에 처리하도록 page_list에 다시 추가한다.
  • if (PageDirty(page) || PageWriteback(page)) goto keep_locked;
    • 다시 한 번 확인차 페이지가 dirty 상태이거나 writeback 상태이면 다음에 처리하도록 page_list에 다시 추가한다.
  • mapping = page_mapping(page);
    • 매핑을 얻어온다.
  • case PAGE_CLEAN: }
    • 페이지를 free하기 위해 아래 루틴을 계속한다.

 

                /*
                 * 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 (!mapping || !__remove_mapping(mapping, page, true))
                        goto keep_locked;

                /*
                 * At this point, we have no other references and there is
                 * no way to pick any more up (removed from LRU, removed
                 * from pagecache). Can use non-atomic bitops now (and
                 * we obviously don't have to worry about waking up a process
                 * waiting on the page lock, because there are no references.
                 */
                __clear_page_locked(page);
  • if (page_has_private(page)) { if (!try_to_release_page(page, sc->gfp_mask)) goto activate_locked;
    • private 페이지인 경우 페이지를 release 시도하고 실패한 경우 page의 Active 플래그 비트를 설정하고 다음에 처리하도록 page_list에 다시 추가한다.
  • if (!mapping && page_count(page) == 1) { unlock_page(page); if (put_page_testzero(page)) goto free_it; else { nr_reclaimed++; continue; } }
    • 매핑이 없으면서 페이지 참조 횟수가 1인 경우 0으로 변경하여 사용자가 없는 경우 free_it 레이블로 이동하여 페이지를 free 한다. 만일 아직 사용자가 있는 경우 nr_reclaimed 카운트를 증가하고 루프를 계속 수행한다.
  •  if (!mapping || !__remove_mapping(mapping, page, true)) goto keep_locked;
    • 매핑이 없거나 매핑을 제거하다 실패한 경우 다음에 처리하도록 page_list에 다시 추가한다.
  • __clear_page_locked(page);
    • 더 이상 사용자가 없는 페이지에 대해 lock 비트 플래그를 클리어한다.

 

free_it:
                nr_reclaimed++;

                /*
                 * Is there need to periodically free_page_list? It would
                 * appear not as the counts should be low
                 */
                list_add(&page->lru, &free_pages);
                continue;

cull_mlocked:
                if (PageSwapCache(page))
                        try_to_free_swap(page);
                unlock_page(page);
                putback_lru_page(page);
                continue;

activate_locked:
                /* Not a candidate for swapping, so reclaim swap space. */
                if (PageSwapCache(page) && vm_swap_full())
                        try_to_free_swap(page);
                VM_BUG_ON_PAGE(PageActive(page), page);
                SetPageActive(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);
        free_hot_cold_page_list(&free_pages, true);

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

        *ret_nr_dirty += nr_dirty;
        *ret_nr_congested += nr_congested;
        *ret_nr_unqueued_dirty += nr_unqueued_dirty;
        *ret_nr_writeback += nr_writeback;
        *ret_nr_immediate += nr_immediate;
        return nr_reclaimed;
}
  • free_it: nr_reclaimed++; list_add(&page->lru, &free_pages); continue;
    • nr_reclaimed 카운터를 증가시키고 페이지를 free_pages 리스트에 추가하고 루프를 계속 수행한다.
      • 루프 완료 후 free_pages에 있는 페이지 항목들은 모두 버디 시스템으로 free될 예정이다.
  • cull_mlocked: if (PageSwapCache(page)) try_to_free_swap(page); unlock_page(page); putback_lru_page(page); continue;
    • 회수될 페이지가 아니라 판단되어 lruvec로 다시 되돌리고 루프를 계속 수행한다. 단 swapcache 페이지인 경우 swap 공간을 free하기 위해 시도한다.
  • activate_locked: if (PageSwapCache(page) && vm_swap_full()) try_to_free_swap(page); SetPageActive(page); pgactivate++;
    • swapcache 페이지이면서 swap 공간이 가득찬 경우 swap 공간을 free하기 위해 시도한다. 그런 후 페이지의 active 플래그 비트를 설정하고 pgactivate 카운터를 증가한다.
  • keep_locked: unlock_page(page);
    • 페이지 락을 클리어한다.
  • keep:  list_add(&page->lru, &ret_pages);
    • 페이지를 ret_pages에 추가한다.
  • mem_cgroup_uncharge_list(&free_pages);
    • memcg에 free_pages에 대한 uncharge 정보를 전달한다.
  • free_hot_cold_page_list(&free_pages, true);
    • free_pages 들을 버디 시스템에 cold 방향으로 free 시킨다.
  •  list_splice(&ret_pages, page_list);
    • ret_pages에 남은 페이지들을 page_list에 포함시킨다.

 

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

shrink_page_list-1

 

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)) {
                *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 여부를 알아온다.

  • if (!page_is_file_cache(page)) { *dirty = false; *writeback = false; return; }
    • file 캐시 페이지가 아닌 경우 출력 인수 dirty와 writeback에 false를 담고 함수를 종료한다.
  • *dirty = PageDirty(page);
    • dirty 페이지인 경우 출력 인수 dirty에 true를 대입한다.
  • *writeback = PageWriteback(page);
    • writeback 페이지인 경우 출력 인수 writeback에 true를 대입한다.
  • if (!page_has_private(page)) return;
    • private page가 아닌 경우 함수를 종료한다.
  • mapping = page_mapping(page); if (mapping && mapping->a_ops->is_dirty_writeback) mapping->a_ops->is_dirty_writeback(page, dirty, writeback);
    • 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
    • 페이지 회수를 진행
  • PAGEREF_KEEP
    • 다음에 처리하게 유보
  • PAGEREF_ACTIVATE
    • 페이지가 active 중이므로 다음에 처리하게 유보

 

  • referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup, &vm_flags);
    • 페이지 참조 횟수를 알아온다.
  • referenced_page = TestClearPageReferenced(page);
    • 참조 비트를 알아오고 페이지의 참조 비트는 클리어한다.
  • if (vm_flags & VM_LOCKED) return PAGEREF_RECLAIM;
    • VM_LOCKED가 설정된 경우 페이지 회수 중 상태로 반환한다.
  • if (referenced_ptes) { if (PageSwapBacked(page)) return PAGEREF_ACTIVATE;
    • 페이지 참조 중이면서 swapbacked 플래그 상태이면 활동중 상태로 반환한다.
  • SetPageReferenced(page);
    • 페이지의 참조 플래그를 설정한다.
  • if (referenced_page || referenced_ptes > 1) return PAGEREF_ACTIVATE;
    • 기존에 참조 비트가 설정된 상태였었거나 참조 횟수가 1보다 큰 경우 활동중 상태로 반환한다.
  • if (vm_flags & VM_EXEC)  return PAGEREF_ACTIVATE;
    • VM_EXEC 플래그가 설정된 경우 활동 중 상태로 반환한다.
  • return PAGEREF_KEEP; }
  • if (referenced_page && !PageSwapBacked(page)) return PAGEREF_RECLAIM_CLEAN;
    • 기존에 참조 비트가 설정되었으면서 swapbacked가 아닌 경우 RECLAIM_CLEAN 상태로 반환한다.
  • return PAGEREF_RECLAIM;
    • 회수 상태로 반환한다.

 

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
 * @nr_scanned: pressure numerator
 * @nr_eligible: pressure denominator
 *
 * 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. If it is not NULL,
 * only shrinkers with SHRINKER_MEMCG_AWARE set will be called to scan
 * objects from the memory cgroup specified. Otherwise all shrinkers
 * are called, and memcg aware shrinkers are supposed to scan the
 * global list then.
 *
 * @nr_scanned and @nr_eligible form a ratio that indicate how much of
 * the available objects should be scanned.  Page reclaim for example
 * passes the number of pages scanned and the number of pages on the
 * LRU lists that it considered on @nid, plus a bias in @nr_scanned
 * when it encountered mapped pages.  The ratio is further biased by
 * the ->seeks setting of the shrink function, which indicates the
 * cost to recreate an object relative to that of an LRU page.
 *
 * Returns the number of reclaimed slab objects.
 */
static unsigned long shrink_slab(gfp_t gfp_mask, int nid,
                                 struct mem_cgroup *memcg,
                                 unsigned long nr_scanned,
                                 unsigned long nr_eligible)
{
        struct shrinker *shrinker;
        unsigned long freed = 0;

        if (memcg && !memcg_kmem_is_active(memcg))
                return 0;

        if (nr_scanned == 0)
                nr_scanned = SWAP_CLUSTER_MAX;

        if (!down_read_trylock(&shrinker_rwsem)) {
                /*
                 * If we would return 0, our callers would understand that we
                 * have nothing else to shrink and give up trying. By returning
                 * 1 we keep it going and assume we'll be able to shrink next
                 * time.
                 */
                freed = 1;
                goto out;
        }

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

                if (memcg && !(shrinker->flags & SHRINKER_MEMCG_AWARE))
                        continue;

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

                freed += do_shrink_slab(&sc, shrinker, nr_scanned, nr_eligible);
        }

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

slub 캐시를 shrink한다.

  • if (memcg && !memcg_kmem_is_active(memcg)) return 0;
    • memcg->kmem_acct_active가 0인 경우 처리를 중단한다.
  • if (nr_scanned == 0) nr_scanned = SWAP_CLUSTER_MAX;
    • nr_scanned가 0인 경우 SWAP_CLUSTER_MAX(128)로 대입한다.
  • if (!down_read_trylock(&shrinker_rwsem)) { freed = 1; goto out; }
    • read 세마포어락 획득을 시도하여 실패한 경우 1을 리턴한다.
  • list_for_each_entry(shrinker, &shrinker_list, list) {
    • 등록된 모든 shrink들 만큼 루프를 돈다.
  • if (memcg && !(shrinker->flags & SHRINKER_MEMCG_AWARE)) continue;
    • flag가 SHRINKER_MEMCG_AWARE가 없는 경우 skip
  • if (!(shrinker->flags & SHRINKER_NUMA_AWARE)) sc.nid = 0;
    • flag에 SHRINKER_NUMA_AWARE가 없는 경우 노드 id에 0을 대입한다.
  • freed += do_shrink_slab(&sc, shrinker, nr_scanned, nr_eligible);
    • shrink의 slub 캐시를 shrink하고 free된 object 수를 알아와서 freed에 더한다.
  • up_read(&shrinker_rwsem);
    • read 세마포어락을 release한다.

 

do_shrink_slab()

mm/vmscan.c

static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
                                    struct shrinker *shrinker,
                                    unsigned long nr_scanned,
                                    unsigned long nr_eligible)
{
        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;

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

        /*
         * 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;
        delta = (4 * nr_scanned) / shrinker->seeks;
        delta *= freeable;
        do_div(delta, nr_eligible + 1);
        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;
        }

        /*
         * 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,
                                   nr_scanned, nr_eligible,
                                   freeable, delta, total_scan);

slub 캐시를 shrink한다.

 

  • long batch_size = shrinker->batch ? shrinker->batch : SHRINK_BATCH;
    • 한 번에 처리할 slub object 갯수로 지정되지 않는 경우 SHRINK_BATCH(128) 개를 대입한다.
  • freeable = shrinker->count_objects(shrinker, shrinkctl); if (freeable == 0) return 0;
    • 지정된 shrinker의 slub object의 수를 알아오고 처리할 object가 없으면 처리를 중단한다.
  • nr = atomic_long_xchg(&shrinker->nr_deferred[nid], 0);
    • 현재 shrinker의 스캔 카운터를 0으로 설정하여 다른 곳과 동시에 호출되는 것을 막는다.
  • total_scan = nr; delta = (4 * nr_scanned) / shrinker->seeks; delta *= freeable; do_div(delta, nr_eligible + 1); total_scan += delta;
    • total_scan에 delta를 더해 교정한다.
  • if (delta < freeable / 4) total_scan = min(total_scan, freeable / 2);
    • delta가 freeable의 25%보다 작은 경우 total_scan 수가 freeable의 절반을 초과하지 않도록 제한한다.
  • if (total_scan > freeable * 2) total_scan = freeable * 2;
    • total_scan이 freeable의 두 배를 초과하지 않도록 한다.

 

        /*
         * 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;
                ret = shrinker->scan_objects(shrinker, shrinkctl);
                if (ret == SHRINK_STOP)
                        break;
                freed += ret;

                count_vm_events(SLABS_SCANNED, nr_to_scan);
                total_scan -= nr_to_scan;

                cond_resched();
        }

        /*
         * 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 (total_scan > 0)
                new_nr = atomic_long_add_return(total_scan,
                                                &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;
}
  • while (total_scan >= batch_size || total_scan >= freeable) {
    • total_scan이 batch_size 또는 freeable 보다 큰 경우
  • unsigned long nr_to_scan = min(batch_size, total_scan);
    • 스캔할 건수로 batch_size와 total_scan보다 작은 수로 한다.
  • ret = shrinker->scan_objects(shrinker, shrinkctl); if (ret == SHRINK_STOP) break;
    • shrinker에 등록한 scan_objects() 핸들러 함수를 호출하여 object를 scan하고 free된 object 수를 알아온다. 만일 결과가 SHRINK_STOP인 경우 루프를 벗어난다.
  • freed += ret; total_scan -= nr_to_scan;
    • freed에 free된 object 수를 더하고, total_scan에서는 scan한 수만큼 감소시키고 루프를 계속 수행한다.
  • if (total_scan > 0) new_nr = atomic_long_add_return(total_scan, &shrinker->nr_deferred[nid]);
    • 남은 total_scan은 nr_deferred[]에 대입한다.

 

참고