Zonned Allocator -1- (페이지 할당-Fastpath)

 

zone-1a

 

관련 커널 옵션

FAIL_PAGE_ALLOC

alloc_pages()에 대한 디버그 기능으로 fault-injection 처리를 위한 코드가 추가된다.

  • “fail_page_alloc=<interval>,<probability>,<space>,<times>” 커널 옵션을 사용하여 FAULT-INJECTION에 대한 4가지 속성을 변경할 수 있다.

 

페이지 할당(alloc)

 

다음 그림은 페이지 할당을 하는 흐름을 보여준다.

alloc_pages-1

 

alloc_pages()

include/linux/gfp.h

#ifdef CONFIG_NUMA
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
{
        return alloc_pages_current(gfp_mask, order);
}
#else
#define alloc_pages(gfp_mask, order) \
                alloc_pages_node(numa_node_id(), gfp_mask, order)
#endif

버디 시스템을 통하여 order 페이지만큼 할당 받는다.

  • NUMA 시스템인 경우 메모리 정책에 따라 노드를 선택하고 버디 시스템을 통하여 order 페이지만큼 할당 받는다.
  • NUMA 시스템이 아닌 경우 현재 노드에서 버디 시스템을 통하여 order 페이지만큼 할당 받는다.

 

alloc_pages_current()

mm/mempolicy.c

/**
 *      alloc_pages_current - Allocate pages.
 *
 *      @gfp:
 *              %GFP_USER   user allocation,
 *              %GFP_KERNEL kernel allocation,
 *              %GFP_HIGHMEM highmem allocation,
 *              %GFP_FS     don't call back into a file system.
 *              %GFP_ATOMIC don't sleep.        
 *      @order: Power of two of allocation size in pages. 0 is a single page.
 *
 *      Allocate a page from the kernel page pool.  When not in
 *      interrupt context and apply the current process NUMA policy.
 *      Returns NULL when no page can be allocated.
 *
 *      Don't call cpuset_update_task_memory_state() unless
 *      1) it's ok to take cpuset_sem (can WAIT), and
 *      2) allocating for current task (not interrupt).
 */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
        struct mempolicy *pol = &default_policy;
        struct page *page;
        unsigned int cpuset_mems_cookie;
                        
        if (!in_interrupt() && !(gfp & __GFP_THISNODE))
                pol = get_task_policy(current);

retry_cpuset:
        cpuset_mems_cookie = read_mems_allowed_begin();

        /*
         * No reference counting needed for current->mempolicy
         * nor system default_policy
         */
        if (pol->mode == MPOL_INTERLEAVE)
                page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
        else
                page = __alloc_pages_nodemask(gfp, order,
                                policy_zonelist(gfp, pol, numa_node_id()),
                                policy_nodemask(gfp, pol));

        if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
                goto retry_cpuset;
                                          
        return page;
}
EXPORT_SYMBOL(alloc_pages_current);

NUMA 시스템에서 메모리 정책에 따라 노드를 선택하고 버디 시스템을 통하여 order 페이지만큼 할당 받는다.

  • struct mempolicy *pol = &default_policy;
  • if (!in_interrupt() && !(gfp & __GFP_THISNODE)) pol = get_task_policy(current);
    • 인터럽트 핸들러에서 호출되어 수행 중이거나 현재 노드에서만 할당하라는 플래그 요청이 있는 경우 디폴트 메모리 정책을 사용하여 로컬 노드에서 할당하게 한다.
    • 그렇지 않은 경우 현재 태스크의 메모리 정책을 알아온다.
  • retry_cpuset: cpuset_mems_cookie = read_mems_allowed_begin();
    • 시퀀스 락에 의해 재시도 되는 경우 재 호출될 레이블이다.
  • if (pol->mode == MPOL_INTERLEAVE) page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
    • MPOL_INTERLEAVE 메모리 정책 을 사용하는 경우 페이지를 노드별로 돌아가며 할당하게 한다.
  • else page = __alloc_pages_nodemask(gfp, order, policy_zonelist(gfp, pol, numa_node_id()), policy_nodemask(gfp, pol));
    • 그 외 다른 메모리 정책을 사용하는 경우 현재노드의 정책에 따른 zonelist에서 order 페이지를 할당 받는다.
  • if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) goto retry_cpuset;
    • 작은 확률로 페이지 할당이 실패하고 사용한 시퀀스 락이 실패하는 경우 할당을 재시도한다.

 

alloc_pages_node()

include/linux/gfp.h

static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
                                                unsigned int order)
{
        /* Unknown node is current node */
        if (nid < 0)
                nid = numa_node_id();

        return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}

지정된 노드의 zonelist에서 order 페이지를 할당 받는다. 만일 알 수 없는 노드가 지정된 경우 현재 노드의 zonelist에서 할당 받는다.

 

__alloc_pages()

include/linux/gfp.h

static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order,
                struct zonelist *zonelist)
{
        return __alloc_pages_nodemask(gfp_mask, order, zonelist, NULL);
}

지정된 zonelist에서 order 페이지를 할당 받는다.

 

메모리 정책(Policy) 관련

default_policy 전역 객체

mm/mempolicy.c

/*
 * run-time system-wide default policy => local allocation
 */
static struct mempolicy default_policy = {
        .refcnt = ATOMIC_INIT(1), /* never free it */
        .mode = MPOL_PREFERRED,
        .flags = MPOL_F_LOCAL,
};
  • 사용 메모리 정책은 MPOL_PREFERED로 하고 로컬 노드에서 할당을 하는 것을 권장한다.
    • MPOL_F_LOCAL
      • 로컬 노드 할당

 

get_task_policy()

mm/mempolicy.c

struct mempolicy *get_task_policy(struct task_struct *p)
{
        struct mempolicy *pol = p->mempolicy;
        int node;

        if (pol)
                return pol;

        node = numa_node_id();
        if (node != NUMA_NO_NODE) {
                pol = &preferred_node_policy[node];
                /* preferred_node_policy is not initialised early in boot */
                if (pol->mode)
                        return pol;
        }

        return &default_policy;
}

현재 태스크의 메모리 정책을 아래의 케이스에 따라 반환한다.

  • 현재 태스크의 메모리 정책이 지정되어 있는 경우 그 값을 반환한다.
  • 만일 노드가 지정되고 현재 태스크의 메모리 정책이 지정되지 않은 경우 노드별 권장 메모리 정책이 반환된다.
  • 그 외의 케이스에서는 디폴트 메모리 정책을 반환한다.
    • 로컬 노드에서 할당

 

interleave_nodes()

mm/mempolicy.c

/* Do dynamic interleaving for a process */
static unsigned interleave_nodes(struct mempolicy *policy)
{
        unsigned nid, next;
        struct task_struct *me = current;

        nid = me->il_next;
        next = next_node(nid, policy->v.nodes);
        if (next >= MAX_NUMNODES) 
                next = first_node(policy->v.nodes);
        if (next < MAX_NUMNODES)
                me->il_next = next;
        return nid;
}

currnet->il_next에 기억된 노드를 반환한다. 그 동안 다음에 배정할 노드를 current->il_next에 기억해놓는다.

  • il_next에는 메모리 정책이 MPOL_INTERLEAVE로 설정된 경우 사용할 노드를 interleave(round robin) 방식에 의해 배정한다.
    • next_node()
      • 노드 비트맵에 대하여 지정된 노드의 다음노드를 알아온다. 못 찾은 경우 MAX_NUMNODES를 반환한다.
    • first_node()
      • 노드 비트맵의 처음에 위치한 노드를 알아온다.

 

alloc_page_interleave()

mm/mempolicy.c

/* Allocate a page in interleaved policy.
   Own path because it needs to do special accounting. */
static struct page *alloc_page_interleave(gfp_t gfp, unsigned order,
                                        unsigned nid)
{
        struct zonelist *zl;
        struct page *page;

        zl = node_zonelist(nid, gfp);
        page = __alloc_pages(gfp, order, zl);
        if (page && page_zone(page) == zonelist_zone(&zl->_zonerefs[0]))
                inc_zone_page_state(page, NUMA_INTERLEAVE_HIT);
        return page;
}

