Zoned Allocator -8- (Direct Reclaim-Shrink-1)

다음 그림은 페이지 회수를 위해 shrink_zones() 함수 호출 시 처리되는 함수 호출 관계를 보여준다.

shrink_zones-1b

 

Shrink Zones

shrink_zones()

mm/vmscan.c

/*
 * This is the direct reclaim path, for page-allocating processes.  We only
 * try to reclaim pages from zones which will satisfy the caller's allocation
 * request.
 *
 * We reclaim from a zone even if that zone is over high_wmark_pages(zone).
 * Because:
 * a) The caller may be trying to free *extra* pages to satisfy a higher-order
 *    allocation or
 * b) The target zone may be at high_wmark_pages(zone) but the lower zones
 *    must go *over* high_wmark_pages(zone) to satisfy the `incremental min'
 *    zone defense algorithm.
 *
 * If a zone is deemed to be full of pinned pages then just give it a light
 * scan then give up on it.
 *
 * Returns true if a zone was reclaimable.
 */
static bool shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
{
        struct zoneref *z;
        struct zone *zone;
        unsigned long nr_soft_reclaimed;
        unsigned long nr_soft_scanned;
        gfp_t orig_mask;
        enum zone_type requested_highidx = gfp_zone(sc->gfp_mask);
        bool reclaimable = false;

        /*
         * If the number of buffer_heads in the machine exceeds the maximum
         * allowed level, force direct reclaim to scan the highmem zone as
         * highmem pages could be pinning lowmem pages storing buffer_heads
         */
        orig_mask = sc->gfp_mask;
        if (buffer_heads_over_limit)
                sc->gfp_mask |= __GFP_HIGHMEM;

        for_each_zone_zonelist_nodemask(zone, z, zonelist,
                                        requested_highidx, sc->nodemask) {
                enum zone_type classzone_idx;

                if (!populated_zone(zone))
                        continue;

                classzone_idx = requested_highidx;
                while (!populated_zone(zone->zone_pgdat->node_zones +
                                                        classzone_idx))
                        classzone_idx--;

요청 zone의 zonelist를 대상으로 필요한 zone에 대해 페이지 회수를 수행한다.

 

  • enum zone_type requested_highidx = gfp_zone(sc->gfp_mask);
    • 요청 zone의 인덱스
  • if (buffer_heads_over_limit) sc->gfp_mask |= __GFP_HIGHMEM;
    • 버퍼 헤드의 수가 최대 허락된 레벨을 초과하는 경우 페이지 회수 스캐닝에 highmem zone도 포함시킨다.
  • for_each_zone_zonelist_nodemask(zone, z, zonelist, requested_highidx, sc->nodemask) {
    • zonelist에서 요청zone 이하 및 노드를 대상으로 루프를 돈다.
  • if (!populated_zone(zone)) continue;
    • 활성화된 zone이 아니면 skip
  • classzone_idx = requested_highidx; while (!populated_zone(zone->zone_pgdat->node_zones + classzone_idx)) classzone_idx–;
    • 현재 zone 노드에 있는 zone들에서 classzone_idx zone 부터 가장 낮은 zone까지 감소하며 활성화된 zone을 찾는다.

 

                /*
                 * Take care memory controller reclaiming has small influence
                 * to global LRU.
                 */
                if (global_reclaim(sc)) {
                        if (!cpuset_zone_allowed(zone,
                                                 GFP_KERNEL | __GFP_HARDWALL))
                                continue;

                        if (sc->priority != DEF_PRIORITY &&
                            !zone_reclaimable(zone))
                                continue;       /* Let kswapd poll it */

                        /*
                         * If we already have plenty of memory free for
                         * compaction in this zone, don't free any more.
                         * Even though compaction is invoked for any
                         * non-zero order, only frequent costly order
                         * reclamation is disruptive enough to become a
                         * noticeable problem, like transparent huge
                         * page allocations.
                         */
                        if (IS_ENABLED(CONFIG_COMPACTION) &&
                            sc->order > PAGE_ALLOC_COSTLY_ORDER &&
                            zonelist_zone_idx(z) <= requested_highidx &&
                            compaction_ready(zone, sc->order)) {
                                sc->compaction_ready = true;
                                continue;
                        }

                        /*
                         * This steals pages from memory cgroups over softlimit
                         * and returns the number of reclaimed pages and
                         * scanned pages. This works for global memory pressure
                         * and balancing, not for a memcg's limit.
                         */
                        nr_soft_scanned = 0;
                        nr_soft_reclaimed = mem_cgroup_soft_limit_reclaim(zone,
                                                sc->order, sc->gfp_mask,
                                                &nr_soft_scanned);
                        sc->nr_reclaimed += nr_soft_reclaimed;
                        sc->nr_scanned += nr_soft_scanned;
                        if (nr_soft_reclaimed)
                                reclaimable = true;
                        /* need some check for avoid more shrink_zone() */
                }

                if (shrink_zone(zone, sc, zone_idx(zone) == classzone_idx))
                        reclaimable = true;

                if (global_reclaim(sc) &&
                    !reclaimable && zone_reclaimable(zone))
                        reclaimable = true;
        }

        /*
         * Restore to original mask to avoid the impact on the caller if we
         * promoted it to __GFP_HIGHMEM.
         */
        sc->gfp_mask = orig_mask;

        return reclaimable;
}
  • if (global_reclaim(sc)) {
    • 지정된 memcg가 없어서 global reclaim을 사용해야 하는 경우
  • if (!cpuset_zone_allowed(zone, GFP_KERNEL | __GFP_HARDWALL)) continue;
    • cpuset이 GFP_KERNEL 및 __GFP_HARDWALL 플래그 요청으로 이 zone에서 허락되지 않는 경우 skip
  • if (sc->priority != DEF_PRIORITY && !zone_reclaimable(zone)) continue;
    • 우선순위가 default 우선순위로 부터 바뀌었고 회수 가능한 상태가 아닌 경우 skip
  • if (IS_ENABLED(CONFIG_COMPACTION) && sc->order > PAGE_ALLOC_COSTLY_ORDER && zonelist_zone_idx(z) <= requested_highidx  && compaction_ready(zone, sc->order)) { sc->compaction_ready = true; continue; }
    • 요청 order가 3을 초과하면서 compaction 없이 high order를 처리할 수 있을거라 판단하면  skip
  • nr_soft_scanned = 0; nr_soft_reclaimed = mem_cgroup_soft_limit_reclaim(zone, sc->order, sc->gfp_mask, &nr_soft_scanned);
    • memcg 소프트 제한된 페이지 회수를 시도하여 회수된 페이지를 알아온다.
  • if (shrink_zone(zone, sc, zone_idx(zone) == classzone_idx)) reclaimable = true;
    • zone에서 페이지를 회수한 결과가 true인 경우 반환 값에 true를 대입한다.
  • if (global_reclaim(sc) && !reclaimable && zone_reclaimable(zone)) reclaimable = true;
    • zone에서 페이지 회수가 가능한 경우 반환 값에 true를 대입한다.
  • sc->gfp_mask = orig_mask; return reclaimable;
    • 백업해두었던 gfp_mask를 복구하고 회수 가능 여부를 리턴한다.

 

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

shrink_zones-2

zone_reclaimable()

mm/vmscan.c

bool zone_reclaimable(struct zone *zone)
{
        return zone_page_state(zone, NR_PAGES_SCANNED) <
                zone_reclaimable_pages(zone) * 6;
}

zone에서 스캔된 페이지 수가 회수할 수 있는 페이지의 6배보다 작은 경우 회수가 가능하다고 판단한다.

 

zone_reclaimable_pages()

mm/vmscan.c

static unsigned long zone_reclaimable_pages(struct zone *zone)
{
        int nr;

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

        if (get_nr_swap_pages() > 0)
                nr += zone_page_state(zone, NR_ACTIVE_ANON) +
                      zone_page_state(zone, NR_INACTIVE_ANON);

        return nr;
}

요청 zone의 최대 회수 가능한 페이지 수를 알아온다.

  • active file + inactive file 건 수를 더한 수를 반환환다. 만일 swap 페이지가 있는 경우 active anon과 inactive anon 건 수도 더해 반환한다.

 

get_nr_swap_pages()

include/linux/swap.h

static inline long get_nr_swap_pages(void)
{
        return atomic_long_read(&nr_swap_pages);
}

swap 페이지 수를 반환한다.

 

compaction_ready()

mm/vmscan.c

/*
 * Returns true if compaction should go ahead for a high-order request, or
 * the high-order allocation would succeed without compaction.
 */
static inline bool compaction_ready(struct zone *zone, int order)
{
        unsigned long balance_gap, watermark;
        bool watermark_ok;

        /*
         * Compaction takes time to run and there are potentially other
         * callers using the pages just freed. Continue reclaiming until
         * there is a buffer of free pages available to give compaction
         * a reasonable chance of completing and allocating the page
         */
        balance_gap = min(low_wmark_pages(zone), DIV_ROUND_UP(
                        zone->managed_pages, KSWAPD_ZONE_BALANCE_GAP_RATIO));
        watermark = high_wmark_pages(zone) + balance_gap + (2UL << order);
        watermark_ok = zone_watermark_ok_safe(zone, 0, watermark, 0, 0);

        /*
         * If compaction is deferred, reclaim up to a point where
         * compaction will have a chance of success when re-enabled
         */
        if (compaction_deferred(zone, order))
                return watermark_ok;

        /*
         * If compaction is not ready to start and allocation is not likely
         * to succeed without it, then keep reclaiming.
         */
        if (compaction_suitable(zone, order, 0, 0) == COMPACT_SKIPPED)
                return false;

        return watermark_ok;
}

compaction 없이 high order를 처리할 수 있을거라 판단하면 true를 반환한다.

  • balance_gap = min(low_wmark_pages(zone), DIV_ROUND_UP(zone->managed_pages, KSWAPD_ZONE_BALANCE_GAP_RATIO));
    • 밸런스 갭에 low 워터마크 페이지 또는 managed_pages를 100으로 round up 한 후 100으로 나눈 값 중 가장 작은 값을 담는다.
  • watermark = high_wmark_pages(zone) + balance_gap + (2UL << order);  watermark_ok = zone_watermark_ok_safe(zone, 0, watermark, 0, 0);
    • high 워터마크 페이지 + 밸런스 갭 + order 페이지 x 2로 0 order 워터마크 체크가 ok인지 여부를 파악한다.
  • if (compaction_deferred(zone, order)) return watermark_ok;
    • compaction을 skip해도 좋을 때 zone의 compact_considered=0으로 리셋하고, compact_defer_shift를 증가시킨 후 함수를 빠져나간다.
  • if (compaction_suitable(zone, order, 0, 0) == COMPACT_SKIPPED) return false;
    • 요청 zone에서 과 order 페이지를 compaction 할 필요 없는 상태인 경우 false로 함수를 빠져나간다.

 

shrink_zone()

mm/vmscan.c

static bool shrink_zone(struct zone *zone, struct scan_control *sc,
                        bool is_classzone)
{
        struct reclaim_state *reclaim_state = current->reclaim_state;
        unsigned long nr_reclaimed, nr_scanned;
        bool reclaimable = false;

        do {
                struct mem_cgroup *root = sc->target_mem_cgroup;
                struct mem_cgroup_reclaim_cookie reclaim = {
                        .zone = zone,
                        .priority = sc->priority,
                };
                unsigned long zone_lru_pages = 0;
                struct mem_cgroup *memcg;

                nr_reclaimed = sc->nr_reclaimed;
                nr_scanned = sc->nr_scanned;

                memcg = mem_cgroup_iter(root, NULL, &reclaim);
                do {
                        unsigned long lru_pages;
                        unsigned long scanned;
                        struct lruvec *lruvec;
                        int swappiness;

                        if (mem_cgroup_low(root, memcg)) {
                                if (!sc->may_thrash)
                                        continue;
                                mem_cgroup_events(memcg, MEMCG_LOW, 1);
                        }

                        lruvec = mem_cgroup_zone_lruvec(zone, memcg);
                        swappiness = mem_cgroup_swappiness(memcg);
                        scanned = sc->nr_scanned;

                        shrink_lruvec(lruvec, swappiness, sc, &lru_pages);
                        zone_lru_pages += lru_pages;

                        if (memcg && is_classzone)
                                shrink_slab(sc->gfp_mask, zone_to_nid(zone),
                                            memcg, sc->nr_scanned - scanned,
                                            lru_pages);

                        /*
                         * Direct reclaim and kswapd have to scan all memory
                         * cgroups to fulfill the overall scan target for the
                         * zone.
                         *
                         * Limit reclaim, on the other hand, only cares about
                         * nr_to_reclaim pages to be reclaimed and it will
                         * retry with decreasing priority if one round over the
                         * whole hierarchy is not sufficient.
                         */
                        if (!global_reclaim(sc) &&
                                        sc->nr_reclaimed >= sc->nr_to_reclaim) {
                                mem_cgroup_iter_break(root, memcg);
                                break;
                        }
                } while ((memcg = mem_cgroup_iter(root, memcg, &reclaim)));
  • struct reclaim_state *reclaim_state = current->reclaim_state;
    • 현재 태스크의 reclaim 상태
  • struct mem_cgroup *root = sc->target_mem_cgroup;
    • root에 타겟 memcg를 대입한다.
  • memcg = mem_cgroup_iter(root, NULL, &reclaim);
    • root 다음 memcg를 알아온다. root가 지정되지 않은 경우 null을 반환한다.
  • if (mem_cgroup_low(root, memcg)) { if (!sc->may_thrash) continue; mem_cgroup_events(memcg, MEMCG_LOW, 1); }
    • 메모리 소비가 기본 범위 미만이고 may_thrash가 설정되지 않은 경우 skip 하고 설정된 경우 MEMCG_LOW stat을 증가시킨다.
  • lruvec = mem_cgroup_zone_lruvec(zone, memcg);
    • memcg로부터 lruvec를 알아온다. memcg가 없는 경우는 zone->lruvec를 알아온다.
  • swappiness = mem_cgroup_swappiness(memcg);
    • memcg로부터 swappiness 값을 구해온다.
      • memcg가 없는 경우 기본 값 60을 구해온다.
      • swappiness는 0 ~ 100까지 범위이다. 큰 값일 수록 swappy 이다.
  • shrink_lruvec(lruvec, swappiness, sc, &lru_pages); zone_lru_pages += lru_pages;
    • lruvec로부터 swappiness 만큼 shrink 하고 evictable 페이지 수를 zone_lru_pages에 추가한다.
  • if (memcg && is_classzone) shrink_slab(sc->gfp_mask, zone_to_nid(zone), memcg, sc->nr_scanned – scanned, lru_pages);
    • memcg과 is_classzone이 지정된 경우 slab을 shrink 한다.
  • if (!global_reclaim(sc) && sc->nr_reclaimed >= sc->nr_to_reclaim) { mem_cgroup_iter_break(root, memcg); break; }
    • global reclaim이 아니면서 회수된 페이지가 회수될 페이지 이상인 경우 fre page가 확보되었으므로 루프를 break 한다.
  • } while ((memcg = mem_cgroup_iter(root, memcg, &reclaim)));
    • 다음 memcg를 찾아서 루프를 돈다.

 

                /*
                 * Shrink the slab caches in the same proportion that
                 * the eligible LRU pages were scanned.
                 */
                if (global_reclaim(sc) && is_classzone)
                        shrink_slab(sc->gfp_mask, zone_to_nid(zone), NULL,
                                    sc->nr_scanned - nr_scanned,
                                    zone_lru_pages);

                if (reclaim_state) {
                        sc->nr_reclaimed += reclaim_state->reclaimed_slab;
                        reclaim_state->reclaimed_slab = 0;
                }

                vmpressure(sc->gfp_mask, sc->target_mem_cgroup,
                           sc->nr_scanned - nr_scanned,
                           sc->nr_reclaimed - nr_reclaimed);

                if (sc->nr_reclaimed - nr_reclaimed)
                        reclaimable = true;

        } while (should_continue_reclaim(zone, sc->nr_reclaimed - nr_reclaimed,
                                         sc->nr_scanned - nr_scanned, sc));

        return reclaimable;
}
  • if (global_reclaim(sc) && is_classzone) shrink_slab(sc->gfp_mask, zone_to_nid(zone), NULL, sc->nr_scanned – nr_scanned, zone_lru_pages);
    • 지정된 memcg를 사용하지 않으면서 is_classzone이 지정된 경우 slab을 shrink한다.
  • if (reclaim_state) { sc->nr_reclaimed += reclaim_state->reclaimed_slab; reclaim_state->reclaimed_slab = 0; }
    • reclaim_state가 null이 아닌 경우 회수된 페이지 수에 회수된 slab 페이지 갯수를 더한다.
  • vmpressure(sc->gfp_mask, sc->target_mem_cgroup, sc->nr_scanned – nr_scanned, sc->nr_reclaimed – nr_reclaimed);
    • scaned 및 reclaimed 비율로 메모리 pressure를 계량한다.
  • if (sc->nr_reclaimed – nr_reclaimed) reclaimable = true;
    • 회수된 페이지가 있는 경우 반환 값 reclaimable을 true로 대입한다.
  • } while (should_continue_reclaim(zone, sc->nr_reclaimed – nr_reclaimed, sc->nr_scanned – nr_scanned, sc));
    • reclaim을 계속 해야 하는 동안 반복한다.

 

