mm/mglru: fix ineffective memory protection for non-kswapd reclaim
Subsystems (6)
v4 cover mm/mglru: fix ineffective memory protection for non-kswapd reclaim
From: Ridong Chen <[email protected]>
For MGLRU, memory.min/low is not honored during non-kswapd global reclaim (global direct reclaim and root-level memory.reclaim), because these paths shrink memcgs using stale protection (emin/elow).
Patch 2 is the actual fix. Patch 1 is a prerequisite: an integer overflow in effective_protection(), spotted by the sashiko review tool, which patch 2's new caller would also be exposed to.
---
v4:
- Per Barry's and Johannes's review comments.
v3:
- Per Barry Song's review comments
v2:
- Fix an issue caused by non-atomic read races in patch 1 [1]
[1] https://sashiko.dev/#/patchset/[email protected]?part=1
Ridong Chen (2):
mm/page_counter: avoid integer overflow in effective_protection() mm/mglru: fix ineffective memory protection for non-kswapd reclaim
include/linux/memcontrol.h | 10 +++++++++ mm/memcontrol.c | 45 ++++++++++++++++++++++++++++++++++++++ mm/page_counter.c | 21 +++++++++++++----- mm/vmscan.c | 8 ++++++- 4 files changed, 77 insertions(+), 7 deletions(-)
Signature · 1 line
2.34.1
PATCH v4 1/2 mm/page_counter: avoid integer overflow in effective_protection()
From: Ridong Chen <[email protected]>
effective_protection() scales a parent's protection by a ratio of page counts, e.g. for recursive protection:
(parent_effective - siblings_protected) * (usage - protected)
/ (parent_usage - siblings_protected)
The multiply is done at unsigned long width before dividing. On systems with >= 16TB RAM the product can exceed 2^64 and wrap, giving a bogus protection value and silently breaking memory.min/low enforcement.
Use mul_u64_u64_div_u64() to multiply in a 128-bit intermediate. Because usage and parent_usage are not read atomically (a child is charged before its parent), usage - protected can briefly exceed the divisor, making the quotient overflow 64 bits and trap (#DE on x86). Cap it so the ratio stays <= 1.
Reported by the sashiko review tool [1].
[1] https://sashiko.dev/#/patchset/[email protected]?part=1
--- mm/page_counter.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-)
mm/page_counter.c +15 -6
@@ -8,6 +8,7 @@#include <linux/page_counter.h> #include <linux/atomic.h> #include <linux/kernel.h> +#include <linux/math64.h> #include <linux/string.h> #include <linux/sched.h> #include <linux/bug.h>
@@ -356,7 +357,8 @@ static unsigned long effective_protection(unsigned long usage,* otherwise get a smaller chunk than what they claimed. */ if (siblings_protected > parent_effective) -return protected * parent_effective / siblings_protected; +return mul_u64_u64_div_u64 (protected, parent_effective, + siblings_protected);/* * Ok, utilized protection of all children is within what the
@@ -397,13 +399,20 @@ static unsigned long effective_protection(unsigned long usage,if (parent_effective > siblings_protected && parent_usage > siblings_protected && usage > protected) { -unsigned long unclaimed; +unsigned long parent_unclaimed, parent_unprotected, unprotected; - unclaimed = parent_effective - siblings_protected; - unclaimed *= usage - protected; - unclaimed /= parent_usage - siblings_protected; + parent_unclaimed = parent_effective - siblings_protected; + parent_unprotected = parent_usage - siblings_protected; - ep += unclaimed; +/* +* The usages aren't read atomically, so a child can transiently +* appear to use more than its parent, making the ratio exceed 1 +* and the quotient overflow 64 bits (#DE on x86). Cap it. +*/ + unprotected =min (usage - protected, parent_unprotected); + + ep +=mul_u64_u64_div_u64 (parent_unclaimed, unprotected, + parent_unprotected); }return ep;
Signature · 1 line
2.34.1
PATCH v4 2/2 mm/mglru: fix ineffective memory protection for non-kswapd reclaim
In reply to Ridong ChenFrom: Ridong Chen <[email protected]>
For MGLRU, memory.min/low is not honored during global proactive reclaim (writing to the root memory.reclaim) and global direct reclaim, because these paths shrink memcgs using stale protection (emin/elow). It can be reproduced as follows:
# echo 7 > /sys/kernel/mm/lru_gen/enabled # cd /sys/fs/cgroup # mkdir -p a/b # echo 100M > a/memory.min # echo +memory > a/cgroup.subtree_control # echo 100M > a/b/memory.min # echo $$ > a/b/cgroup.procs # dd if=/dev/zero of=/tmp/testfile bs=1M count=200 # cat a/b/memory.current 222650368 # echo 500M > memory.reclaim -bash: echo: write error: Resource temporarily unavailable # cat a/b/memory.current 6070272
memory.min is 100M, yet reclaim drops a/b down to 6M, breaking the protection. The traditional LRU path is not affected because shrink_node() calls mem_cgroup_calculate_protection() for each memcg it visits during a top-down tree walk.
Commit 30d77b7eef01 ("mm/mglru: fix ineffective protection calculation") moved the protection computation into lru_gen_age_node(), which only runs for kswapd. Non-kswapd global reclaim reaches shrink_one() through lru_gen_shrink_node() -> shrink_many() without any protection computation, so emin/elow are whatever a previous kswapd run left behind
- or zero if kswapd never ran on this node. Relying on a prior kswapd pass is not correct either: a memcg's emin/elow are derived from its ancestors' memory.min/low settings and from children_min_usage, both of which change over time, so emin/elow go stale even after kswapd has run and must be recomputed at the point of reclaim.
Introduce mem_cgroup_calculate_protection_path() which computes emin/elow along the root-to-target path only, by iterating through the cgroup ancestors array top-down. This avoids the full tree traversal that would be needed with mem_cgroup_calculate_protection(), limiting the cost to O(depth) per memcg - typically 3-5 levels.
Call it from shrink_one() for the non-kswapd path so that each memcg about to be shrunk has correct protection values.
--- include/linux/memcontrol.h | 10 +++++++++ mm/memcontrol.c | 45 ++++++++++++++++++++++++++++++++++++++ mm/vmscan.c | 8 ++++++- 3 files changed, 62 insertions(+), 1 deletion(-)
include/linux/memcontrol.h +10 -0
@@ -1885,6 +1885,16 @@ static inline bool memcg_is_dying(struct mem_cgroup *memcg) }#endif /* CONFIG_MEMCG */ +#if defined(CONFIG_MEMCG ) && defined(CONFIG_LRU_GEN ) +void mem_cgroup_calculate_protection_path (struct mem_cgroup *root, +struct mem_cgroup *memcg); +#else +static inline void mem_cgroup_calculate_protection_path (struct mem_cgroup *root, +struct mem_cgroup *memcg) +{ +} +#endif +#if defined(CONFIG_MEMCG ) && defined(CONFIG_ZSWAP )bool obj_cgroup_may_zswap (struct obj_cgroup *objcg);void obj_cgroup_charge_zswap (struct obj_cgroup *objcg,size_t size);
mm/memcontrol.c +45 -0
51 lines not on this page · raw message
mm/vmscan.c +7 -1
@@ -5111,7 +5111,13 @@ static int shrink_one(struct lruvec *lruvec, struct scan_control *sc)struct mem_cgroup *memcg =lruvec_memcg (lruvec);struct pglist_data *pgdat =lruvec_pgdat (lruvec); -/* lru_gen_age_node() called mem_cgroup_calculate_protection() */ +/* +* For kswapd, mem_cgroup_calculate_protection() has already +* been called during the top-down cgroup traversal. +*/ +if (!current_is_kswapd ()) +mem_cgroup_calculate_protection_path (NULL , memcg); +if (mem_cgroup_below_min (NULL , memcg))return MEMCG_LRU_YOUNG ;
Signature · 1 line
2.34.1