지정된 노드의 zonelist를 알아와서 order 페이지를 할당 받는다.

  • zl = node_zonelist(nid, gfp);
    • 지정된 노드와 플래그를 통해 zonelist를 가져온다.
      • gfp 플래그에 따라  전체 노드 zonelist인 node_zonelist[0]을 가져오거나 지정된 노드 zone만을 담은 node_zonelist[1]을 가져온다.
  • page = __alloc_pages(gfp, order, zl);
    • zonelist에서 order 페이지를 할당 받는다.
  • if (page && page_zone(page) == zonelist_zone(&zl->_zonerefs[0])) inc_zone_page_state(page, NUMA_INTERLEAVE_HIT);
    • 만일 페이지 할당이 성공하고 할당 받은 page의 zone이 zonelist의 첫 zone인 경우 NUMA_INTERLEAVE_HIT stat을 증가시킨다.

 

__alloc_pages_nodemask()

mm/page_alloc.c

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
                        struct zonelist *zonelist, nodemask_t *nodemask)
{
        struct zoneref *preferred_zoneref;
        struct page *page = NULL;
        unsigned int cpuset_mems_cookie;
        int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
        gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
        struct alloc_context ac = {
                .high_zoneidx = gfp_zone(gfp_mask),
                .nodemask = nodemask,
                .migratetype = gfpflags_to_migratetype(gfp_mask),
        };

        gfp_mask &= gfp_allowed_mask;

        lockdep_trace_alloc(gfp_mask);

        might_sleep_if(gfp_mask & __GFP_WAIT);

        if (should_fail_alloc_page(gfp_mask, order))
                return NULL;

        /*
         * Check the zones suitable for the gfp_mask contain at least one
         * valid zone. It's possible to have an empty zonelist as a result
         * of GFP_THISNODE and a memoryless node
         */
        if (unlikely(!zonelist->_zonerefs->zone))
                return NULL;

        if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE)
                alloc_flags |= ALLOC_CMA;

지정된 노드마스크, zonelist 및 flags 설정을 참고하여 노드와 zone을 선택한 후 order 페이지 할당을 한다.

 

  • int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
    • 할당 플래그에 워터마크 하한을 설정하고 cpuset이 허용하는 노드 및 공정한 할당을 요청하는 플래그를 설정한다.
      • ALLOC_WMARK_LOW
        • 워터마크 하한까지 페이지 할당 제한
      • ALLOC_CPUSET
        • 현재 태스크에서 cgroup의 cpuset 서브 시스템에서 설정한 정책 사용
      • ALLOC_FAIR
        • NUMA 시스템에서 공정한 노드별 할당을 사용
  • .high_zoneidx = gfp_zone(gfp_mask),
    • ZONE 비트 정보가 포함된 flags를 사용하여 zone_type (based 0)을 리턴한다.
  • .migratetype = gfpflags_to_migratetype(gfp_mask),
    • gfp_flags에서 migrate type을 알아내어 반환한다.
      • gfp 플래그에 __GFP_MOVABLE 사용 시 MIGRATE_MOVABLE
      • gfp 플래그에 __GFP_RECLAIMABLE을 사용하는 경우 MIGRATE_RECLAIMABLE
      • 예외적으로 메모리가 적은 시스템의 경우에는 전역 page_group_by_mobility_disabled  변수가 설정되어 __GFP_UNMOVABLE만 사용된다.
      • 그 외의 migrate 타입은 gfp 플래그에 사용되지 않는다
  • gfp_mask &= gfp_allowed_mask;
    • 커널 부트업 프로세스가 처리되는 동안은 페이지 할당을 위해 io 처리를 위한 드라이버나 파일 시스템이 준비되지 않으며 따라서 메모리 회수 시스템도 구동되지 않고 있는 상태이다. 따라서 이러한 요청들이 발생할 때 이의 기능을 사용하지 못하게 막기 위해  gfp 플래그에서 __GFP_RECLAIM, __GFP_IO 및 |__GFP_FS 비트를 제거한다.
  • might_sleep_if(gfp_mask & __GFP_WAIT);
    • __GFP_WAIT 설정이 있는 경우 preemption point를 실행한다.
    • preemption point
      • 주로 데스크 탑 용도로 사용되는 리눅스 커널의 경우 CONFIG_PREEMPT_VOLUNTARY 커널 옵션을 설정하여 사용하는데 현재 태스크보다 급하게 리스케쥴링 요청하는 태스크가 있는 경우 이를 지원하여 리스케쥴링되고 현재 프로세스는 sleep 상태가 된다.
  • if (should_fail_alloc_page(gfp_mask, order)) return NULL;
    • page alloc에 대한 FAULT-INJECTION이 동작하여 fault 조건에서 동작하지 않도록 루틴을 빠져나간다.
  • if (unlikely(!zonelist->_zonerefs->zone)) return NULL;
    • zonelist에 zone이 없는 경우 루틴을 빠져나간다.
  •  if (IS_ENABLED(CONFIG_CMA) && ac.migratetype == MIGRATE_MOVABLE) alloc_flags |= ALLOC_CMA;
    • CONFIG_CMA 커널 옵션이 사용되면서 MIGRATE_MOVABLE 타입 요청인 alloc_flags에 ALLOC_CMA를 추가하여 CMA 영역도 할당 영역에 포함시킨다.
    • CMA 영역에 movable 타입 페이지의 할당을 허락하는 이유
      • CMA 타입으로 free 페이지 요청 시 버디 시스템에 등록된 CMA 페이지가 부족한 경우 해당 CMA 영역에서 이미 사용중인 movable 페이지들을 다른 영역으로 강제로 이주시켜서라도 free 페이지를 확보하여 할당할 수 있다.
      • 또한 리눅스 시스템에서 CMA 메모리 요청은 빈번하지 않으므로 CMA 영역내의 메모리를 놀리지 않고 효율적으로 사용할 수 있게 하기 위해 이러한 방법으로 운용한다.

 

retry_cpuset:
        cpuset_mems_cookie = read_mems_allowed_begin();

        /* We set it here, as __alloc_pages_slowpath might have changed it */
        ac.zonelist = zonelist;
        /* The preferred zone is used for statistics later */
        preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx,
                                ac.nodemask ? : &cpuset_current_mems_allowed,
                                &ac.preferred_zone);
        if (!ac.preferred_zone)
                goto out;
        ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);

        /* First allocation attempt */
        alloc_mask = gfp_mask|__GFP_HARDWALL;
        page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
        if (unlikely(!page)) {
                /*
                 * Runtime PM, block IO and its error handling path
                 * can deadlock because I/O on the device might not
                 * complete.
                 */
                alloc_mask = memalloc_noio_flags(gfp_mask);

                page = __alloc_pages_slowpath(alloc_mask, order, &ac);
        }

        if (kmemcheck_enabled && page)
                kmemcheck_pagealloc_alloc(page, order, gfp_mask);

        trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);

out:
        /*
         * When updating a task's mems_allowed, it is possible to race with
         * parallel threads in such a way that an allocation can fail while
         * the mask is being updated. If a page allocation is about to fail,
         * check if the cpuset changed during allocation and if so, retry.
         */
        if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie)))
                goto retry_cpuset;

        return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);
  • retry_cpuset: cpuset_mems_cookie = read_mems_allowed_begin();
    • cpuset 관련한 리드 시퀀스 값을 알아온다. 페이지 할당을 실패하는 동안 cpuset에 변경이 일어나는 경우에 한하여 이 레이블 위치에서 재시도한다.
  • ac.zonelist = zonelist;
    • alloc context에 zonlist 대입
    • slowpath 할당에서 ac.zonelist가 변동이 일어날 수 있어서 다시 인수로 요청했었던 오리지널 zonelist를 대입한다.
  • preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx, ac.nodemask ? : &cpuset_current_mems_allowed, &ac.preferred_zone);
    • 현재 zonelist의 사용할 수 있는 가장 첫 zone을 preferred_zoneref에 저장하고 이 값은 나중에 통계에서 사용되어질 예정이다
      • ac.nodemask가 지정되지 않아 null인 경우 현재 태스크에 cpuset으로 지정된 노드마스크를 사용한다
  • if (!ac.preferred_zone) goto out;
    • 첫 zone이 없는 경우는 페이지 할당을 할 수 없으므로 out 레이블로 이동하여 함수를 빠져나간다
  • ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);
    • preferred zone의 타입 인덱스(0~3)