shrink_lruvec()

mm/vmscan.c

/*
 * This is a basic per-zone page freer.  Used by both kswapd and direct reclaim.
 */
static void shrink_lruvec(struct lruvec *lruvec, int swappiness,
                          struct scan_control *sc, unsigned long *lru_pages)
{
        unsigned long nr[NR_LRU_LISTS];
        unsigned long targets[NR_LRU_LISTS];
        unsigned long nr_to_scan;
        enum lru_list lru;
        unsigned long nr_reclaimed = 0;
        unsigned long nr_to_reclaim = sc->nr_to_reclaim;
        struct blk_plug plug;
        bool scan_adjusted;

        get_scan_count(lruvec, swappiness, sc, nr, lru_pages);

        /* Record the original scan target for proportional adjustments later */
        memcpy(targets, nr, sizeof(nr));

        /*
         * Global reclaiming within direct reclaim at DEF_PRIORITY is a normal
         * event that can occur when there is little memory pressure e.g.
         * multiple streaming readers/writers. Hence, we do not abort scanning
         * when the requested number of pages are reclaimed when scanning at
         * DEF_PRIORITY on the assumption that the fact we are direct
         * reclaiming implies that kswapd is not keeping up and it is best to
         * do a batch of work at once. For memcg reclaim one check is made to
         * abort proportional reclaim if either the file or anon lru has already
         * dropped to zero at the first pass.
         */
        scan_adjusted = (global_reclaim(sc) && !current_is_kswapd() &&
                         sc->priority == DEF_PRIORITY);

