mem_cgroup_calculate_protection() is not stateless and should only be used as part of a top-down tree traversal. shrink_one() traverses the per-node memcg LRU instead of the root_mem_cgroup tree, and therefore it should not call mem_cgroup_calculate_protection(). The existing misuse in shrink_one() can cause ineffective protection of sub-trees that are grandchildren of root_mem_cgroup. Fix it by reusing lru_gen_age_node(), which already traverses the root_mem_cgroup tree, to calculate the protection. Previously lru_gen_age_node() opportunistically skips the first pass, i.e., when scan_control->priority is DEF_PRIORITY. On the second pass, lruvec_is_sizable() uses appropriate scan_control->priority, set by set_initial_priority() from lru_gen_shrink_node(), to decide whether a memcg is too small to reclaim from. Now lru_gen_age_node() unconditionally traverses the root_mem_cgroup tree. So it should call set_initial_priority() upfront, to make sure lruvec_is_sizable() uses appropriate scan_control->priority on the first pass. Otherwise, lruvec_is_reclaimable() can return false negatives and result in premature OOM kills when min_ttl_ms is used.
Unified · Split
| @@ -3915,6 +3915,32 @@ done: · 3914 unmodified lines | ||
| 3915 | 3915 | * working set protection |
| 3916 | 3916 | ******************************************************************************/ |
| 3917 | 3917 | |
| 3918 | +static void set_initial_priority(struct pglist_data *pgdat, struct scan_control *sc) | |
| 3919 | +{ | |
| 3920 | + int priority; | |
| 3921 | + unsigned long reclaimable; | |
| 3922 | + | |
| 3923 | + if (sc->priority != DEF_PRIORITY || sc->nr_to_reclaim < MIN_LRU_BATCH) | |
| 3924 | + return; | |
| 3925 | + /* | |
| 3926 | + * Determine the initial priority based on | |
| 3927 | + * (total >> priority) * reclaimed_to_scanned_ratio = nr_to_reclaim, | |
| 3928 | + * where reclaimed_to_scanned_ratio = inactive / total. | |
| 3929 | + */ | |
| 3930 | + reclaimable = node_page_state(pgdat, NR_INACTIVE_FILE); | |
| 3931 | + if (can_reclaim_anon_pages(NULL, pgdat->node_id, sc)) | |
| 3932 | + reclaimable += node_page_state(pgdat, NR_INACTIVE_ANON); | |
| 3933 | + | |
| 3934 | + /* round down reclaimable and round up sc->nr_to_reclaim */ | |
| 3935 | + priority = fls_long(reclaimable) - 1 - fls_long(sc->nr_to_reclaim - 1); | |
| 3936 | + | |
| 3937 | + /* | |
| 3938 | + * The estimation is based on LRU pages only, so cap it to prevent | |
| 3939 | + * overshoots of shrinker objects by large margins. | |
| 3940 | + */ | |
| 3941 | + sc->priority = clamp(priority, DEF_PRIORITY / 2, DEF_PRIORITY); | |
| 3942 | +} | |
| 3943 | + | |
| 3918 | 3944 | static bool lruvec_is_sizable(struct lruvec *lruvec, struct scan_control *sc) |
| 3919 | 3945 | { |
| 3920 | 3946 | int gen, type, zone; |
| @@ -3948,19 +3974,17 @@ static bool lruvec_is_reclaimable(struct lruvec *lruvec, struct scan_control *sc · 27 unmodified lines | ||
| 3948 | 3974 | struct mem_cgroup *memcg = lruvec_memcg(lruvec); |
| 3949 | 3975 | DEFINE_MIN_SEQ(lruvec); |
| 3950 | 3976 | |
| 3951 | − /* see the comment on lru_gen_folio */ | |
| 3952 | − gen = lru_gen_from_seq(min_seq[LRU_GEN_FILE]); | |
| 3953 | − birth = READ_ONCE(lruvec->lrugen.timestamps[gen]); | |
| 3954 | − | |
| 3955 | − if (time_is_after_jiffies(birth + min_ttl)) | |
| 3977 | + if (mem_cgroup_below_min(NULL, memcg)) | |
| 3956 | 3978 | return false; |
| 3957 | 3979 | |
| 3958 | 3980 | if (!lruvec_is_sizable(lruvec, sc)) |
| 3959 | 3981 | return false; |
| 3960 | 3982 | |
| 3961 | − mem_cgroup_calculate_protection(NULL, memcg); | |
| 3983 | + /* see the comment on lru_gen_folio */ | |
| 3984 | + gen = lru_gen_from_seq(min_seq[LRU_GEN_FILE]); | |
| 3985 | + birth = READ_ONCE(lruvec->lrugen.timestamps[gen]); | |
| 3962 | 3986 | |
| 3963 | − return !mem_cgroup_below_min(NULL, memcg); | |
| 3987 | + return time_is_before_jiffies(birth + min_ttl); | |
| 3964 | 3988 | } |
| 3965 | 3989 | |
| 3966 | 3990 | /* to protect the working set of the last N jiffies */ |
| @@ -3970,23 +3994,20 @@ static void lru_gen_age_node(struct pglist_data *pgdat, struct scan_control *sc) · 3 unmodified lines | ||
| 3970 | 3994 | { |
| 3971 | 3995 | struct mem_cgroup *memcg; |
| 3972 | 3996 | unsigned long min_ttl = READ_ONCE(lru_gen_min_ttl); |
| 3997 | + bool reclaimable = !min_ttl; | |
| 3973 | 3998 | |
| 3974 | 3999 | VM_WARN_ON_ONCE(!current_is_kswapd()); |
| 3975 | 4000 | |
| 3976 | − /* check the order to exclude compaction-induced reclaim */ | |
| 3977 | − if (!min_ttl || sc->order || sc->priority == DEF_PRIORITY) | |
| 3978 | − return; | |
| 4001 | + set_initial_priority(pgdat, sc); | |
| 3979 | 4002 | |
| 3980 | 4003 | memcg = mem_cgroup_iter(NULL, NULL, NULL); |
| 3981 | 4004 | do { |
| 3982 | 4005 | struct lruvec *lruvec = mem_cgroup_lruvec(memcg, pgdat); |
| 3983 | 4006 | |
| 3984 | − if (lruvec_is_reclaimable(lruvec, sc, min_ttl)) { | |
| 3985 | − mem_cgroup_iter_break(NULL, memcg); | |
| 3986 | − return; | |
| 3987 | − } | |
| 4007 | + mem_cgroup_calculate_protection(NULL, memcg); | |
| 3988 | 4008 | |
| 3989 | − cond_resched(); | |
| 4009 | + if (!reclaimable) | |
| 4010 | + reclaimable = lruvec_is_reclaimable(lruvec, sc, min_ttl); | |
| 3990 | 4011 | } while ((memcg = mem_cgroup_iter(NULL, memcg, NULL))); |
| 3991 | 4012 | |
| 3992 | 4013 | /* |
| @@ -3994,7 +4015,7 @@ static void lru_gen_age_node(struct pglist_data *pgdat, struct scan_control *sc) · 1 unmodified line | ||
| 3994 | 4015 | * younger than min_ttl. However, another possibility is all memcgs are |
| 3995 | 4016 | * either too small or below min. |
| 3996 | 4017 | */ |
| 3997 | − if (mutex_trylock(&oom_lock)) { | |
| 4018 | + if (!reclaimable && mutex_trylock(&oom_lock)) { | |
| 3998 | 4019 | struct oom_control oc = { |
| 3999 | 4020 | .gfp_mask = sc->gfp_mask, |
| 4000 | 4021 | }; |
| @@ -4786,8 +4807,7 @@ static int shrink_one(struct lruvec *lruvec, struct scan_control *sc) · 785 unmodified lines | ||
| 4786 | 4807 | struct mem_cgroup *memcg = lruvec_memcg(lruvec); |
| 4787 | 4808 | struct pglist_data *pgdat = lruvec_pgdat(lruvec); |
| 4788 | 4809 | |
| 4789 | − mem_cgroup_calculate_protection(NULL, memcg); | |
| 4790 | − | |
| 4810 | + /* lru_gen_age_node() called mem_cgroup_calculate_protection() */ | |
| 4791 | 4811 | if (mem_cgroup_below_min(NULL, memcg)) |
| 4792 | 4812 | return MEMCG_LRU_YOUNG; |
| 4793 | 4813 | |
| @@ -4911,32 +4931,6 @@ static void lru_gen_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc · 117 unmodified lines | ||
| 4911 | 4931 | blk_finish_plug(&plug); |
| 4912 | 4932 | } |
| 4913 | 4933 | |
| 4914 | −static void set_initial_priority(struct pglist_data *pgdat, struct scan_control *sc) | |
| 4915 | −{ | |
| 4916 | − int priority; | |
| 4917 | − unsigned long reclaimable; | |
| 4918 | − | |
| 4919 | − if (sc->priority != DEF_PRIORITY || sc->nr_to_reclaim < MIN_LRU_BATCH) | |
| 4920 | − return; | |
| 4921 | − /* | |
| 4922 | − * Determine the initial priority based on | |
| 4923 | − * (total >> priority) * reclaimed_to_scanned_ratio = nr_to_reclaim, | |
| 4924 | − * where reclaimed_to_scanned_ratio = inactive / total. | |
| 4925 | − */ | |
| 4926 | − reclaimable = node_page_state(pgdat, NR_INACTIVE_FILE); | |
| 4927 | − if (can_reclaim_anon_pages(NULL, pgdat->node_id, sc)) | |
| 4928 | − reclaimable += node_page_state(pgdat, NR_INACTIVE_ANON); | |
| 4929 | − | |
| 4930 | − /* round down reclaimable and round up sc->nr_to_reclaim */ | |
| 4931 | − priority = fls_long(reclaimable) - 1 - fls_long(sc->nr_to_reclaim - 1); | |
| 4932 | − | |
| 4933 | − /* | |
| 4934 | − * The estimation is based on LRU pages only, so cap it to prevent | |
| 4935 | − * overshoots of shrinker objects by large margins. | |
| 4936 | − */ | |
| 4937 | − sc->priority = clamp(priority, DEF_PRIORITY / 2, DEF_PRIORITY); | |
| 4938 | −} | |
| 4939 | − | |
| 4940 | 4934 | static void lru_gen_shrink_node(struct pglist_data *pgdat, struct scan_control *sc) |
| 4941 | 4935 | { |
| 4942 | 4936 | struct blk_plug plug; |