fastpath

  • alloc_mask = gfp_mask|__GFP_HARDWALL; page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
    • 이제 첫 할당 성공을 위해 시도하는 fastpath이다. 이 때 gfp 마스크에 __GFP_HARDWALL을 사용하여 현재 태스크에서 페이지 할당을 할 때 상위 cpuset을 찾지못하게 막고 현재 cpuset에 지정된 설정만을 사용하게 하여 페이지 할당을 시도한다
    • __GFP_HARDWALL은 fastpath 페이지 할당에서도 사용되고, 사용자 태스크에서 페이지 할당 요청 및 slab 페이지 할당 등에서 사용된다.
      • #define GFP_USER   (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
    • 메모리 부족으로 인해 slowpath 할당을 시도할 때 사용자 태스크에서 페이지 할당을 요청한 경우가 아니면 커널은 slab 할당 등의 특수한 경우를 제외하고 일반적인 경우 __GFP_HARDWALL 플래그를  사용하지 않으며 이 경우에는 hardwall 또는 exclusive 설정이 된 현재 cpuset을 포함한 가장 가까운 상위 cpuset을 찾아 설정을 사용할 수 있다.

slowpath

  • if (unlikely(!page)) {
    • 만일 낮은 확률로 fastpath 페이지 할당이 실패한 경우
  • alloc_mask = memalloc_noio_flags(gfp_mask);
    • slowpath용 alloc_mask에 낮은 확률로 현재 태스크의 플래그에 PF_MEMALLOC_NOIO 요청이 있는 경우에만 gfp 플래그에서 __GFP_IO 및 __GFP_FS를 제거한 값을 설정하여 페이지 할당 시 디스크 캐시의 사용이나 페이지의 회수 등을 이유로 io 및 파일 시스템의 이용을 금지한다.
  •  page = __alloc_pages_slowpath(alloc_mask, order, &ac);
    • slowpath 페이지 할당을 시도한다.
    • slowpath에서는 fastpath가 사용한 __GFP_HARDWALL을 추가하지 않으므로 처음 부터 gfp 플래그에 __GFP_HARDWALL 요청을 하지 않은 경우이므로 현재 태스크가 현재 cpuset을 포함하여 가장 가까운 hardwall 이나 exclusive 부모 cpuset 을 선택하여 그 설정을 사용하여 할당을 시도한다.
  • if (unlikely(!page && read_mems_allowed_retry(cpuset_mems_cookie))) goto retry_cpuset;
    • 페이지 할당이 실패하였으면서 현재 태스크에 대한 cpuset 설정이 바뀐 경우(/sys/fs/cpuset 디렉토리에 있는 설정) 페이지 할당을 다시 시도하면 성공할 확률이 있으므로 재시도하게 한다

 

should_fail_alloc_page()

mm/page_alloc.c

static bool should_fail_alloc_page(gfp_t gfp_mask, unsigned int order)
{
        if (order < fail_page_alloc.min_order)
                return false;
        if (gfp_mask & __GFP_NOFAIL)
                return false;
        if (fail_page_alloc.ignore_gfp_highmem && (gfp_mask & __GFP_HIGHMEM))
                return false;
        if (fail_page_alloc.ignore_gfp_wait && (gfp_mask & __GFP_WAIT))
                return false;

        return should_fail(&fail_page_alloc.attr, 1 << order);
}

CONFIG_FAIL_PAGE_ALLOC 커널 옵션을 사용하는 경우 디버깅 목적으로 사용되며 fail_page_alloc 전역 객체의 조건을 벗어나는 gfp_mask를 가진 경우 false를 반환한다. 또한 __GFP_NOFAIL 옵션을 사용하는 경우에도 false를 반환한다.

 

read_mems_allowed_begin()

include/linux/cpuset.h

/*
 * read_mems_allowed_begin is required when making decisions involving
 * mems_allowed such as during page allocation. mems_allowed can be updated in
 * parallel and depending on the new value an operation can fail potentially
 * causing process failure. A retry loop with read_mems_allowed_begin and
 * read_mems_allowed_retry prevents these artificial failures.
 */
static inline unsigned int read_mems_allowed_begin(void)
{
        return read_seqcount_begin(&current->mems_allowed_seq);
}

현재 태스크의 mems_allowed_seq 시퀀스 락 값을 알아온다.

  • 현재 태스크에 대한 cpuset 설정이 바뀐 경우(/sys/fs/cpuset 디렉토리에 있는 설정) current->mems_allowed_seq 값이 변경된다.

 

Fastpath 페이지 할당

아래의 함수는 페이지 할당 시 Fastpath와 Slowpath 두 곳에서 호출되어 사용되는데 Fastpath에서 호출될 때에는 인수 gfp_mask에 __GFP_HARDWALL을 추가하여 호출한다.

  • GFP_USER
    • (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
  • GFP_KERNEL
    • (__GFP_WAIT | __GFP_IO | __GFP_FS)
  • user space에서 페이지를 할당 시 __GFP_HARDWALL을 설정하여 현재 태스크의 cpuset이 허락하는 메모리 노드가 아닌 곳에서 할당되는 것을 허용하지 않게 한다.

 

다음 그림은 페이지 할당 시 사용되는 Fastpath 루틴과 Slowpath 루틴에서 사용되는 함수를 구분하여 보여준다.

  • 단 get_page_from_freelist() 함수가 __alloc_pages_slowpath() 함수에서 호출되는 경우에는 Slowpath의 일부분이다.

alloc_pages-2a

 

get_page_from_freelist()

mm/page_alloc.c

/*
 * get_page_from_freelist goes through the zonelist trying to allocate
 * a page.
 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                                                const struct alloc_context *ac)
{
        struct zonelist *zonelist = ac->zonelist;
        struct zoneref *z;
        struct page *page = NULL;
        struct zone *zone;
        nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
        int zlc_active = 0;             /* set if using zonelist_cache */
        int did_zlc_setup = 0;          /* just call zlc_setup() one time */
        bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
                                (gfp_mask & __GFP_WRITE);
        int nr_fair_skipped = 0;
        bool zonelist_rescan;

zonelist_scan:
        zonelist_rescan = false;

        /*
         * Scan zonelist, looking for a zone with enough free.
         * See also __cpuset_node_allowed() comment in kernel/cpuset.c.
         */
        for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,
                                                                ac->nodemask) {
                unsigned long mark;

                if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                        !zlc_zone_worth_trying(zonelist, z, allowednodes))
                                continue;
                if (cpusets_enabled() &&
                        (alloc_flags & ALLOC_CPUSET) &&
                        !cpuset_zone_allowed(zone, gfp_mask))
                                continue;
                /*
                 * Distribute pages in proportion to the individual
                 * zone size to ensure fair page aging.  The zone a
                 * page was allocated in should have no effect on the
                 * time the page has in memory before being reclaimed.
                 */
                if (alloc_flags & ALLOC_FAIR) {
                        if (!zone_local(ac->preferred_zone, zone))
                                break;
                        if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
                                nr_fair_skipped++;
                                continue;
                        }
                }