        blk_start_plug(&plug);
        while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
                                        nr[LRU_INACTIVE_FILE]) {
                unsigned long nr_anon, nr_file, percentage;
                unsigned long nr_scanned;

                for_each_evictable_lru(lru) {
                        if (nr[lru]) {
                                nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
                                nr[lru] -= nr_to_scan;

                                nr_reclaimed += shrink_list(lru, nr_to_scan,
                                                            lruvec, sc);
                        }
                }

                if (nr_reclaimed < nr_to_reclaim || scan_adjusted)
                        continue;
  • unsigned long nr[NR_LRU_LISTS];
  • get_scan_count(lruvec, swappiness, sc, nr, lru_pages);
    • 요청한 lruvec에 대해 밸런스를 고려하여 스캔할 페이지 비율을 산출한다.
  • memcpy(targets, nr, sizeof(nr));
    • 나중에 일부 조정을 위해 산출된 nr[] 배열을 targets[] 배열에 백업해둔다.
  • scan_adjusted = (global_reclaim(sc) && !current_is_kswapd() && sc->priority == DEF_PRIORITY);
    • direct reclaim을 사용하는 global reclaim이 처음 시도하는 경우메모리 압박이 적어서 scan_adjust를 설정한다.
  • while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] || nr[LRU_INACTIVE_FILE]) {
    • nr[]배열에서 inactive anon+file 또는 active file이 0보다 큰 경우 루프를 돈다.
      • active anon은 제외한다.
  • for_each_evictable_lru(lru) { if (nr[lru]) {
    • nr[] 배열의 evictable lru에 대해서만 그 수가 0보다 큰 경우
  • nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX); nr[lru] -= nr_to_scan; nr_reclaimed += shrink_list(lru, nr_to_scan, lruvec, sc); }
    • 해당 lru에 대해 SWAP_CLUSTER_MAX(128)을 초과하지 않는 nr[lru] 만큼 shrink(스캔 -> 분리 -> 회수)를 수행하고 회수한 페이지 수를 nr_reclaimed에 추가한다.
  • if (nr_reclaimed < nr_to_reclaim || scan_adjusted) continue;
    • 아직 회수할 페이지가 회수한 페이지보다 크거나 scan_adjusted가 설정된 경우 계속 처리한다.

 

                /*
                 * For kswapd and memcg, reclaim at least the number of pages
                 * requested. Ensure that the anon and file LRUs are scanned
                 * proportionally what was requested by get_scan_count(). We
                 * stop reclaiming one LRU and reduce the amount scanning
                 * proportional to the original scan target.
                 */
                nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE];
                nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON];

                /*
                 * It's just vindictive to attack the larger once the smaller
                 * has gone to zero.  And given the way we stop scanning the
                 * smaller below, this makes sure that we only make one nudge
                 * towards proportionality once we've got nr_to_reclaim.
                 */
                if (!nr_file || !nr_anon)
                        break;

                if (nr_file > nr_anon) {
                        unsigned long scan_target = targets[LRU_INACTIVE_ANON] +
                                                targets[LRU_ACTIVE_ANON] + 1;
                        lru = LRU_BASE;
                        percentage = nr_anon * 100 / scan_target;
                } else {
                        unsigned long scan_target = targets[LRU_INACTIVE_FILE] +
                                                targets[LRU_ACTIVE_FILE] + 1;
                        lru = LRU_FILE;
                        percentage = nr_file * 100 / scan_target;
                }

                /* Stop scanning the smaller of the LRU */
                nr[lru] = 0;
                nr[lru + LRU_ACTIVE] = 0;

                /*
                 * Recalculate the other LRU scan count based on its original
                 * scan target and the percentage scanning already complete
                 */
                lru = (lru == LRU_FILE) ? LRU_BASE : LRU_FILE;
                nr_scanned = targets[lru] - nr[lru];
                nr[lru] = targets[lru] * (100 - percentage) / 100;
                nr[lru] -= min(nr[lru], nr_scanned);

                lru += LRU_ACTIVE;
                nr_scanned = targets[lru] - nr[lru];
                nr[lru] = targets[lru] * (100 - percentage) / 100;
                nr[lru] -= min(nr[lru], nr_scanned);

                scan_adjusted = true;
        }
        blk_finish_plug(&plug);
        sc->nr_reclaimed += nr_reclaimed;

        /*
         * Even if we did not try to evict anon pages at all, we want to
         * rebalance the anon lru active/inactive ratio.
         */
        if (inactive_anon_is_low(lruvec))
                shrink_active_list(SWAP_CLUSTER_MAX, lruvec,
                                   sc, LRU_ACTIVE_ANON);

        throttle_vm_writeout(sc->gfp_mask);
}
  • nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE]; nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON];
    • 회수한 페이지가 목표치를 초과 달성한 경우 scan 비율을 조절하기 위해 먼저 스캔 후 남은 file 페이지 수와 anon 페이지 수를 준비한다.
  • if (!nr_file || !nr_anon) break;
    • 처리 후 남은 file 또는 anon 페이지가 없는 경우 비율을 조절할 필요가 없으므로 루프를 빠져나간다.
  • if (nr_file > nr_anon) { unsigned long scan_target = targets[LRU_INACTIVE_ANON] + targets[LRU_ACTIVE_ANON] + 1; lru = LRU_BASE; percentage = nr_anon * 100 / scan_target;
    • 스캔 후 남은 잔량이 file이 많은 경우 스캔 목표 대비 남은 anon 페이지의 백뷴율을 산출한다.
      • 예) shrink  전 산출하여 백업해둔 anon=200, shrink 후 anon=140
        • scan_target=201, percentage=약 70%의 anon 페이지를 스캔하지 못함
  • } else { unsigned long scan_target = targets[LRU_INACTIVE_FILE] + targets[LRU_ACTIVE_FILE] + 1; lru = LRU_FILE; percentage = nr_file * 100 / scan_target; }
    • 스캔 후 남은 잔량이 anon이 많은 경우 스캔 목표 대비 남은 file 페이지의 백뷴율을 산출한다.
      • 예) shrink  전 산출하여 백업해둔 file=200, shrink 후 file=140
        • scan_target=201, percentage=약 70%의 file 페이지를 스캔하지 못함
  • nr[lru] = 0; nr[lru + LRU_ACTIVE] = 0;
    • 대상 lru는 많이 처리되었기 때문에 다음에 스캔하지 않도록 inactive와 active 스캔 카운트를 0으로 설정한다.
  • lru = (lru == LRU_FILE) ? LRU_BASE : LRU_FILE; nr_scanned = targets[lru] – nr[lru]; nr[lru] = targets[lru] * (100 – percentage) / 100; nr[lru] -= min(nr[lru], nr_scanned);
    • 대상 lru의 반대(file <-> anon) inactive를 선택하고 스캔 목표에서 원래 대상 lru가 스캔한 백분율만큼의 페이지를 감소 시킨 페이지 수를 nr[]에 산출한다.
      • 감소 시킬 때 원래 대상 lru가 스캔한 페이지 수를 초과하지 않도록 조정한다.
  • lru += LRU_ACTIVE; nr_scanned = targets[lru] – nr[lru]; nr[lru] = targets[lru] * (100 – percentage) / 100; nr[lru] -= min(nr[lru], nr_scanned);
    • active anon 또는 file lru를 선택하고 스캔 목표에서 원래 대상 lru가 스캔한 백분율만큼의 페이지를 감소 시킨 페이지 수를 nr[]에 산출한다.
      • 감소 시킬 때 원래 대상 lru가 스캔한 페이지 수를 초과하지 않도록 조정한다.
  • scan_adjusted = true;
    • 스캔 값이 조절된 후에는 루프내에서 다시 재조정되지 않도록 한다.
  • sc->nr_reclaimed += nr_reclaimed;
    • 회수된 페이지 수를 갱신한다.
  • if (inactive_anon_is_low(lruvec)) shrink_active_list(SWAP_CLUSTER_MAX, lruvec, sc, LRU_ACTIVE_ANON);
    • inactive anon이 active anon보다 작을 경우 active 리스트에 대해 shrink를 수행하여 active와 inactive간의 밸런스를 다시 잡아준다.
  • throttle_vm_writeout(sc->gfp_mask);
    • 백그라운드 writeback과 dirty에 스로틀링이 필요 시 100ms 이내로 sleep한다.

 

