Zonned 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된 페이지의 수

 

참고

답글 남기기

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