zonelist에서 high_zoneidx 이하의 zone과 지정된 노드 마스크에 대해서 순서대로 zone을 조사하여 zlc 및 워터마크 관련 조건들을 만족하는 경우 그 zone에서 페이지 할당을 한다. 요청 zone에서 할당이 실패된 경우 조건에 따라 zonelist를 다시 구성하여 재시도를 한다.

  • bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) && (gfp_mask & __GFP_WRITE);
    • alloc_flags에 ALLOC_WMARK_LOW가 있고 gfp_mask에 __GFP_WRITE가 있는 경우 true
  • zonelist_scan: zonelist_rescan = false;
    • zonelist_rescan이 설정되는 경우 다시 이 레이블에서 시작한다.
  • for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx, ac->nodemask) {
    • zonelist에서 highest_zoneidx 이하의 zone이면서 nodemask에 설정된 노드인 zone 엔트리들에 대하여 루프를 돈다.
  • if (IS_ENABLED(CONFIG_NUMA) && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes)) continue;
    • NUMA 시스템을 지원하면서 zlc(Zone List Cache)가 활성화된 경우 zlc로 알아봐서 해당 zone이 full된 경우 skip 한다.
      • 첫 시도 시 allowednodes는 null로 모든 노드를 대상으로 한다.
  • if (cpusets_enabled() && (alloc_flags & ALLOC_CPUSET) && !cpuset_zone_allowed(zone, gfp_mask)) continue;
    • cpusets이 활성화된 경우이면서 ALLOC_CPUSET 플래그가 설정된 경우 현재 태스크에 지정된 노드에 해당 zone 으로 할당할 수 없는 경우 skip 한다.
  • if (alloc_flags & ALLOC_FAIR) {
    • ALLOC_FAIR 플래그가 설정된 경우 페이지 할당 시 preferred_zone의 노드에서 할당을 시도하는데 실패하는 경우 ALLOC_FAIR 플래그를 제거하고 다시 시도한다. 이 때에는 현재 처리하려는 zone의 노드가 권장 zone의 노드와 다르더라도 할당 시도를 하게 한다
    • fastpath 할당 과정에서 보통 권장 zone이 있는 노드에서 할당이 되곤 하므로 균등하게 할당되지만 메모리 부족으로 문제가 일어나게 되면 slowpath 루틴에서 페이지 회수 매커니즘 들이 동작하기 전에 리모트 zone에서도 할당을 시도해 본다
  • if (!zone_local(ac->preferred_zone, zone)) break;
    • 권장 zone의 노드와 현재 zone의 노드가 서로 다른 경우 더 이상 처리할 필요가 없으므로 루프를 탈출한다.
  • if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) { nr_fair_skipped++; continue; }
    • 역시 fastpath 할당 과정에서 권장 zone의 노드와 현재 처리하고자 하는 zone의 노드가 같은 경우 이면서 이 zone에 ZONE_FAIR_DEPLETED 플래그가 설정된 경우 free 페이지가 없어 페이지 할당을 할 수 없는 zone 상태를 의미하므로 nr_fair_skipped를 증가시키고 skip한다.

 

                /*
                 * When allocating a page cache page for writing, we
                 * want to get it from a zone that is within its dirty
                 * limit, such that no single zone holds more than its
                 * proportional share of globally allowed dirty pages.
                 * The dirty limits take into account the zone's
                 * lowmem reserves and high watermark so that kswapd
                 * should be able to balance it without having to
                 * write pages from its LRU list.
                 *
                 * This may look like it could increase pressure on
                 * lower zones by failing allocations in higher zones
                 * before they are full.  But the pages that do spill
                 * over are limited as the lower zones are protected
                 * by this very same mechanism.  It should not become
                 * a practical burden to them.
                 *
                 * XXX: For now, allow allocations to potentially
                 * exceed the per-zone dirty limit in the slowpath
                 * (ALLOC_WMARK_LOW unset) before going into reclaim,
                 * which is important when on a NUMA setup the allowed
                 * zones are together not big enough to reach the
                 * global limit.  The proper fix for these situations
                 * will require awareness of zones in the
                 * dirty-throttling and the flusher threads.
                 */
                if (consider_zone_dirty && !zone_dirty_ok(zone))
                        continue;

                mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
                if (!zone_watermark_ok(zone, order, mark,
                                       ac->classzone_idx, alloc_flags)) {
                        int ret;

                        /* Checked here to keep the fast path fast */
                        BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
                        if (alloc_flags & ALLOC_NO_WATERMARKS)
                                goto try_this_zone;

                        if (IS_ENABLED(CONFIG_NUMA) &&
                                        !did_zlc_setup && nr_online_nodes > 1) {
                                /*
                                 * we do zlc_setup if there are multiple nodes
                                 * and before considering the first zone allowed
                                 * by the cpuset.
                                 */
                                allowednodes = zlc_setup(zonelist, alloc_flags);
                                zlc_active = 1;
                                did_zlc_setup = 1;
                        }

                        if (zone_reclaim_mode == 0 ||
                            !zone_allows_reclaim(ac->preferred_zone, zone))
                                goto this_zone_full;
  • if (consider_zone_dirty && !zone_dirty_ok(zone)) continue;
    • 파일을 쓰기(dirty-buffered write) 목적으로 할당되는 페이지 캐시가 증가하여 현재 zone의 dirty limit 기준치를 초과하는 경우 skip 한다.
  • mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
    • zone에 설정된 3가지 high, low, min 워터마크 값 중 하나를 가져온다. 3가지 워터마크의 선택은 다음 alloc 플래그에 의해 구분된다.
      • ALLOC_WMARK_MIN, ALLOC_WMARK_LOW, ALLOC_WMARK_HIGH
  • if (!zone_watermark_ok(zone, order, mark, ac->classzone_idx, alloc_flags)) {
    • 해당 zone의 free page가 워터마크 값의 기준을 벗어난 경우
  • if (alloc_flags & ALLOC_NO_WATERMARKS) goto try_this_zone;
    • 해당 zone의 free page가 워터마크 기준을 벗어난 경우라도 alloc 플래그에서 3 가지 워터마크 기준에 대해 사용하지 않은 경우 그냥 현재 zone에서 페이지 할당을 시도하러 try_this_zone 레이블로 이동한다.
  • if (IS_ENABLED(CONFIG_NUMA) && !did_zlc_setup && nr_online_nodes > 1) { allowednodes = zlc_setup(zonelist, alloc_flags); zlc_active = 1; did_zlc_setup = 1; }
    • NUMA 시스템이고 zlc_setup() 함수를 호출하지 않았으면서 online 노드가 1개 이상인 경우 zlc_setup() 함수를 호출하여 태스크에 허락된 노드를 알아온다.
  • if (zone_reclaim_mode == 0 || !zone_allows_reclaim(ac->preferred_zone, zone)) goto this_zone_full;
    • NUMA 시스템에서 zone_reclaim_mode가 0이거나 현재 zone의 노드와 권장 zone의 노드 간 거리가 기준(30) 이상으로 회수하기에 너무 먼 경우 현재 zone을 reclaim 처리하지 않고 다음 zone으로 skip 한다.
      • zone_reclaim_mode
        • NUMA 시스템에서 “proc/sys/fs/zone_reclaim_mode” 파일 값으로 설정한다
          • 0=워터마크 기준 이하인 경우 현재 zone의 회수 처리 없이 다음 zone으로 skip
          • 1=워터마크 기준과 관련 없이 현재 zone에 대해 회수 처리한다
        • NUMA 시스템이 아닌 경우 항상 0이다.
    • NUMA 시스템에서 노드 간 거리가 30을 초과하는 경우 회수하기에 적합하지 않다고 판단한다

 

                        /*
                         * As we may have just activated ZLC, check if the first
                         * eligible zone has failed zone_reclaim recently.
                         */
                        if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
                                !zlc_zone_worth_trying(zonelist, z, allowednodes))
                                continue;

                        ret = zone_reclaim(zone, gfp_mask, order);
                        switch (ret) {
                        case ZONE_RECLAIM_NOSCAN:
                                /* did not scan */
                                continue;
                        case ZONE_RECLAIM_FULL:
                                /* scanned but unreclaimable */
                                continue;
                        default:
                                /* did we reclaim enough */
                                if (zone_watermark_ok(zone, order, mark,
                                                ac->classzone_idx, alloc_flags))
                                        goto try_this_zone;

                                /*
                                 * Failed to reclaim enough to meet watermark.
                                 * Only mark the zone full if checking the min
                                 * watermark or if we failed to reclaim just
                                 * 1<<order pages or else the page allocator
                                 * fastpath will prematurely mark zones full
                                 * when the watermark is between the low and
                                 * min watermarks.
                                 */
                                if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
                                    ret == ZONE_RECLAIM_SOME)
                                        goto this_zone_full;

                                continue;
                        }
                }

try_this_zone:
                page = buffered_rmqueue(ac->preferred_zone, zone, order,
                                                gfp_mask, ac->migratetype);
                if (page) {
                        if (prep_new_page(page, order, gfp_mask, alloc_flags))
                                goto try_this_zone;
                        return page;
                }