다음 그림은 lru 벡터 리스트를 shrink하는 모습을 보여준다.

shrink_lruvec-1a

 

get_scan_count()

mm/vmscan.c

/*
 * Determine how aggressively the anon and file LRU lists should be
 * scanned.  The relative value of each set of LRU lists is determined
 * by looking at the fraction of the pages scanned we did rotate back
 * onto the active list instead of evict.
 *
 * nr[0] = anon inactive pages to scan; nr[1] = anon active pages to scan
 * nr[2] = file inactive pages to scan; nr[3] = file active pages to scan
 */
static void get_scan_count(struct lruvec *lruvec, int swappiness,
                           struct scan_control *sc, unsigned long *nr,
                           unsigned long *lru_pages)
{
        struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
        u64 fraction[2];
        u64 denominator = 0;    /* gcc */
        struct zone *zone = lruvec_zone(lruvec);
        unsigned long anon_prio, file_prio;
        enum scan_balance scan_balance;
        unsigned long anon, file;
        bool force_scan = false;
        unsigned long ap, fp;
        enum lru_list lru;
        bool some_scanned;
        int pass;

        /*
         * If the zone or memcg is small, nr[l] can be 0.  This
         * results in no scanning on this priority and a potential
         * priority drop.  Global direct reclaim can go to the next
         * zone and tends to have no problems. Global kswapd is for
         * zone balancing and it needs to scan a minimum amount. When
         * reclaiming for a memcg, a priority drop can cause high
         * latencies, so it's better to scan a minimum amount there as
         * well.
         */
        if (current_is_kswapd()) {
                if (!zone_reclaimable(zone))
                        force_scan = true;
                if (!mem_cgroup_lruvec_online(lruvec))
                        force_scan = true;
        }
        if (!global_reclaim(sc))
                force_scan = true;

        /* If we have no swap space, do not bother scanning anon pages. */
        if (!sc->may_swap || (get_nr_swap_pages() <= 0)) {
                scan_balance = SCAN_FILE;
                goto out;
        }

        /*
         * Global reclaim will swap to prevent OOM even with no
         * swappiness, but memcg users want to use this knob to
         * disable swapping for individual groups completely when
         * using the memory controller's swap limit feature would be
         * too expensive.
         */
        if (!global_reclaim(sc) && !swappiness) {
                scan_balance = SCAN_FILE;
                goto out;
        }

anon & file lru 리스트를 어떻게 스캔해야 하는를 결정한다.

  • lru리스트 셋의 각 상대 값은 eviction 대신 active list로 다시 rotate back을 수행해야 하는 페이지의 비율을 찾는 것에 의해 결정된다.

 

  • if (current_is_kswapd()) {
    • 현재 태스크가 kswapd 인 경우
  • if (!zone_reclaimable(zone)) force_scan = true;
    • zone이 reclaimable하지 않아도 조금이라도 스캔을 할 수 있도록 force_scan을 설정한다.
  • if (!mem_cgroup_lruvec_online(lruvec)) force_scan = true;
    • memcg lruvec가 online이 아닌 경우 스캔을 할 수 있도록 froce_scan을 설정한다.
  • if (!global_reclaim(sc)) force_scan = true;
    • global reclaim이 아닌 경우 스캔을 할 수 있도록 froce_scan을 설정한다.
  • if (!sc->may_swap || (get_nr_swap_pages() <= 0)) { scan_balance = SCAN_FILE; goto out; }
    • swap이 필요 없거나 swap space가 없는 경우 anon 페이지 스캔을 하지 않도록 scan_balance에 SCAN_FILE을 대입하고 out 레이블로 이동한다.

 

        /*
         * Do not apply any pressure balancing cleverness when the
         * system is close to OOM, scan both anon and file equally
         * (unless the swappiness setting disagrees with swapping).
         */
        if (!sc->priority && swappiness) {
                scan_balance = SCAN_EQUAL;
                goto out;
        }

        /*
         * Prevent the reclaimer from falling into the cache trap: as
         * cache pages start out inactive, every cache fault will tip
         * the scan balance towards the file LRU.  And as the file LRU
         * shrinks, so does the window for rotation from references.
         * This means we have a runaway feedback loop where a tiny
         * thrashing file LRU becomes infinitely more attractive than
         * anon pages.  Try to detect this based on file LRU size.
         */
        if (global_reclaim(sc)) {
                unsigned long zonefile;
                unsigned long zonefree;

                zonefree = zone_page_state(zone, NR_FREE_PAGES);
                zonefile = zone_page_state(zone, NR_ACTIVE_FILE) +
                           zone_page_state(zone, NR_INACTIVE_FILE);

                if (unlikely(zonefile + zonefree <= high_wmark_pages(zone))) {
                        scan_balance = SCAN_ANON;
                        goto out;
                }
        }

        /*
         * There is enough inactive page cache, do not reclaim
         * anything from the anonymous working set right now.
         */
        if (!inactive_file_is_low(lruvec)) {
                scan_balance = SCAN_FILE;
                goto out;
        }

        scan_balance = SCAN_FRACT;

        /*
         * With swappiness at 100, anonymous and file have the same priority.
         * This scanning priority is essentially the inverse of IO cost.
         */
        anon_prio = swappiness;
        file_prio = 200 - anon_prio;
  • if (!sc->priority && swappiness) { scan_balance = SCAN_EQUAL; goto out; }
    • swappiness가 설정되었고 최고 우선순위인 경우 최대한 남은 페이지들을 스캔할 수 있도록 scan_balance에 SCAN_EQUAL를 대입하여 out 레이블로 이동한다.
      • 시스템이 OOM에 가까와진 경우 밸런싱 압박을 주지 않도록 한다.
  • if (global_reclaim(sc)) {
    • global reclaim인 경우
  • zonefree = zone_page_state(zone, NR_FREE_PAGES);
    • free 페이지 수
  • zonefile = zone_page_state(zone, NR_ACTIVE_FILE) + zone_page_state(zone, NR_INACTIVE_FILE);
    • file 페이지 수
  • if (unlikely(zonefile + zonefree <= high_wmark_pages(zone))) { scan_balance = SCAN_ANON; goto out; }
    • 작은 확률로 zonefile + zonefree가 high 워터마크보다 작은 경우 scan_balance에 SCAN_ANON을 대입하고 out 레이블로 이동한다.
  • if (!inactive_file_is_low(lruvec)) { scan_balance = SCAN_FILE; goto out; }
    • inactive file 페이지 수가 active file 페이지 수보다 크면 scan_balance에 SCAN_FILE을 대입하고 out 레이블로 이동한다.
  • scan_balance = SCAN_FRACT;
    • 밸런스를 위해 비율을 계산하기 위해 scan_balance에 SCAN_FRACT를 대입한다.
  • anon_prio = swappiness; file_prio = 200 – anon_prio;
    • swappiness가 100일 경우 anon_prio와 file_prio가 동일하다.
    • 이 값은 다음 fs를 통해서 바꿀 수 있다.
    • swappiness가 0일 때 문제가되는 경우도 있으니 주의해야 한다.

 

다음 그림은 swapness가 변화되는 값을 보여준다.

swapness-1

 

        /*
         * OK, so we have swap space and a fair amount of page cache
         * pages.  We use the recently rotated / recently scanned
         * ratios to determine how valuable each cache is.
         *
         * Because workloads change over time (and to avoid overflow)
         * we keep these statistics as a floating average, which ends
         * up weighing recent references more than old ones.
         *
         * anon in [0], file in [1]
         */

        anon  = get_lru_size(lruvec, LRU_ACTIVE_ANON) +
                get_lru_size(lruvec, LRU_INACTIVE_ANON);
        file  = get_lru_size(lruvec, LRU_ACTIVE_FILE) +
                get_lru_size(lruvec, LRU_INACTIVE_FILE);

        spin_lock_irq(&zone->lru_lock);
        if (unlikely(reclaim_stat->recent_scanned[0] > anon / 4)) {
                reclaim_stat->recent_scanned[0] /= 2;
                reclaim_stat->recent_rotated[0] /= 2;
        }

        if (unlikely(reclaim_stat->recent_scanned[1] > file / 4)) {
                reclaim_stat->recent_scanned[1] /= 2;
                reclaim_stat->recent_rotated[1] /= 2;
        }

        /*
         * The amount of pressure on anon vs file pages is inversely
         * proportional to the fraction of recently scanned pages on
         * each list that were recently referenced and in active use.
         */
        ap = anon_prio * (reclaim_stat->recent_scanned[0] + 1);
        ap /= reclaim_stat->recent_rotated[0] + 1;

        fp = file_prio * (reclaim_stat->recent_scanned[1] + 1);
        fp /= reclaim_stat->recent_rotated[1] + 1;
        spin_unlock_irq(&zone->lru_lock);

        fraction[0] = ap;
        fraction[1] = fp;
        denominator = ap + fp + 1; 
  • anon = get_lru_size(lruvec, LRU_ACTIVE_ANON) + get_lru_size(lruvec, LRU_INACTIVE_ANON);
    • anon 페이지 수
  • file = get_lru_size(lruvec, LRU_ACTIVE_FILE) + get_lru_size(lruvec, LRU_INACTIVE_FILE);
    • file 페이지 수
  • if (unlikely(reclaim_stat->recent_scanned[0] > anon / 4)) { reclaim_stat->recent_scanned[0] /= 2; reclaim_stat->recent_rotated[0] /= 2; }
    • 작은 확률로 최근 anon scan 페이지 수가 anon의 25%보다 큰 경우 최근 anon scan 페이지 수와 최근 anon rotate 수를 절반으로 줄인다.
  • if (unlikely(reclaim_stat->recent_scanned[1] > file / 4)) { reclaim_stat->recent_scanned[1] /= 2; reclaim_stat->recent_rotated[1] /= 2; }
    • 작은 확률로 최근 file scan 페이지 수가 file의 25%보다 큰 경우 최근 file scan 페이지 수와 최근 file rotate 수를 절반으로 줄인다
  • ap = anon_prio * (reclaim_stat->recent_scanned[0] + 1); ap /= reclaim_stat->recent_rotated[0] + 1;
    • ap = anon_prio * ((최근 anon 스캔 페이지 수+1) / (최근 anon rotate 페이지 수+1))
  • fp = file_prio * (reclaim_stat->recent_scanned[1] + 1); fp /= reclaim_stat->recent_rotated[1] + 1;
    • fp = file_prio * ((최근 file 스캔 페이지 수+1) / (최근 file rotate 페이지 수+1))
  • fraction[0] = ap; fraction[1] = fp; denominator = ap + fp + 1;
  • SCAN_FRACT에서 scan 페이지 수를 비율로 산출하기 위해 fraction[]과 denominator를 준비한다.

 

out:
        some_scanned = false;
        /* Only use force_scan on second pass. */
        for (pass = 0; !some_scanned && pass < 2; pass++) {
                *lru_pages = 0;
                for_each_evictable_lru(lru) {
                        int file = is_file_lru(lru);
                        unsigned long size;
                        unsigned long scan;

                        size = get_lru_size(lruvec, lru);
                        scan = size >> sc->priority;

                        if (!scan && pass && force_scan)
                                scan = min(size, SWAP_CLUSTER_MAX);

                        switch (scan_balance) {
                        case SCAN_EQUAL:
                                /* Scan lists relative to size */
                                break;
                        case SCAN_FRACT:
                                /*
                                 * Scan types proportional to swappiness and
                                 * their relative recent reclaim efficiency.
                                 */
                                scan = div64_u64(scan * fraction[file],
                                                        denominator);
                                break;
                        case SCAN_FILE:
                        case SCAN_ANON:
                                /* Scan one type exclusively */
                                if ((scan_balance == SCAN_FILE) != file) {
                                        size = 0;
                                        scan = 0;
                                }
                                break;
                        default:
                                /* Look ma, no brain */
                                BUG();
                        }

                        *lru_pages += size;
                        nr[lru] = scan;

                        /*
                         * Skip the second pass and don't force_scan,
                         * if we found something to scan.
                         */
                        some_scanned |= !!scan;
                }
        }
}
  • for (pass = 0; !some_scanned && pass < 2; pass++) {
    • 최대 2 번의 루프를 돈다.
  • for_each_evictable_lru(lru) {
    • evictable한 lru index (0~3)
  • int file = is_file_lru(lru);
    • file lru 여부로 LRU_INACTIVE_FILE과 LRU_ACTIVE_ANON의 경우 true
  • size = get_lru_size(lruvec, lru); scan = size >> sc->priority;
    • 해당 lruvec 페이지 수와 scan할 페이지 수를 2^priority 만큼 나눈다.
  • if (!scan && pass && force_scan) scan = min(size, SWAP_CLUSTER_MAX);
    • scan이 0이면서 pass와 force_scan이 설정된 경우 최소 size를 SWAP_CLUSTER_MAX(32)로 조절한다.
  • switch (scan_balance) { case SCAN_EQUAL: break;
    • 정해진 scan 페이지 수와 횟수를 그대로 이용한다.
  • case SCAN_FRACT: scan = div64_u64(scan * fraction[file], denominator); break;
    • 비율을 적용하여 scan 페이지 수와 횟수를 산출하여 이용한다.
  • case SCAN_FILE: case SCAN_ANON: if ((scan_balance == SCAN_FILE) != file) { size = 0; scan = 0; } break;
    • SCAN_FILE이면서 lruvec이 FILE 속성이 아니면 size와 scan을 0으로 한다.
    • SCAN_ANON이면서 lruvec이 ANON 속성이 아니면 size와 scan을 0으로 한다.
    • SCAN_FILE 및 SCAN_ANON 이면서 lruvec 또한 속성이 같은 경우 size와 scan을 그대로 이용한다.
  • *lru_pages += size; nr[lru] = scan;
    • 출력 인수 lru_pages에 size를 추가하고, 출력 인수 nr[]에 scan 횟수를 추가한다.
  • some_scanned |= !!scan;
    • scan이 0인 경우 재시도를 한다.

 

다음 그림은 get_scan_coun() 함수를 통해 스캔 모드와 SCAN_FRACT에서의 스캔 카운터 nr[]을 산출하는 모습을 보여준다.

get_scan_count-1

 

shrink_list()

mm/vmscan.c

static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
                                 struct lruvec *lruvec, struct scan_control *sc)
{
        if (is_active_lru(lru)) {
                if (inactive_list_is_low(lruvec, lru))
                        shrink_active_list(nr_to_scan, lruvec, sc, lru);
                return 0;
        }

        return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
}

shrink를 하되 처리할 lru가 active 리스트이면서 inactive 리스트보다 작은 경우 처리를 하지 않는다.

  • active 리스트에 대한 shrink 요청 시 inactive 리스트보다 페이지 수가 적으면 active 리스트에 대해 shrink를 수행하지 않는다.
  • inactive 리스트에 대한 shrink 요청은 조건 없이 수행한다.

 

shrink_active_list()

mm/vmscan.c

static void shrink_active_list(unsigned long nr_to_scan,
                               struct lruvec *lruvec,
                               struct scan_control *sc,
                               enum lru_list lru)
{
        unsigned long nr_taken;
        unsigned long nr_scanned;
        unsigned long vm_flags;
        LIST_HEAD(l_hold);      /* The pages which were snipped off */
        LIST_HEAD(l_active);
        LIST_HEAD(l_inactive);
        struct page *page;
        struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
        unsigned long nr_rotated = 0;
        isolate_mode_t isolate_mode = 0;
        int file = is_file_lru(lru);
        struct zone *zone = lruvec_zone(lruvec);

        lru_add_drain();

        if (!sc->may_unmap)
                isolate_mode |= ISOLATE_UNMAPPED;
        if (!sc->may_writepage)
                isolate_mode |= ISOLATE_CLEAN;

        spin_lock_irq(&zone->lru_lock);

        nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
                                     &nr_scanned, sc, isolate_mode, lru);
        if (global_reclaim(sc))
                __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned);

        reclaim_stat->recent_scanned[file] += nr_taken;

        __count_zone_vm_events(PGREFILL, zone, nr_scanned);
        __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
        __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);
        spin_unlock_irq(&zone->lru_lock);

active lru 리스트에서 일정 분량의 페이지를 isolation한 후 user 태스크가 만든 file 캐시 페이지인 경우 다시 active lru 리스트로 rotate 시키고, 나머지는 inactive lru 리스트로 옮긴다. 단 이들 중 unevictable 페이지는 unevictable lru 리스트로 옮긴다. 그리고 처리하는 동안 사용자가 없어진 페이지들은 버디 시스템에 free 한다.

 

  •  lru_add_drain()
    • per cpu lru들을 비우고 lruvec으로 가져온다.
  • if (!sc->may_unmap) isolate_mode |= ISOLATE_UNMAPPED;
    • may_unmap 요청이 없는 경우 unmapped 페이지만 isolation을 시도하게 모드에 추가한다.
  • if (!sc->may_writepage) isolate_mode |= ISOLATE_CLEAN;
    • may_writepage 요청이 없는 경우  clean 페이지만 isolation하도록 모드에 추가한다.
  • nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold, &nr_scanned, sc, isolate_mode, lru);
    • 지정한 lru 벡터 리스트로부터 nr_to_scan 만큼 스캔을 시도하여 분리된 페이지는 l_hold 리스트에 담고 분리된 페이지 수를 반환한다.
  • if (global_reclaim(sc)) __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned)
    • global reclaim인 경우 NR_PAGES_SCANNED에 nr_scanned를 추가한다.
  • reclaim_stat->recent_scanned[file] += nr_taken;
    • recent_scanned[]에 nr_taken을 추가한다.
  • __count_zone_vm_events(PGREFILL, zone, nr_scanned);
    • zone의 PGREFILL stat에 nr_scanned를 더한다.
  • __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
    • zone의 lru 벡터 건수에 nr_taken을 뺀다.
  • __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);
    • zone의 NR_ISOLATED_ANON 또는 NR_ISOLATED_FILE에 nr_taken을 추가한다.

 

        while (!list_empty(&l_hold)) {
                cond_resched();
                page = lru_to_page(&l_hold);
                list_del(&page->lru);

                if (unlikely(!page_evictable(page))) {
                        putback_lru_page(page);
                        continue;
                }

                if (unlikely(buffer_heads_over_limit)) {
                        if (page_has_private(page) && trylock_page(page)) {
                                if (page_has_private(page))
                                        try_to_release_page(page, 0);
                                unlock_page(page);
                        }
                }

                if (page_referenced(page, 0, sc->target_mem_cgroup,
                                    &vm_flags)) {
                        nr_rotated += hpage_nr_pages(page);
                        /*
                         * Identify referenced, file-backed active pages and
                         * give them one more trip around the active list. So
                         * that executable code get better chances to stay in
                         * memory under moderate memory pressure.  Anon pages
                         * are not likely to be evicted by use-once streaming
                         * IO, plus JVM can create lots of anon VM_EXEC pages,
                         * so we ignore them here.
                         */
                        if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) {
                                list_add(&page->lru, &l_active);
                                continue;
                        }
                }

                ClearPageActive(page);  /* we are de-activating */
                list_add(&page->lru, &l_inactive);
        }

        /*
         * Move pages back to the lru list.
         */
        spin_lock_irq(&zone->lru_lock);
        /*
         * Count referenced pages from currently used mappings as rotated,
         * even though only some of them are actually re-activated.  This
         * helps balance scan pressure between file and anonymous pages in
         * get_scan_count.
         */
        reclaim_stat->recent_rotated[file] += nr_rotated;

        move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
        move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE);
        __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
        spin_unlock_irq(&zone->lru_lock);

        mem_cgroup_uncharge_list(&l_hold);
        free_hot_cold_page_list(&l_hold, true);
}
  • while (!list_empty(&l_hold)) {
    • isolatin한 l_hold 리스트의 엔트리 수만큼 루프를 돈다.
  • page = lru_to_page(&l_hold); list_del(&page->lru);
    • l_hold 리스트에서 페이지를 제거한다.
  • if (unlikely(!page_evictable(page))) { putback_lru_page(page); continue; }
    • 작은 확률로 unevictabe 페이지인 경우 lruvec->lists[LRU_UNEVICTABLE] 리스트로 되돌린다.
  • if (unlikely(buffer_heads_over_limit)) { if (page_has_private(page) && trylock_page(page)) { if (page_has_private(page)) try_to_release_page(page, 0); unlock_page(page); } }
    • 작은 확률로 buffer_heads_over_limit이 설정되었고 private 페이지에서 lock 획득이 성공한 경우 페이지를 release하고 unlock한다.
  • if (page_referenced(page, 0, sc->target_mem_cgroup, &vm_flags)) { nr_rotated += hpage_nr_pages(page);
    • 참조된 페이지인 경우 nr_rotaged에 페이지 수를 더한다.
  • if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) { list_add(&page->lru, &l_active); continue; }
    • VM_EXEC 플래그가 있고 file 캐시 페이지인 경우 l_active로 다시 되돌린다(hot 방향으로 rotate.)
  • ClearPageActive(page); list_add(&page->lru, &l_inactive);
    • 페이지의 active 플래그 비트를 설정하고 l_inactive 리스트의 선두(hot) 방향으로 추가한다.
  •  reclaim_stat->recent_rotated[file] += nr_rotated;
    • 최근 rotated[]에 nr_rotated를 더한다.
  • move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
    • l_active에 모아둔 페이지들을 active lru 벡터에 옮기고 그 와중에 사용자가 없어 free 가능한 페이지들은 l_hold로 옮긴다.
  • move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru – LRU_ACTIVATE);
    • l_inactive에 모아둔 페이지들을 inactive lru 벡터에 옮기고 그 와중에 사용자가 없어 free 가능한 페이지들은 l_hold로 옮긴다.
  • __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
    • NR_ISOLATED_ANON 또는 NR_ISOLATED_FILE stat에서 nr_taken을 뺀다.
  • mem_cgroup_uncharge_list(&l_hold);
    • memcg에 l_hold 리스트를 uncarge 보고한다.
  • free_hot_cold_page_list(&l_hold, true);
    • l_hold에 있는 페이지들 모두를 버디시스템의 cold 방향으로 free 한다.

 

