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

 

참고

 

답글 남기기

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