this_zone_full:
                if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
                        zlc_mark_zone_full(zonelist, z);
        }
  • if (IS_ENABLED(CONFIG_NUMA) && zlc_active && !zlc_zone_worth_trying(zonelist, z, allowednodes)) continue;
    • 해당 zone이 zlc에서 full되어 있는 경우 skip한다.
  • ret = zone_reclaim(zone, gfp_mask, order);
    • 해당 zone에 대한 free page 회수를 시행한다.
  • case ZONE_RECLAIM_NOSCAN: continue;
    • 페이지 회수를 위해 scan을 하지 않았을 경우 skip한다.
  • case ZONE_RECLAIM_FULL: continue;
    • 페이지 회수를 시도했지만 회수 되지 않은 경우 skip한다.
  • default: if (zone_watermark_ok(zone, order, mark, ac->classzone_idx, alloc_flags)) goto try_this_zone;
    • 페이지를 회수 해서 다시 워터마크 이내에 들어간 경우 페이지 할당을 시도한다.
  • if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) || ret == ZONE_RECLAIM_SOME) goto this_zone_full;
    • 워터마크 이내에 들어가지 못한 경우 ALLOC_WMARK_MIN으로 설정되었거나 페이지 회수 결과가 일부만 된 경우 zone을 full 상태로 한다.
  • try_this_zone: page = buffered_rmqueue(ac->preferred_zone, zone, order, gfp_mask, ac->migratetype);
    • 해당 zone의 버디 시스템에서 order 페이지를 할당받는다.
  • if (page) { if (prep_new_page(page, order, gfp_mask, alloc_flags)) goto try_this_zone; return page; }
    • 페이지가 할당되었지만 매핑이 안되었거나 _count 및 _mapcount 등이 0으로 초기화되지 않은 경우 문제가 있다고 판단하여 다시 할당을 시도한다.
  • this_zone_full: if (IS_ENABLED(CONFIG_NUMA) && zlc_active) zlc_mark_zone_full(zonelist, z); }
    • zlc의 fullzones에서 지정된 zone의 비트를 설정하여 지정된 zone에서 할당을 할 수 없도록 막는다.

 

        /*
         * The first pass makes sure allocations are spread fairly within the
         * local node.  However, the local node might have free pages left
         * after the fairness batches are exhausted, and remote zones haven't
         * even been considered yet.  Try once more without fairness, and
         * include remote zones now, before entering the slowpath and waking
         * kswapd: prefer spilling to a remote zone over swapping locally.
         */
        if (alloc_flags & ALLOC_FAIR) {
                alloc_flags &= ~ALLOC_FAIR;
                if (nr_fair_skipped) {
                        zonelist_rescan = true;
                        reset_alloc_batches(ac->preferred_zone);
                }
                if (nr_online_nodes > 1)
                        zonelist_rescan = true;
        }

        if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
                /* Disable zlc cache for second zonelist scan */
                zlc_active = 0;
                zonelist_rescan = true;
        }

        if (zonelist_rescan)
                goto zonelist_scan;

        return NULL;
}
  • if (alloc_flags & ALLOC_FAIR) { alloc_flags &= ~ALLOC_FAIR;
    • alloc_flags에서 ALLOC_FAIR 설정이 된 경우 제거한다.
    • 처음 페이지 할당 시도 시 항상 ALLOC_FAIR 플래그를 설정하여 권장 zone의 노드에서만 할당을 시도하는데 실패하게 되면 ALLOC_FAIR 플래그를 제거하고 다른 노드에서도 할당을 할 수 있도록 하게 한다.
  • if (nr_fair_skipped) { zonelist_rescan = true; reset_alloc_batches(ac->preferred_zone); }
    • zone에 free 페이지가 없어서 nr_fair_skipped된 적이 있으면 리스캔 플래그를  설정하고 권장 zone이 있는 노드의 모든 zone에 NR_ALLOC_BATCH stat을 초기화하고 zone의 플래그에서 ZONE_FAIR_DEPLETED를 제거한 후 리스캔하게 한다.
    • NR_ALLOC_BATCH 카운터에 약간의 페이지가 있는 것으로 위장한 후 재 시도를 하는 방법으로 이를 통해 재성공 확률이 높기 때문에 수행하였다.
    • fair zone 정책은 NUMA 시스템의 메모리 정책을 사용하면서 불필요하게 되어 최근 커널 v4.8-rc1에서 제거되었다.
  • if (nr_online_nodes > 1) zonelist_rescan = true;
    • online 노드가 1개를 초과하는 경우 리스캔 플래그를 설정한다.
  • if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) { zlc_active = 0; zonelist_rescan = true; }
    • 누마가 설정되고 zlc_active 중인 경우 lzc_active를 끄고 리스캔 플래그를 설정한다.
  • if (zonelist_rescan) goto zonelist_scan;
    • 리스캔 플래그가 설정된 경우 다시 zonelist 스캔을 한다.

 

CPUSET 관련

cpuset_zone_allowed()

include/linux/cpuset.h

static inline int cpuset_zone_allowed(struct zone *z, gfp_t gfp_mask)
{
        return cpuset_node_allowed(zone_to_nid(z), gfp_mask);
}

요청 zone의 노드가 현재 cpu가 지원하는 노드인 경우 true를 반환한다. 그 외에 우선되는 경우는 인터럽트 수행중에 호출되었거나 __GFP_THISNODE 플래그가 설정되었거나 현재 태스크가 이미 허락하는 노드이거나 태스크가 TIF_MEMDIE 플래그 또는 PF_EXITING 플래그가 설정된 경우는 true를 반환하고 __GFP_HARDWALL이 설정된 경우 현재 태스크의 cpuset이 허락한 메모리 노드가 아닌 노드에 기회를 주지않게 하기 위해 false를 반환한다.

 

cpuset_node_allowed()

include/linux/cpuset.h

static inline int cpuset_node_allowed(int node, gfp_t gfp_mask)
{
        return nr_cpusets() <= 1 || __cpuset_node_allowed(node, gfp_mask);
}

아래 함수와 동일

 

__cpuset_node_allowed()

kernel/cpuset.c

/**
 * cpuset_node_allowed - Can we allocate on a memory node?
 * @node: is this an allowed node?
 * @gfp_mask: memory allocation flags
 *
 * If we're in interrupt, yes, we can always allocate.  If __GFP_THISNODE is
 * set, yes, we can always allocate.  If node is in our task's mems_allowed,
 * yes.  If it's not a __GFP_HARDWALL request and this node is in the nearest
 * hardwalled cpuset ancestor to this task's cpuset, yes.  If the task has been
 * OOM killed and has access to memory reserves as specified by the TIF_MEMDIE
 * flag, yes.
 * Otherwise, no.
 *
 * The __GFP_THISNODE placement logic is really handled elsewhere,
 * by forcibly using a zonelist starting at a specified node, and by
 * (in get_page_from_freelist()) refusing to consider the zones for
 * any node on the zonelist except the first.  By the time any such
 * calls get to this routine, we should just shut up and say 'yes'.
 *
 * GFP_USER allocations are marked with the __GFP_HARDWALL bit,
 * and do not allow allocations outside the current tasks cpuset
 * unless the task has been OOM killed as is marked TIF_MEMDIE.
 * GFP_KERNEL allocations are not so marked, so can escape to the
 * nearest enclosing hardwalled ancestor cpuset.
 *
 * Scanning up parent cpusets requires callback_lock.  The
 * __alloc_pages() routine only calls here with __GFP_HARDWALL bit
 * _not_ set if it's a GFP_KERNEL allocation, and all nodes in the
 * current tasks mems_allowed came up empty on the first pass over
 * the zonelist.  So only GFP_KERNEL allocations, if all nodes in the
 * cpuset are short of memory, might require taking the callback_lock.
 *
 * The first call here from mm/page_alloc:get_page_from_freelist()
 * has __GFP_HARDWALL set in gfp_mask, enforcing hardwall cpusets,
 * so no allocation on a node outside the cpuset is allowed (unless
 * in interrupt, of course).
 *
 * The second pass through get_page_from_freelist() doesn't even call
 * here for GFP_ATOMIC calls.  For those calls, the __alloc_pages()
 * variable 'wait' is not set, and the bit ALLOC_CPUSET is not set
 * in alloc_flags.  That logic and the checks below have the combined
 * affect that:
 *      in_interrupt - any node ok (current task context irrelevant)
 *      GFP_ATOMIC   - any node ok
 *      TIF_MEMDIE   - any node ok
 *      GFP_KERNEL   - any node in enclosing hardwalled cpuset ok
 *      GFP_USER     - only nodes in current tasks mems allowed ok.
 */

 