다음 그림은 lru active 벡터 리스트를 shrink하는 모습을 보여준다.

shrink_active_list-1a

 

shrink_inactive_list()

mm/vmscan.c

/*
 * shrink_inactive_list() is a helper for shrink_zone().  It returns the number
 * of reclaimed pages
 */
static noinline_for_stack unsigned long
shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
                     struct scan_control *sc, enum lru_list lru)
{
        LIST_HEAD(page_list);
        unsigned long nr_scanned;
        unsigned long nr_reclaimed = 0;
        unsigned long nr_taken;
        unsigned long nr_dirty = 0;
        unsigned long nr_congested = 0;
        unsigned long nr_unqueued_dirty = 0;
        unsigned long nr_writeback = 0;
        unsigned long nr_immediate = 0;
        isolate_mode_t isolate_mode = 0;
        int file = is_file_lru(lru);
        struct zone *zone = lruvec_zone(lruvec);
        struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;

        while (unlikely(too_many_isolated(zone, file, sc))) {
                congestion_wait(BLK_RW_ASYNC, HZ/10);

                /* We are about to die and free our memory. Return now. */
                if (fatal_signal_pending(current))
                        return SWAP_CLUSTER_MAX;
        }

        lru_add_drain();

        if (!sc->may_unmap)
                isolate_mode |= ISOLATE_UNMAPPED;
        if (!sc->may_writepage)
                isolate_mode |= ISOLATE_CLEAN;

