<kernel v5.0>
Zoned Allocator -11- (Direct Reclaim)
Reclaim 판단
should_continue_reclaim()
mm/vmscan.c
2 | * Reclaim/compaction is used for high-order allocation requests. It reclaims |
3 | * order-0 pages before compacting the zone. should_continue_reclaim() returns |
4 | * true if more pages should be reclaimed such that when the page allocator |
5 | * calls try_to_compact_zone() that it will have enough free pages to succeed. |
6 | * It will give up earlier than that if there is difficulty reclaiming pages. |
01 | static inline bool should_continue_reclaim( struct pglist_data *pgdat, |
02 | unsigned long nr_reclaimed, |
03 | unsigned long nr_scanned, |
04 | struct scan_control *sc) |
06 | unsigned long pages_for_compaction; |
07 | unsigned long inactive_lru_pages; |
11 | if (!in_reclaim_compaction(sc)) |
15 | if (sc->gfp_mask & __GFP_RETRY_MAYFAIL) { |
22 | if (!nr_reclaimed && !nr_scanned) |
41 | pages_for_compaction = compact_gap(sc->order); |
42 | inactive_lru_pages = node_page_state(pgdat, NR_INACTIVE_FILE); |
43 | if (get_nr_swap_pages() > 0) |
44 | inactive_lru_pages += node_page_state(pgdat, NR_INACTIVE_ANON); |
45 | if (sc->nr_reclaimed < pages_for_compaction && |
46 | inactive_lru_pages > pages_for_compaction) |
50 | for (z = 0; z <= sc->reclaim_idx; z++) { |
51 | struct zone *zone = &pgdat->node_zones[z]; |
52 | if (!managed_zone(zone)) |
55 | switch (compaction_suitable(zone, sc->order, 0, sc->reclaim_idx)) { |
57 | case COMPACT_CONTINUE: |
high order 페이지 요청을 처리하는데 reclaim/compaction이 계속되야 하는 경우 true를 반환한다.
- 코드 라인 11~12에서 reclaim/compaction 모드가 아니면 처리를 중단한다.
- 코드 라인 15~35에서 __GFP_RETRY_MAYFAIL 플래그가 사용된 경우 reclaimed 페이지와 scanned 페이지가 없는 경우 false를 반환한다. 플래그가 사용되지 않은 경우 reclaimed 페이지가 없는 경우 false를 반환한다.
- 코드 라인 41~47에서 reclaimed 페이지가 order 페이지의 두 배보다 작아 compaction을 위해 작지만 inactive lru 페이지 수가 order 페이지의 두 배보다는 커 충분한 경우 true를 반환한다.
- 코드 라인 50~64에서 reclaim_idx만큼 존을 순회하며 compaction이 이미 성공하였거나 계속해야 하는 경우 false를 반환한다.
- 코드 라인 64에서 순회한 모든 존에서 compaction의 성공이 없는 경우 true를 반환하여 compaction이 계속되어야 함을 알린다.
in_reclaim_compaction()
mm/vmscan.c
02 | static bool in_reclaim_compaction( struct scan_control *sc) |
04 | if (IS_ENABLED(CONFIG_COMPACTION) && sc->order && |
05 | (sc->order > PAGE_ALLOC_COSTLY_ORDER || |
06 | sc->priority < DEF_PRIORITY - 2)) |
reclaim/compaction 모드인 경우 true를 반환한다.
- 0 order 요청을 제외하고 다음 두 조건을 만족하면 true를 반환한다.
- 우선 순위를 2번 이상 높여 반복 수행 중이다. (낮은 priority 번호가 높은 우선 순위)
- costly order 요청이다.(order 4부터)
Direct-Reclaim 수행
__alloc_pages_direct_reclaim()
mm/page_alloc.c
02 | static inline struct page * |
03 | __alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order, |
04 | int alloc_flags, const struct alloc_context *ac, |
05 | unsigned long *did_some_progress) |
07 | struct page *page = NULL; |
10 | *did_some_progress = __perform_reclaim(gfp_mask, order, ac); |
11 | if (unlikely(!(*did_some_progress))) |
15 | page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac); |
21 | if (!page && !drained) { |
22 | unreserve_highatomic_pageblock(ac, false ); |
23 | drain_all_pages(NULL); |
페이지를 회수한 후 페이지 할당을 시도한다. 만일 처음 실패하는 경우 pcp 캐시를 비워 버디 시스템에 free 페이지를 확보한 후 재시도를 한다.
- 코드 라인 10~12에서 페이지를 회수하며 작은 확률로 회수한 페이지가 없는 경우 null을 반환한다.
- 코드 라인 14~15에서 retry: 레이블에서 order 페이지 할당을 시도한다.
- 코드 라인 21~26에서 페이지 할당이 실패하였고 첫 실패인 경우 highatomic 페이지 블럭을 해제하고, pcp 캐시를 비워 버디시스템에 free 페이지를 확보한 후 재시도 한다.
다음 그림은 direct reclaim을 통해 페이지를 회수하는 과정을 보여준다.
__perform_reclaim()
mm/page_alloc.c
03 | __perform_reclaim(gfp_t gfp_mask, unsigned int order, |
04 | const struct alloc_context *ac) |
06 | struct reclaim_state reclaim_state; |
08 | unsigned int noreclaim_flag; |
14 | cpuset_memory_pressure_bump(); |
15 | psi_memstall_enter(&pflags); |
16 | fs_reclaim_acquire(gfp_mask); |
17 | noreclaim_flag = memalloc_noreclaim_save(); |
18 | reclaim_state.reclaimed_slab = 0; |
19 | current->reclaim_state = &reclaim_state; |
21 | progress = try_to_free_pages(ac->zonelist, order, gfp_mask, |
24 | current->reclaim_state = NULL; |
25 | memalloc_noreclaim_restore(noreclaim_flag); |
26 | fs_reclaim_release(gfp_mask); |
27 | psi_memstall_leave(&pflags); |
페이지를 회수한다. 반환되는 값은 회수한 페이지 수이다.
- 코드 라인 14에서 전역 cpuset_memory_pressure_enabled가 설정된 경우 현재 태스크 cpuset의 frequency meter를 업데이트한다.
- 루트 cpuset에 있는 memory_pressure_enabled 파일을 1로 설정하여 사용한다.
- 코드 라인 15에서 메모리 압박이 시작되었음을 psi에 알린다.
- 코드 라인 17에서 페이지 회수를 목적으로 잠시 페이지 할당이 필요하다. 이 때 다시 페이지 회수 루틴이 재귀 호출되지 않도록 방지하기 위해 reclaim을 하는 동안 잠시 현재 태스크의 플래그에 PF_MEMALLOC를 설정하여 워터 마크 기준을 없앤 후 할당할 수 있도록 한다.
- 코드 라인 18~19에서 reclaimed_slab 카운터를 0으로 리셋하고, 현재 태스크에 지정한다.
- 코드 라인 21~22에서 페이지를 회수하고 회수한 페이지 수를 알아온다.
- 코드 라인 24에서 태스크에 지정한 reclaim_state를 해제한다.
- 코드 라인 25에서 현재 태스크의 플래그에 reclaim을 하는 동안 잠시 설정해두었던 PF_MEMALLOC을 제거한다.
- 코드 라인 27에서 메모리 압박이 완료되었음을 psi에 알린다.
Scan Control
스캔 컨트롤을 사용하는 루틴들은 다음과 같다.
- reclaim_clean_pages_from_list()
- try_to_free_pages()
- mem_cgroup_shrink_node()
- try_to_free_mem_cgroup_pages()
- balance_pgdat()
- shrink_all_memory()
- __node_reclaim()
페이지 회수로 free 페이지 확보 시도
try_to_free_pages()
mm/vmscan.c
01 | unsigned long try_to_free_pages( struct zonelist *zonelist, int order, |
02 | gfp_t gfp_mask, nodemask_t *nodemask) |
04 | unsigned long nr_reclaimed; |
05 | struct scan_control sc = { |
06 | .nr_to_reclaim = SWAP_CLUSTER_MAX, |
07 | .gfp_mask = current_gfp_context(gfp_mask), |
08 | .reclaim_idx = gfp_zone(gfp_mask), |
11 | .priority = DEF_PRIORITY, |
12 | .may_writepage = !laptop_mode, |
22 | BUILD_BUG_ON(MAX_ORDER > S8_MAX); |
23 | BUILD_BUG_ON(DEF_PRIORITY > S8_MAX); |
24 | BUILD_BUG_ON(MAX_NR_ZONES > S8_MAX); |
31 | if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask)) |
34 | trace_mm_vmscan_direct_reclaim_begin(order, |
39 | nr_reclaimed = do_try_to_free_pages(zonelist, &sc); |
41 | trace_mm_vmscan_direct_reclaim_end(nr_reclaimed); |
페이지 회수(Reclaim)를 시도하고 회수된 페이지 수를 반환한다. 유저 요청 시 free page가 normal 존 이하에서 min 워터마크 기준의 절반 이상을 확보할 때까지 태스크가 스로틀링(sleep)될 수 있다.
- 코드 라인 5~16에서 페이지 회수를 위한 scan_control 구조체를 준비한다.
- 코드 라인 31~32에서 direct-reclaim을 위해 일정 기준 이상 스로틀링 중 fatal 시그널을 전달 받은 경우 즉각 루틴을 빠져나간다. 단 1을 반환하므로 OOM kill 루틴을 수행하지 못하게 방지한다.
- 코드 라인 39에서 페이지 회수를 시도한다.
유저 요청 시 스로틀링
throttle_direct_reclaim()
mm/vmscan.c
2 | * Throttle direct reclaimers if backing storage is backed by the network |
3 | * and the PFMEMALLOC reserve for the preferred node is getting dangerously |
4 | * depleted. kswapd will continue to make progress and wake the processes |
5 | * when the low watermark is reached. |
7 | * Returns true if a fatal signal was delivered during throttling. If this |
8 | * happens, the page allocator should not consider triggering the OOM killer. |
01 | static bool throttle_direct_reclaim(gfp_t gfp_mask, struct zonelist *zonelist, |
06 | pg_data_t *pgdat = NULL; |
15 | if (current->flags & PF_KTHREAD) |
22 | if (fatal_signal_pending(current)) |
39 | for_each_zone_zonelist_nodemask(zone, z, zonelist, |
40 | gfp_zone(gfp_mask), nodemask) { |
41 | if (zone_idx(zone) > ZONE_NORMAL) |
45 | pgdat = zone->zone_pgdat; |
46 | if (allow_direct_reclaim(pgdat)) |
56 | count_vm_event(PGSCAN_DIRECT_THROTTLE); |
66 | if (!(gfp_mask & __GFP_FS)) { |
67 | wait_event_interruptible_timeout(pgdat->pfmemalloc_wait, |
68 | allow_direct_reclaim(pgdat), HZ); |
74 | wait_event_killable(zone->zone_pgdat->pfmemalloc_wait, |
75 | allow_direct_reclaim(pgdat)); |
78 | if (fatal_signal_pending(current)) |
유저 태스크에서 direct-reclaim 요청 시 필요한 만큼 스로틀링한다. 파일 시스템을 사용하지 않는(nofs) direct-reclaim 요청인 경우 스로틀링은 1초로 제한된다. 스로틀링 중 sigkill 시그널 수신 여부를 반환한다.
- 코드 라인 15~16에서 커널 스레드에서 요청한 경우 스로틀링을 하지 않기 위해 처리를 중단하고 false를 반환한다.
- 코드 라인 22~23에서 SIGKILL 시그널이 처리되고 있는 태스크의 경우도 역시 처리를 중단하고 false를 반환한다.
- 코드 라인 39~49에서 요청한 노드의 lowmem 존들의 direct-reclaim이 허용 기준 이상인 경우 스로틀링을 포기한다.
- 코드 라인 52~53에서 사용할 수 있는 노드가 없는 경우 처리를 포기한다.
- 코드 라인 56에서 스로틀링이 시작되는 구간이다. PGSCAN_DIRECT_THROTTLE stat을 증가시킨다.
- 코드 라인 66~71에서 파일 시스템을 사용하지 않는 direct-reclaim 요청인 경우 direct-reclaim을 허락할 때까지 최대 1초간 스로틀링 후 check_pending 레이블로 이동한다.
- 코드 라인74~75에서 파일 시스템을 사용하는 direct-reclaim의 경우 kswapd를 깨워 free page를 확보하며 direct-reclaim을 허락할 때까지 슬립한다.
- 코드 라인 77~82에서 현재 태스크에 SIGKILL 시그널이 요청된 경우 true를 반환하고 그렇지 않은 경우 false를 반환한다.
다음 그림은 유저 요청 direct-reclaim 시 파일 시스템 사용 여부에 따라 direct-reclaim을 사용하기 위해 스로틀링하는 과정을 보여준다.
direct-reclaim 허락 여부
allow_direct_reclaim()
mm/vmscan.c
01 | static bool allow_direct_reclaim(pg_data_t *pgdat) |
04 | unsigned long pfmemalloc_reserve = 0; |
05 | unsigned long free_pages = 0; |
09 | if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES) |
12 | for (i = 0; i <= ZONE_NORMAL; i++) { |
13 | zone = &pgdat->node_zones[i]; |
14 | if (!managed_zone(zone)) |
17 | if (!zone_reclaimable_pages(zone)) |
20 | pfmemalloc_reserve += min_wmark_pages(zone); |
21 | free_pages += zone_page_state(zone, NR_FREE_PAGES); |
25 | if (!pfmemalloc_reserve) |
28 | wmark_ok = free_pages > pfmemalloc_reserve / 2; |
31 | if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) { |
32 | pgdat->kswapd_classzone_idx = min(pgdat->kswapd_classzone_idx, |
33 | ( enum zone_type)ZONE_NORMAL); |
34 | wake_up_interruptible(&pgdat->kswapd_wait); |
요청한 노드에서 direct-reclaim을 허락하는지 여부를 반환한다. 만일 lowmem 존들의 free 페이지가 min 워터마크 50% 이하인 경우 현재 태스크를 슬립하고, kswapd를 깨운 뒤 false를 반환한다. 그리고 그 이상인 경우 direct-reclaim을 시도해도 좋다고 판단하여 true를 반환한다.
- 코드 라인 9~10에서 reclaim 실패 횟수가 MAX_RECLAIM_RETRIES(16)번 이상일 때 스로틀을 하지못하게 곧바로 true를 반환한다.
- 코드 라인 12~22에서 lowmem 존들의 min 워터마크를 합산한 pfmemalloc_reserve 값 및 free 페이지 수의 합산 값을 구한다.
- 코드 라인 25~26에서 pfmemalloc_reserve 값이 0인 경우 스로틀을 하지 못하게 곧바로 true를 반환한다.
- 코드 라인 28~35에서 free 페이지 합산 수가 lowmem 존들의 min 워터마크 합산 값의 50% 이하이면 현재 태스크를 슬립시키고 kswapd를 깨운 뒤 false를 반환한다.
다음 그림은 direct-reclaim 허락 여부를 알아오는 과정을 보여준다.
do_try_to_free_pages()
mm/vmscan.c
02 | * This is the main entry point to direct page reclaim. |
04 | * If a full scan of the inactive list fails to free enough memory then we |
05 | * are "out of memory" and something needs to be killed. |
07 | * If the caller is !__GFP_FS then the probability of a failure is reasonably |
08 | * high - the zone may be full of dirty or under-writeback pages, which this |
09 | * caller can't do much about. We kick the writeback threads and take explicit |
10 | * naps in the hope that some of these pages can be written. But if the |
11 | * allocating task holds filesystem locks which prevent writeout this might not |
12 | * work, and the allocation attempt will fail. |
14 | * returns: 0, if no pages reclaimed |
15 | * else, the number of pages reclaimed |
01 | static unsigned long do_try_to_free_pages( struct zonelist *zonelist, |
02 | struct scan_control *sc) |
04 | int initial_priority = sc->priority; |
05 | pg_data_t *last_pgdat; |
09 | delayacct_freepages_start(); |
11 | if (global_reclaim(sc)) |
12 | __count_zid_vm_events(ALLOCSTALL, sc->reclaim_idx, 1); |
15 | vmpressure_prio(sc->gfp_mask, sc->target_mem_cgroup, |
18 | shrink_zones(zonelist, sc); |
20 | if (sc->nr_reclaimed >= sc->nr_to_reclaim) |
23 | if (sc->compaction_ready) |
30 | if (sc->priority < DEF_PRIORITY - 2) |
31 | sc->may_writepage = 1; |
32 | } while (--sc->priority >= 0); |
35 | for_each_zone_zonelist_nodemask(zone, z, zonelist, sc->reclaim_idx, |
37 | if (zone->zone_pgdat == last_pgdat) |
39 | last_pgdat = zone->zone_pgdat; |
40 | snapshot_refaults(sc->target_mem_cgroup, zone->zone_pgdat); |
41 | set_memcg_congestion(last_pgdat, sc->target_mem_cgroup, false ); |
44 | delayacct_freepages_end(); |
47 | return sc->nr_reclaimed; |
50 | if (sc->compaction_ready) |
54 | if (sc->memcg_low_skipped) { |
55 | sc->priority = initial_priority; |
56 | sc->memcg_low_reclaim = 1; |
57 | sc->memcg_low_skipped = 0; |
direct-reclaim 요청을 통해 페이지를 회수하여 free 페이지를 확보를 시도한다.
- 코드 라인 8~9에서 retry: 레이블이다. 페이지 회수에 소요되는 시간을 계량하기 위해 시작한다.
- 코드 라인 11~12에서 global reclaim을 사용해야하는 경우 ALLOCSTALL stat을 증가시킨다.
- 코드 라인 14~16에서 루프를 돌며 우선 순위가 높아져 스캔 depth가 깊어지는 경우 vmpressure 정보를 갱신한다.
- 코드 라인 17~18에서 스캔 건 수를 리셋시키고 페이지를 회수하고 회수한 건 수를 알아온다.
- 코드 라인 20~21에서 회수 건 수가 회수해야 할 건 수보다 큰 경우 처리를 위해 루프에서 벗어난다.
- 코드 라인 23~24에서 compaction이 준비된 경우 처리를 위해 루프에서 벗어난다.
- 코드 라인 30~31에서 우선 순위를 2 단계 더 높여 처리하는 경우 writepage 기능을 설정한다.
- 코드 라인 32에서 우선 순위를 최고까지 높여가며(0으로 갈수록 높아진다) 루프를 돈다.
- 코드 라인 34~42에서 zonelist를 순회하며 노드에 대해 노드 또는 memcg lru의 refaults를 갱신하고 memcg 노드의 congested를 false로 리셋한다.
- 코드 라인 44에서 페이지 회수에 소요되는 시간을 계량한다.
- 코드 라인 46~47네거 회수한 적이 있는 경우 그 값을 반환한다.
- 코드 라인 50~51에서 compaction이 준비된 경우 1을 반환한다.
- 코드 라인 54~59에서 sc->memcg_low_skipped가 설정된 경우 처음 재시도에 한해 priority를 다시 원래 요청 priority로 바꾸고 재시도한다.
global_reclaim()
mm/vmscan.c
02 | static bool global_reclaim( struct scan_control *sc) |
04 | return !sc->target_mem_cgroup; |
07 | static bool global_reclaim( struct scan_control *sc) |
CONFIG_MEMCG 커널 옵션을 사용하여 Memory Control Group을 사용하는 경우 scan_control의 target_mem_cgroup이 정해진 경우 false를 반환한다. 그렇지 않은 경우 global reclaim을 위해 true를 반환한다. CONFIG_MEMCG를 사용하지 않는 경우 항상 true이다.
Memory Pressure (per-cpuset reclaims)
cpuset_memory_pressure_bump()
include/linux/cpuset.h
1 | #define cpuset_memory_pressure_bump() \ |
3 | if (cpuset_memory_pressure_enabled) \ |
4 | __cpuset_memory_pressure_bump(); \ |
현재 태스크 cpuset의 frequency meter를 업데이트한다.
__cpuset_memory_pressure_bump()
kernel/cpuset.c
02 | * cpuset_memory_pressure_bump - keep stats of per-cpuset reclaims. |
04 | * Keep a running average of the rate of synchronous (direct) |
05 | * page reclaim efforts initiated by tasks in each cpuset. |
07 | * This represents the rate at which some task in the cpuset |
08 | * ran low on memory on all nodes it was allowed to use, and |
09 | * had to enter the kernels page reclaim code in an effort to |
10 | * create more free memory by tossing clean pages or swapping |
11 | * or writing dirty pages. |
13 | * Display to user space in the per-cpuset read-only file |
14 | * "memory_pressure". Value displayed is an integer |
15 | * representing the recent rate of entry into the synchronous |
16 | * (direct) page reclaim by any task attached to the cpuset. |
1 | void __cpuset_memory_pressure_bump( void ) |
4 | fmeter_markevent(&task_cs(current)->fmeter); |
현재 태스크 cpuset의 frequency meter를 업데이트한다.
fmeter_markevent()
kernel/cpuset.c
2 | static void fmeter_markevent( struct fmeter *fmp) |
6 | fmp->cnt = min(FM_MAXCNT, fmp->cnt + FM_SCALE); |
7 | spin_unlock(&fmp->lock); |
요청한 frequency meter를 업데이트하고 다음 계산을 위해 이벤트 수에 1,000을 대입하되 최대 1,000,000을 넘기지 않게 한다.
fmeter_update()
kernel/cpuset.c
02 | static void fmeter_update( struct fmeter *fmp) |
04 | time_t now = get_seconds(); |
05 | time_t ticks = now - fmp-> time ; |
10 | ticks = min(FM_MAXTICKS, ticks); |
12 | fmp->val = (FM_COEF * fmp->val) / FM_SCALE; |
15 | fmp->val += ((FM_SCALE - FM_COEF) * fmp->cnt) / FM_SCALE; |
요청한 frequency meter로 val 값을 계산하고 이벤트 수를 0으로 리셋한다.
- 코드 라인 4~8에서 fmeter에 기록된 초(second)로부터 경과한 초를 알아온다.
- 코드 라인 10~12에서 ticks는 최대 99까지로 제한하고, ticks 만큼 fmp->val *= 93.3%를 반복한다.
- 코드 라인 13에서 다음 계산을 위해 현재 초로 갱신한다.
- 코드 라인 15~16에서 fmp->val에 fmp->cnt x 6.7%를 더한 후 이벤트 수를 0으로 리셋한다.
fmeter 구조체
kernel/cgroup/cpuset.c
- cnt
- val
- time
- val 값이 계산될 때의 clock(secs)
참고