int __cpuset_node_allowed(int node, gfp_t gfp_mask)
{
        struct cpuset *cs;              /* current cpuset ancestors */
        int allowed;                    /* is allocation in zone z allowed? */
        unsigned long flags;

        if (in_interrupt() || (gfp_mask & __GFP_THISNODE))
                return 1;
        if (node_isset(node, current->mems_allowed))
                return 1;
        /*
         * Allow tasks that have access to memory reserves because they have
         * been OOM killed to get memory anywhere.
         */
        if (unlikely(test_thread_flag(TIF_MEMDIE)))
                return 1;
        if (gfp_mask & __GFP_HARDWALL)  /* If hardwall request, stop here */
                return 0;

        if (current->flags & PF_EXITING) /* Let dying task have memory */
                return 1;

        /* Not hardwall and node outside mems_allowed: scan up cpusets */
        spin_lock_irqsave(&callback_lock, flags);

        rcu_read_lock();
        cs = nearest_hardwall_ancestor(task_cs(current));
        allowed = node_isset(node, cs->mems_allowed);
        rcu_read_unlock();

        spin_unlock_irqrestore(&callback_lock, flags);
        return allowed;
}

요청 노드가 현재 cpu가 지원하는 노드인 경우 true를 반환한다. 그 외에 우선되는 경우는 인터럽트 수행중에 호출되었거나 __GFP_THISNODE 플래그가 설정되었거나 현재 태스크가 이미 허락하는 노드이거나 태스크가 TIF_MEMDIE 플래그 또는 PF_EXITING 플래그가 설정된 경우는 true를 반환하고 __GFP_HARDWALL이 설정된 경우 현재 태스크의 cpuset이 허락한 메모리 노드가 아닌 노드에 기회를 주지않게 하기 위해 false를 반환한다.

  • user space(GFP_USER)에서 요청 시 __GFP_HARDWALL이 포함되어 있다.

 

  • if (in_interrupt() || (gfp_mask & __GFP_THISNODE)) return 1;
    • 인터럽트 핸들러에서 호출되었거나 __GFP_THISNODE 플래그가 설정된 경우 true를 반환한다.
  • if (node_isset(node, current->mems_allowed)) return 1;
    • 현재 태스크가 허락하는 노드인 경우 true를 반환한다.
  • if (unlikely(test_thread_flag(TIF_MEMDIE))) return 1;
    • 낮은 확률로 현재 태스크에 TIF_MEMDIE 플래그가 설정된 경우 true를 반환한다.
  • if (gfp_mask & __GFP_HARDWALL) return 0;
    • __GFP_HARDWALL이 설정된 경우 false를 반환한다.
  • if (current->flags & PF_EXITING) return 1;
    • 현재 태스크가 PF_EXITING 플래그가 설정된 경우 true를 반환한다.
  • cs = nearest_hardwall_ancestor(task_cs(current));
    • 현재 태스크의 cpuset에서 가장 가까운 hardwall 부모 cpuset을 알아온다.
  • allowed = node_isset(node, cs->mems_allowed);
    • cpuset에 허락된 메모리 노드의 여부를 반환한다.

 

nearest_hardwall_ancestor()

kernel/cpuset.c

/*
 * nearest_hardwall_ancestor() - Returns the nearest mem_exclusive or
 * mem_hardwall ancestor to the specified cpuset.  Call holding
 * callback_lock.  If no ancestor is mem_exclusive or mem_hardwall
 * (an unusual configuration), then returns the root cpuset.
 */
static struct cpuset *nearest_hardwall_ancestor(struct cpuset *cs)
{
        while (!(is_mem_exclusive(cs) || is_mem_hardwall(cs)) && parent_cs(cs))
                cs = parent_cs(cs);
        return cs;
}

cpuset이 메모리를 베타적으로 사용하거나 부모 cpuset이 hardwall인 경우 cpuset을 반환한다. 조건을 만족하지 못하면 만족할 때 까지 부모 cpuset을 계속 찾는다.

 

Zone Dirty 관련

zone_dirty_ok()

mm/page-writeback.c

/**
 * zone_dirty_ok - tells whether a zone is within its dirty limits
 * @zone: the zone to check
 *
 * Returns %true when the dirty pages in @zone are within the zone's
 * dirty limit, %false if the limit is exceeded.
 */
bool zone_dirty_ok(struct zone *zone)
{
        unsigned long limit = zone_dirty_limit(zone);

        return zone_page_state(zone, NR_FILE_DIRTY) +
               zone_page_state(zone, NR_UNSTABLE_NFS) +
               zone_page_state(zone, NR_WRITEBACK) <= limit;
}

zone이 사용할 수 있는 제한된 dirty(write buffer) 영역의 크기가 NR_FILE_DRITY + NRUNSTABLE_NFS + WRITEBACK 이상인 경우 true를 반환한다.

  • zone 카운터들 대한 수치 확인은 “cat /proc/zoneinfo” 명령을 통해 간단히 확인할 수 있다.

 

zone_dirty_limit()

mm/page-writeback.c

/**
 * zone_dirty_limit - maximum number of dirty pages allowed in a zone
 * @zone: the zone
 *
 * Returns the maximum number of dirty pages allowed in a zone, based
 * on the zone's dirtyable memory.                      
 */ 
static unsigned long zone_dirty_limit(struct zone *zone)
{                                 
        unsigned long zone_memory = zone_dirtyable_memory(zone);
        struct task_struct *tsk = current; 
        unsigned long dirty;
        
        if (vm_dirty_bytes)
                dirty = DIV_ROUND_UP(vm_dirty_bytes, PAGE_SIZE) *
                        zone_memory / global_dirtyable_memory();
        else
                dirty = vm_dirty_ratio * zone_memory / 100;

        if (tsk->flags & PF_LESS_THROTTLE || rt_task(tsk))
                dirty += dirty / 4;

        return dirty;
}

zone에 허락된 캐시(dirty) 가능한 페이지 수의 일정 비율만큼으로 제한한 페이지 수를 반환한다. 만일 태스크에 PF_LESS_THROTTLE가 설정되어 있거나 우선 순위가 user task보다 높은 태스크인 경우 25%를 추가한다.

  • vm_dirty_bytes가 설정된 경우 zone이 사용하는 dirty 페이지의 비율만큼 배정한다.
  • vm_dirty_bytes가 설정되지 않은 경우 vm_dirty_ratio 백분율 만큼으로 배정한다.
  • 태스크 우선 순위
    • 0~139중 rt_task는 100이하의 높은 우선 순위를 가진다.
      • 100~139는 user space 태스크의 우선순위를 가진다.
      • 낮은 숫자가 가장 높은 우선순위를 가진다.

 

아래 그림은 zone_dirty_limit()을 산출 시 zone_dirty_bytes 또는 zone_dirty_ratio를 사용할 때 달라지는 모습을 보여준다.

zone_dirty_limit-1a

 

zone_dirtyable_memory()

mm/page-writeback.c

/*
 * In a memory zone, there is a certain amount of pages we consider
 * available for the page cache, which is essentially the number of
 * free and reclaimable pages, minus some zone reserves to protect
 * lowmem and the ability to uphold the zone's watermarks without
 * requiring writeback.
 *
 * This number of dirtyable pages is the base value of which the
 * user-configurable dirty ratio is the effictive number of pages that
 * are allowed to be actually dirtied.  Per individual zone, or
 * globally by using the sum of dirtyable pages over all zones.
 *
 * Because the user is allowed to specify the dirty limit globally as
 * absolute number of bytes, calculating the per-zone dirty limit can
 * require translating the configured limit into a percentage of
 * global dirtyable memory first.
 */

/**
 * zone_dirtyable_memory - number of dirtyable pages in a zone
 * @zone: the zone
 *
 * Returns the zone's number of pages potentially available for dirty
 * page cache.  This is the base value for the per-zone dirty limits.
 */
static unsigned long zone_dirtyable_memory(struct zone *zone)
{
        unsigned long nr_pages;

        nr_pages = zone_page_state(zone, NR_FREE_PAGES);
        nr_pages -= min(nr_pages, zone->dirty_balance_reserve);

        nr_pages += zone_page_state(zone, NR_INACTIVE_FILE);
        nr_pages += zone_page_state(zone, NR_ACTIVE_FILE);

        return nr_pages;
}