        spin_lock_irq(&zone->lru_lock);

        nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,
                                     &nr_scanned, sc, isolate_mode, lru);

        __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
        __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);

        if (global_reclaim(sc)) {
                __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned);
                if (current_is_kswapd())
                        __count_zone_vm_events(PGSCAN_KSWAPD, zone, nr_scanned);
                else
                        __count_zone_vm_events(PGSCAN_DIRECT, zone, nr_scanned);
        }
        spin_unlock_irq(&zone->lru_lock);

        if (nr_taken == 0)
                return 0;

inactive lru 리스트에서 일정 분량의 페이지를 shrink하여 free page를 확보하고, 그 중 일부는 lru 리스트에 되돌리고 writeback 등의 이유로 처리를 유보한 페이지들은 inactive lru 리스트의 선두로 rotate 시킨다.

 

  • lru_add_drain();
    • lru cpu 캐시를 lruvec에 회수한다.
  • if (!sc->may_unmap) isolate_mode |= ISOLATE_UNMAPPED;
    • unmap 허용된 경우 isolate_mode에 ISOLATE_UNMAPPED가 추가된다.
  • if (!sc->may_writepage) isolate_mode |= ISOLATE_CLEAN;
    • writepage가 허용된 경우 isolate_mode에 ISOLATE_CLEAN이 추가된다.
  • nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list, &nr_scanned, sc, isolate_mode, lru);
    • isolate_mode에 맞게 lruvec에서 nr_to_scan 만큼 page_list에 분리해온다.
  • __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
    • lru 카운터를 nr_taken 만큼 감소시킨다.
  • __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);
    • isolated lru 카운터를 nr_taken 만큼 증가시킨다.
  • if (global_reclaim(sc)) { __mod_zone_page_state(zone, NR_PAGES_SCANNED, nr_scanned) }
    • global reclaim인 경우 NR_PAGES_SCANNED 카운터를 nr_scanned만큼 증가시킨다.
  • if (nr_taken == 0) return 0;
    • isolation된 페이지가 없으면 처리를 중단한다.ㅑ

 

        nr_reclaimed = shrink_page_list(&page_list, zone, sc, TTU_UNMAP,
                                &nr_dirty, &nr_unqueued_dirty, &nr_congested,
                                &nr_writeback, &nr_immediate,
                                false);

        spin_lock_irq(&zone->lru_lock);

        reclaim_stat->recent_scanned[file] += nr_taken;

        if (global_reclaim(sc)) {
                if (current_is_kswapd())
                        __count_zone_vm_events(PGSTEAL_KSWAPD, zone,
                                               nr_reclaimed);
                else
                        __count_zone_vm_events(PGSTEAL_DIRECT, zone,
                                               nr_reclaimed);
        }

        putback_inactive_pages(lruvec, &page_list);

        __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);

        spin_unlock_irq(&zone->lru_lock);

        mem_cgroup_uncharge_list(&page_list);
        free_hot_cold_page_list(&page_list, true);

        /*
         * If reclaim is isolating dirty pages under writeback, it implies
         * that the long-lived page allocation rate is exceeding the page
         * laundering rate. Either the global limits are not being effective
         * at throttling processes due to the page distribution throughout
         * zones or there is heavy usage of a slow backing device. The
         * only option is to throttle from reclaim context which is not ideal
         * as there is no guarantee the dirtying process is throttled in the
         * same way balance_dirty_pages() manages.
         *
         * Once a zone is flagged ZONE_WRITEBACK, kswapd will count the number
         * of pages under pages flagged for immediate reclaim and stall if any
         * are encountered in the nr_immediate check below.
         */
        if (nr_writeback && nr_writeback == nr_taken)
                set_bit(ZONE_WRITEBACK, &zone->flags);

  • nr_reclaimed = shrink_page_list(&page_list, zone, sc, TTU_UNMAP, &nr_dirty, &nr_unqueued_dirty, &nr_congested, &nr_writeback, &nr_immediate, false);
    • page_list 만큼 shrink를 수행하고 그 중 회수된 페이지의 수를 알아온다.
  • reclaim_stat->recent_scanned[file] += nr_taken;
    • recent_scanned[] stat에 nr_taken 만큼 증가시킨다.
  • if (global_reclaim(sc)) { if (current_is_kswapd()) __count_zone_vm_events(PGSTEAL_KSWAPD, zone, nr_reclaimed); else __count_zone_vm_events(PGSTEAL_DIRECT, zone, nr_reclaimed); }
    • global reclaim 중이면 현재 태스크가 kswapd인 경우 또는 direct reclaim 중인 경우의 두 가지에 따라 stat을 증가시킨다.
  • putback_inactive_pages(lruvec, &page_list);
    • 남은 page_list에 있는 페이지들을 inactive에 다시 되돌려 놓는다. (hot 방향)
  • __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
    • isolated lru 건수를 nr_taken 만큼 줄인다.
  • mem_cgroup_uncharge_list(&page_list);
    • inactive lru 리스트로 돌아가지 않고 free 페이지에 대해 memcg에 uncharge 보고한다.
  •  free_hot_cold_page_list(&page_list, true);
    • inactive lru 리스트로 돌아가지 않고 page_list에 남아있는 page들을 모두 버디 시스템의 cold 방향으로 free 한다.
  • if (nr_writeback && nr_writeback == nr_taken) set_bit(ZONE_WRITEBACK, &zone->flags);
    • nr_writeback이면서 nr_writeback과 nr_taken이 같은 경우 zone에 WRITEBACK 설정을 한다.

 

        /*
         * memcg will stall in page writeback so only consider forcibly
         * stalling for global reclaim
         */
        if (global_reclaim(sc)) {
                /*
                 * Tag a zone as congested if all the dirty pages scanned were
                 * backed by a congested BDI and wait_iff_congested will stall.
                 */
                if (nr_dirty && nr_dirty == nr_congested)
                        set_bit(ZONE_CONGESTED, &zone->flags);

                /*
                 * If dirty pages are scanned that are not queued for IO, it
                 * implies that flushers are not keeping up. In this case, flag
                 * the zone ZONE_DIRTY and kswapd will start writing pages from
                 * reclaim context.
                 */
                if (nr_unqueued_dirty == nr_taken)
                        set_bit(ZONE_DIRTY, &zone->flags);

                /*
                 * If kswapd scans pages marked marked for immediate
                 * reclaim and under writeback (nr_immediate), it implies
                 * that pages are cycling through the LRU faster than
                 * they are written so also forcibly stall.
                 */
                if (nr_immediate && current_may_throttle())
                        congestion_wait(BLK_RW_ASYNC, HZ/10);
        }

        /*
         * Stall direct reclaim for IO completions if underlying BDIs or zone
         * is congested. Allow kswapd to continue until it starts encountering
         * unqueued dirty pages or cycling through the LRU too quickly.
         */
        if (!sc->hibernation_mode && !current_is_kswapd() &&
            current_may_throttle())
                wait_iff_congested(zone, BLK_RW_ASYNC, HZ/10);

        trace_mm_vmscan_lru_shrink_inactive(zone->zone_pgdat->node_id,
                zone_idx(zone),
                nr_scanned, nr_reclaimed,
                sc->priority,
                trace_shrink_flags(file));
        return nr_reclaimed;
}
  • if (global_reclaim(sc)) {
    • global reclaim인 경우
  • if (nr_dirty && nr_dirty == nr_congested) set_bit(ZONE_CONGESTED, &zone->flags);
    • nr_dirty가 0이 아니면서 nr_congested와 같은 경우 zone에 ZONE_CONGESSTED 플래그 설정을 한다.
  • if (nr_unqueued_dirty == nr_taken) set_bit(ZONE_DIRTY, &zone->flags);
    • nr_unqueued_dirty가 nr_taken과 같은 경우 zone에 ZONE_DIRTY 플래그 설정을 한다.
  • if (nr_immediate && current_may_throttle()) congestion_wait(BLK_RW_ASYNC, HZ/10);
    • nr_immediate가 0이 아니면서 스로틀링을 필요로하는 경우 최대 100ms이내에서 congestion 상태가 끝날 때까지 대기(sleep)한다.
  • if (!sc->hibernation_mode && !current_is_kswapd() && current_may_throttle()) wait_iff_congested(zone, BLK_RW_ASYNC, HZ/10);
    • 절전 모드가 아니고, direct reclaim 중이고 스로틀링을 필요로 하는 경우 최대 100ms이내에서 congestion 상태가 끝날 때까지 대기(sleep)한다.

 

다음 그림은 lru inactive 벡터 리스트를 shrink하는 모습을 보여준다.

shrink_inactive_list-1a

 

 

구조체

scan_control 구조체

struct scan_control {
        /* How many pages shrink_list() should reclaim */
        unsigned long nr_to_reclaim;

        /* This context's GFP mask */
        gfp_t gfp_mask;

        /* Allocation order */
        int order;

        /*
         * Nodemask of nodes allowed by the caller. If NULL, all nodes
         * are scanned.
         */
        nodemask_t      *nodemask;

        /*
         * The memory cgroup that hit its limit and as a result is the
         * primary target of this reclaim invocation.
         */
        struct mem_cgroup *target_mem_cgroup;

        /* Scan (total_size >> priority) pages at once */
        int priority;

        unsigned int may_writepage:1;

        /* Can mapped pages be reclaimed? */
        unsigned int may_unmap:1;

        /* Can pages be swapped as part of reclaim? */
        unsigned int may_swap:1;

        /* Can cgroups be reclaimed below their normal consumption range? */
        unsigned int may_thrash:1;

        unsigned int hibernation_mode:1;

        /* One of the zones is ready for compaction */
        unsigned int compaction_ready:1;

        /* Incremented by the number of inactive pages that were scanned */
        unsigned long nr_scanned;

        /* Number of pages freed so far during a call to shrink_zones() */
        unsigned long nr_reclaimed;
};
  • nr_to_reclaim
    • 회수할  페이지 수
  • gfp_mask
    • GFP mask
  • order
    • 할당 order
  • nodemask
    • 스캔할 노드 마스크 비트맵. null인 경우 모든 노드에서 스캔
  • target_mem_cgroup
    • 페이지 회수 시 사용할 memcg로 한정
  • priority
    • 한 번에 스캔할 페이지 수 (total_size >> priority)
  • may_writepage
    • file 캐시 페이지를 write 시킨 후 회수 가능
  • may_unmap
    • mapped 페이지를 unmap 시킨 후 회수 가능
  • may_swap
    • 스웝을 사용하여 페이지 회수 가능
  • may_thrash
    • cgroup이 보통 소비 범위 이하에서 회수가 가능
  • hibernation_mode
    • 절전모드
  • compaction_ready
    • zone 들 중 하나가 compaction 준비가 된 경우
  • nr_scanned
    • 스캔된 inactive 페이지의 수
  • nr_reclaimed
    • shrink_zones()를 호출 시 회수되어 free된 페이지의 수

 

참고

7 thoughts to “Zoned Allocator -8- (Direct Reclaim-Shrink-1)”

  1. 안녕하세요 좋은 글 감사합니다.
    글을 읽다가 궁금한 점이 생겼는데욥
    만약에 dirty page를 free page로 만들어주기 위해서
    balance_dirty_pages() 함수에서
    storage로 I/O를 발생시켜서 해당 내용을 write해야 하는 경우,
    해당 dirty page의 상태가 clean page로 바뀌어서 free page로 관리가 되려면, storage에서 실제로 그 내용을 써서 I/O completion까지 하고 나서야 가능한 것인가요?
    아니면 혹시 실제 storage에 쓰지 않더라도, block layer에 있는 큐로 I/O 요청을 던지기만 해도(예를 들면) dirty page의 상태가 clean으로 변하게 되는 것인지 궁금합니다

  2. 안녕하세요?
    write-back이 완료될 때 까지 Dirty 상태가 유지됩니다.
    따라서 블럭 레이어의 큐가 아니라 완전히 파일시스템에 기록이 완료되면 write-back 플래그가 클리어되고, 그 후 Dirty 플래그도 클리어됩니다.
    감사합니다.

  3. 아.. 제가 잘못 대답했습니다.
    pageout() 함수를 보니까 dirty 플래그를 지운 후에 I/O 요청을 하네요. ^^;

    1. 답변 감사합니다.
      pageout함수내에 clear_page_dirty_for_io()가 호출이 되네요.
      그렇다면 좀 의문이 드는 점이 있는데요, free page를 만들기 위해 dirty page를 pageout()을 하는 경우(file cache에 있던 dirty page),
      아직 storage와 dirty page가 서로 동기화가 되지 않았는데도 불구하고 이미 free page가 되어
      free page로서 관리가 된다는 건가요?

      1. lru 리스트내에서 페이지 캐시가 완전히 클린이 된 상태에서만 버디에 되돌립니다.
        writeback 작업을 비동기로 요청만한 후 writeback 및 reclaim 플래그를 설정한 채로 계속 lru에 둡니다.
        다시 한 번 lru inactive 리스트를 돌면서 언젠가 자기 페이지 턴이 왔을 때마다 체크를 합니다.
        이 때 기록이 완료되어 writeback이 없어진 클린 상태가 되면 reclaim 플래그도 제거하고 버디 시스템으로 페이지를 보내 페이지를 회수 합니다.

        다음은 이해를 돕기 위한 스텝별 플래그입니다. (lock 플래그는 표현에서 제외)
        lru,dirty 상태-> 회수시작(lru,reclaim) -> 기록중(lru,reclaim,writeback) -> 기록완료(lru,reclaim) -> 클린(lru) -> 버디회수(buddy)

댓글 남기기

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