해당 zone의 캐시 가능한 페이지 수를 반환한다.

  • zone->free 페이지 + zone->file 캐시로 사용 중인 페이지 – zone->dirty_balance_reserve 값을 반환한다.

 

global_dirtyable_memory()

mm/page-writeback.c

/**
 * global_dirtyable_memory - number of globally dirtyable pages
 *
 * Returns the global number of pages potentially available for dirty
 * page cache.  This is the base value for the global dirty limits.
 */
static unsigned long global_dirtyable_memory(void)
{
        unsigned long x;

        x = global_page_state(NR_FREE_PAGES);
        x -= min(x, dirty_balance_reserve);

        x += global_page_state(NR_INACTIVE_FILE);
        x += global_page_state(NR_ACTIVE_FILE);

        if (!vm_highmem_is_dirtyable)
                x -= highmem_dirtyable_memory(x);

        return x + 1;   /* Ensure that we never return 0 */
}

시스템에서 캐시 가능한 페이지 수를 반환한다.

  • free 페이지 + file 캐시로 사용 중인 페이지 – dirty_balance_reserve 값을 반환한다.
  • 만일 highmem을 캐시로 사용되지 못하게 한 경우 highmem의 dirty 페이지 부분을 제외시킨다.

 

highmem_dirtyable_memory()

mm/page-writeback.c

static unsigned long highmem_dirtyable_memory(unsigned long total)
{
#ifdef CONFIG_HIGHMEM
        int node;
        unsigned long x = 0;

        for_each_node_state(node, N_HIGH_MEMORY) {
                struct zone *z = &NODE_DATA(node)->node_zones[ZONE_HIGHMEM];

                x += zone_dirtyable_memory(z);
        }
        /*
         * Unreclaimable memory (kernel memory or anonymous memory
         * without swap) can bring down the dirtyable pages below
         * the zone's dirty balance reserve and the above calculation
         * will underflow.  However we still want to add in nodes
         * which are below threshold (negative values) to get a more
         * accurate calculation but make sure that the total never
         * underflows.
         */
        if ((long)x < 0)
                x = 0;

        /*
         * Make sure that the number of highmem pages is never larger
         * than the number of the total dirtyable memory. This can only
         * occur in very strange VM situations but we want to make sure
         * that this does not occur.
         */
        return min(x, total);
#else
        return 0;
#endif
}

high memory에 대한 dirty 페이지 가능한 수를 알아온다.

 

ZLC(Zone List Cache) 관련

zlc_setup()

mm/page_alloc.c

#ifdef CONFIG_NUMA
/*
 * zlc_setup - Setup for "zonelist cache".  Uses cached zone data to
 * skip over zones that are not allowed by the cpuset, or that have
 * been recently (in last second) found to be nearly full.  See further
 * comments in mmzone.h.  Reduces cache footprint of zonelist scans
 * that have to skip over a lot of full or unallowed zones.
 *
 * If the zonelist cache is present in the passed zonelist, then
 * returns a pointer to the allowed node mask (either the current
 * tasks mems_allowed, or node_states[N_MEMORY].)
 *
 * If the zonelist cache is not available for this zonelist, does
 * nothing and returns NULL.
 *
 * If the fullzones BITMAP in the zonelist cache is stale (more than
 * a second since last zap'd) then we zap it out (clear its bits.)
 *
 * We hold off even calling zlc_setup, until after we've checked the
 * first zone in the zonelist, on the theory that most allocations will
 * be satisfied from that first zone, so best to examine that zone as
 * quickly as we can.
 */
static nodemask_t *zlc_setup(struct zonelist *zonelist, int alloc_flags)
{
        struct zonelist_cache *zlc;     /* cached zonelist speedup info */
        nodemask_t *allowednodes;       /* zonelist_cache approximation */

        zlc = zonelist->zlcache_ptr;
        if (!zlc)
                return NULL;

        if (time_after(jiffies, zlc->last_full_zap + HZ)) {
                bitmap_zero(zlc->fullzones, MAX_ZONES_PER_ZONELIST);
                zlc->last_full_zap = jiffies;
        }

        allowednodes = !in_interrupt() && (alloc_flags & ALLOC_CPUSET) ?
                                        &cpuset_current_mems_allowed :
                                        &node_states[N_MEMORY];
        return allowednodes;
}
#endif

NUMA 시스템에서 zlc 기능이 설정된 경우 페이지를 할당 시 이 루틴이 호출되면 현재 태스크가 메모리 할당 가능한 노드들을 반환한다. 또한 1초 간격으로 zlc->fullzones를 clear하여 각 zone의 메모리 full 상태를 해제한다.

  • zlc 기능은 속도 향상에 크게 관여하지 못하게되어 커널 v4.4에서 삭제된다.

 

  • if (time_after(jiffies, zlc->last_full_zap + HZ)) { bitmap_zero(zlc->fullzones, MAX_ZONES_PER_ZONELIST); zlc->last_full_zap = jiffies; }
    • 1초가 경과된 경우 fullzones를 clear하여 각 zone의 메모리 full 상태를 해제한다.
  • allowednodes = !in_interrupt() && (alloc_flags & ALLOC_CPUSET) ? &cpuset_current_mems_allowed : &node_states[N_MEMORY];
    • 인터럽트 핸들러에 의해 호출된 경우가 아니면서 ALLOC_CPUSET 플래그가 설정된 경우 태스크에 한정된(current->mems_allowed) 노드 비트맵을 반환하고 그렇지 않은 경우 메모리가 있는 노드 비트맵을 반환한다.

 

zlc_zone_worth_trying()

mm/page_alloc.c

/*
 * Given 'z' scanning a zonelist, run a couple of quick checks to see
 * if it is worth looking at further for free memory:
 *  1) Check that the zone isn't thought to be full (doesn't have its
 *     bit set in the zonelist_cache fullzones BITMAP).
 *  2) Check that the zones node (obtained from the zonelist_cache
 *     z_to_n[] mapping) is allowed in the passed in allowednodes mask.
 * Return true (non-zero) if zone is worth looking at further, or
 * else return false (zero) if it is not.
 *
 * This check -ignores- the distinction between various watermarks,
 * such as GFP_HIGH, GFP_ATOMIC, PF_MEMALLOC, ...  If a zone is
 * found to be full for any variation of these watermarks, it will
 * be considered full for up to one second by all requests, unless
 * we are so low on memory on all allowed nodes that we are forced
 * into the second scan of the zonelist.
 *
 * In the second scan we ignore this zonelist cache and exactly
 * apply the watermarks to all zones, even it is slower to do so.
 * We are low on memory in the second scan, and should leave no stone
 * unturned looking for a free page.
 */
static int zlc_zone_worth_trying(struct zonelist *zonelist, struct zoneref *z,
                                                nodemask_t *allowednodes)
{
        struct zonelist_cache *zlc;     /* cached zonelist speedup info */
        int i;                          /* index of *z in zonelist zones */
        int n;                          /* node that zone *z is on */

        zlc = zonelist->zlcache_ptr;
        if (!zlc)
                return 1;

        i = z - zonelist->_zonerefs;
        n = zlc->z_to_n[i];

        /* This zone is worth trying if it is allowed but not full */
        return node_isset(n, *allowednodes) && !test_bit(i, zlc->fullzones);
}

허락된 노드들 중 zlc(Zone List Cache)를 통해 해당 zone의 메모리가 대략 full 상태인 경우 true를 반환한다.

 

zlc_mark_zone_full()

mm/page_alloc.c

/*
 * Given 'z' scanning a zonelist, set the corresponding bit in
 * zlc->fullzones, so that subsequent attempts to allocate a page
 * from that zone don't waste time re-examining it.
 */
static void zlc_mark_zone_full(struct zonelist *zonelist, struct zoneref *z)
{
        struct zonelist_cache *zlc;     /* cached zonelist speedup info */
        int i;                          /* index of *z in zonelist zones */

        zlc = zonelist->zlcache_ptr;
        if (!zlc)
                return;

        i = z - zonelist->_zonerefs;

        set_bit(i, zlc->fullzones);
}

zlc의 fullzones에서 지정된 zone의 비트를 설정하여 지정된 zone에서 할당을 할 수 없도록 막는다.

 

기타

zone_allows_reclaim()

mm/page_alloc.c

static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone)
{
        return node_distance(zone_to_nid(local_zone), zone_to_nid(zone)) <
                                RECLAIM_DISTANCE;
}

local_zone과 요청 zone이 RECLAIM_DISTANCE(30) 이내의 거리에 있는 경우 true를 반환한다.

  • 페이지 회수는 가까운 리모트 zone에서만 가능하게 한다.

prep_new_page()

page_alloc.c

static int prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags,
                                                                int alloc_flags)
{
        int i;

        for (i = 0; i < (1 << order); i++) {
                struct page *p = page + i;
                if (unlikely(check_new_page(p)))
                        return 1;
        }

        set_page_private(page, 0);
        set_page_refcounted(page);

        arch_alloc_page(page, order);
        kernel_map_pages(page, 1 << order, 1);
        kasan_alloc_pages(page, order);

        if (gfp_flags & __GFP_ZERO)
                prep_zero_page(page, order, gfp_flags);

        if (order && (gfp_flags & __GFP_COMP))
                prep_compound_page(page, order);

        set_page_owner(page, order, gfp_flags);

        /*
         * page->pfmemalloc is set when ALLOC_NO_WATERMARKS was necessary to
         * allocate the page. The expectation is that the caller is taking
         * steps that will free more memory. The caller should avoid the page
         * being used for !PFMEMALLOC purposes.
         */
        page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);

        return 0;
}

할당 받은 페이지에 문제가 있는 경우 true를 반환하고 그렇지 않은 경우 할당 받은 페이지를 초기화하고 false를 반환한다.

  • for (i = 0; i < (1 << order); i++) { struct page *p = page + i; if (unlikely(check_new_page(p))) return 1; }
    • 할당 받은 모든 페이지들을 검사하여 문제가 있는 경우 true를 반환한다.
  • set_page_private(page, 0);
    • 페이지의 private에 0을 대입한다.
  • set_page_refcounted(page);
    • 페이지의 _refcount에 1을 대입한다.
  • arch_alloc_page(page, order);
    • 페이지 할당에 대한 아키텍처 고유 함수가 준비된 경우 수행한다.
      • 32bit arm에서는 빈 함수이다.
  • kernel_map_pages(page, 1 << order, 1);
    • CONFIG_DEBUG_PAGEALLOC 커널 옵션을 사용하는 디버그에서 사용된다.
  • kasan_alloc_pages(page, order);
    • CONFIG_KASAN 커널 옵션을 사용하는 디버그에서 사용된다.
  • if (gfp_flags & __GFP_ZERO) prep_zero_page(page, order, gfp_flags);
    • __GFP_ZERO 플래그를 사용한 경우 페이지를 0으로 초기화한다.
      • 이 함수 내부에서는 highmem 페이지에 대한 초기화를 위해 kmap() 과 kummap() 함수를 이용한다. 따라서 인터럽트 context에서 사용되면 안된다.
  • if (order && (gfp_flags & __GFP_COMP)) prep_compound_page(page, order);
    • __GFP_COMP 플래그가 사용된 경우 compound 페이지를 설정한다.
  •  set_page_owner(page, order, gfp_flags);
    • page_ext->order, page_ext->gfp_mask를 설정하고 page_ext->flags에 PAGE_EXT_OWNER 비트를 설정한다.
  • page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
    • page->pfmemalloc에 ALLOC_NO_WATERMARKS 할당 플래그를 사용했는지 여부를 대입한다.

 

prep_zero_page()

mm/page_alloc.c

static inline void prep_zero_page(struct page *page, unsigned int order,
                                                        gfp_t gfp_flags)
{
        int i;

        /*
         * clear_highpage() will use KM_USER0, so it's a bug to use __GFP_ZERO
         * and __GFP_HIGHMEM from hard or soft interrupt context.
         */
        VM_BUG_ON((gfp_flags & __GFP_HIGHMEM) && in_interrupt());
        for (i = 0; i < (1 << order); i++)
                clear_highpage(page + i);
}

order 페이지의 모든 페이지에 대해 0으로 초기화한다.

  • 내부에서 highmem 페이지에 대한 초기화를 위해 kmap() 과 kummap() 함수를 이용한다. 따라서 이 함수는 인터럽트 context에서 사용되면 안된다.

 

prep_compound_page()

/mm/page_alloc.c

void prep_compound_page(struct page *page, unsigned long order)
{
        int i;
        int nr_pages = 1 << order;

        set_compound_page_dtor(page, free_compound_page);
        set_compound_order(page, order);
        __SetPageHead(page);
        for (i = 1; i < nr_pages; i++) {
                struct page *p = page + i;
                set_page_count(p, 0); 
                p->first_page = page;
                /* Make sure p->first_page is always valid for PageTail() */
                smp_wmb();
                __SetPageTail(p);
        }
}

compound 페이지를 설정한다.

  • 파괴자에 free_compound_page() 함수를 대입한다.
  • 두 번째 페이지에 order를 설정한다.
  • 헤더 페이지의 플래그에 PG_Head 비트를 설정한다.
  • 나머지 페이지의 _count에 0을 대입하고, first_page는 헤드 페이지를 가리키게 한 후 플래그에 PG_Tail 비트를 설정한다.

 

reset_alloc_batches()

mm/page_alloc.c

static void reset_alloc_batches(struct zone *preferred_zone)
{
        struct zone *zone = preferred_zone->zone_pgdat->node_zones;

        do {
                mod_zone_page_state(zone, NR_ALLOC_BATCH,
                        high_wmark_pages(zone) - low_wmark_pages(zone) -
                        atomic_long_read(&zone->vm_stat[NR_ALLOC_BATCH]));
                clear_bit(ZONE_FAIR_DEPLETED, &zone->flags);
        } while (zone++ != preferred_zone);
}

권장 zone의 노드에 있는 zone에 대해서 처음부터 권장 zone이 나올때까지 루프를 돌며 NR_ALLOC_BATCH stat에 최고 워터마크에서 최저 워터마크를 빼고 현재의 NR_ALLOC_BATCH 값을 뺀 값으로 초기화하고 zone 플래그의 ZONE_FAIR_DEPLETED 비트를 클리어한다.

 

memalloc_noio_flags()

include/linux/sched.h

static inline gfp_t memalloc_noio_flags(gfp_t flags)
{
        if (unlikely(current->flags & PF_MEMALLOC_NOIO))
                flags &= ~(__GFP_IO | __GFP_FS);
        return flags;
}

현재 태스크의 플래그에 PF_MEMALLOC_NOIO가 사용되는 경우 인수 flags에서 __GFP_IO 및 __GFP_FS를 삭제한다.

 

구조체

alloc_context 구조체

mm/internal.h

/*
 * Structure for holding the mostly immutable allocation parameters passed
 * between functions involved in allocations, including the alloc_pages*
 * family of functions.
 *      
 * nodemask, migratetype and high_zoneidx are initialized only once in
 * __alloc_pages_nodemask() and then never change.
 *      
 * zonelist, preferred_zone and classzone_idx are set first in
 * __alloc_pages_nodemask() for the fast path, and might be later changed
 * in __alloc_pages_slowpath(). All other functions pass the whole strucure
 * by a const pointer.
 */
struct alloc_context {
        struct zonelist *zonelist;
        nodemask_t *nodemask;           
        struct zone *preferred_zone;
        int classzone_idx;
        int migratetype;
        enum zone_type high_zoneidx;
};
  • zonelist
    • 페이지 할당 시 사용하는 zonelist
  • nodemask
    • __alloc_pages_nodemask()에서 처음 한 번 초기화된 후 변경되지 않는다.
  • preferred_zone
    • zonelist의 fastpath를 위해 사용된다. __alloc_pages_slowpath()에서 변경될 수 있다.
  • classzone_idx
    • preferred_zone의 zoneref에 해당하는 인덱스 번호
  • migratetype
    • migrate type은 __alloc_pages_nodemask()에서 처음 한 번 초기화된 후 변경되지 않는다.
  • high_zoneidx
    • high zone 타입의 인덱스로 __alloc_pages_nodemask()에서 처음 한 번 초기화된 후 변경되지 않는다.

 

참고

 

 

 

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.