[PATCH 0/6] mm: memcontrol: implement the memsw limit on cgroup v2
Jingxiang Zeng via B4 Relay <[email protected]>
| Newsgroups | org.kernel.vger.linux-doc,org.freedesktop.lists.dri-devel,org.kernel.feeds.b4-sent,org.kernel.vger.cgroups,org.kernel.vger.linux-kernel,org.kvack.linux-mm |
|---|---|
| Message-ID | <[email protected]> |
cgroup v1 caps the sum of memory and swap through memsw.limit_in_bytes. v2 only offers separate memory.max and memory.swap.max, so a workload that has to be capped on the total of the two has no equivalent knob: memory.max alone can be met by swapping, and capping both separately reserves swap the workload may never use. A container capped at N pages should stay capped at N once swapping is possible, but with memory.max alone the pages move to swap and it allocates N more; reserving swap per cgroup instead means guessing how much of the footprint will be cold at any moment. The combined limit states the intent directly - N pages however they are divided between RAM and swap - and leaves the host free to offload cold pages. This has been discussed before [1], and the direction agreed on was to track and control the combined counter as a proper v2 feature rather than fold it into the existing swap knobs: My suggestion is to factor out from struct page_counter all the stuff that is not necessary for all users, and then have separate counters for swap and memsw. The protection stuff is long overdue for this. It makes up nearly half of the struct's members, but is only used by the memory counter. Even before your patches this is unnecessary bloat in the swap/memsw, kmem and tcpmem counters. Fix that and having separate counters is a non-issue. and, on the interface: This should be new knobs, e.g. memory.memsw.current, memory.memsw.max. Patches 1-3 are that refactoring. Hierarchical protection is only used by the memory counter and by dmem pools, yet every page_counter carried it; moving it into a separate struct page_counter_protection, embedded only where it is used, takes struct page_counter from 192 to 128 bytes on x86_64. Both embedders put the context on a cache line boundary (384 in struct mem_cgroup, 192 in the dmem pool state), so the fields the charge path touches - min, low and the four usage counters - share a cache line with emin, which only reclaim recomputes; only elow crosses into the next one. Patch 4 splits the counters. swap and memsw share a union today because v1 only ever charges memsw and v2 only ever charges swap, so charging both on both hierarchies is what the combined limit needs. v2 hands the combined charge over to the swap slot in __mem_cgroup_try_charge_swap() and drops it again in __mem_cgroup_uncharge_swap(). memsw.max still defaults to "max" and v2 cannot set it, so behaviour does not change. Patch 5 exposes memory.memsw.current and memory.memsw.max on non-root cgroups. A charge counted there is held for as long as the memory occupies either RAM or a swap slot, so reclaim cannot get back under the limit by swapping; try_to_free_mem_cgroup_pages() is called without MEMCG_RECLAIM_MAY_SWAP when the limit is written, and the cgroup OOM killer is the last resort, mirroring memory.max. Both writers keep memory.max <= memory.memsw.max under a mutex, so the two limits cannot be configured into a state reclaim could never satisfy - the invariant v1 keeps in mem_cgroup_resize_max(), including its serialization. Patch 6 clamps mem_cgroup_get_max() to the new limit. That value becomes oc->totalpages for memcg OOM and oom_badness() scales oom_score_adj by it, so while the v2 branch derived the ceiling from memory.max plus memory.swap.max it overstated the reachable total once a combined limit was set, and oom_score_adj weighed correspondingly more than intended. Size effect, measured with pahole (x86_64, 64-byte cache lines). The split costs a cgroup one more page counter, and without patches 1-3 that counter would be 192 bytes: struct mem_cgroup (CONFIG_MEMCG_V1=y) 2240 -> 2432 -> 2240 struct mem_cgroup (CONFIG_MEMCG_V1=n) 1664 -> 1856 -> 1792 The middle figure is the same tree with the union split but the counters left at 192 bytes, so it is what the series would have cost without the refactoring. On a v1 kernel that is the whole 192 bytes paid back; on a v2-only one the three counters involved shed 192 bytes between them, but the protection context takes 72 back and alignment a further 56, so 64 are paid back. Counters that gain nothing keep their share: struct page_counter 192 -> 128 bytes struct hugetlb_cgroup 1344 -> 1088 bytes dmem pool state 320 -> 320 bytes Tested on x86_64 with CONFIG_MEMCG_V1=y and =n, each patch building on its own: - memory.memsw.current equals memory.current plus memory.swap.current across swapout, swapin and swapoff. - Squeezing memory.max drops memory.current while memory.memsw.current holds, and a combined limit stops a 128M working set that otherwise escapes a 32M memory.max by swapping. - Writing memory.memsw.max reclaims without MEMCG_RECLAIM_MAY_SWAP: swap usage stays flat and the cgroup goes to OOM, where writing memory.max swaps instead. - Concurrent writers to the two limits could not leave the invariant violated in 600 rounds. - A kretprobe on mem_cgroup_get_max() returns 48M for memory.max=32M and memory.memsw.max=48M with 2G of swap, and is unchanged with memory.memsw.max left at "max". - v1 memsw.limit_in_bytes, and the v1 paths reaching the new NULL ->prot guards (css_offline, css_reset, lru_gen_age_node), behave as before. [1] https://lore.kernel.org/all/[email protected]/ Signed-off-by: Jingxiang Zeng <[email protected]> --- Jingxiang Zeng (6): mm: page_counter: add page_counter_protection struct and init API mm: page_counter: track protection state in page_counter_protection mm: page_counter: drop protection fields from struct page_counter mm: memcontrol: give swap and memsw their own page counters mm: memcontrol: add memory.memsw.max to the default hierarchy mm: memcontrol: clamp mem_cgroup_get_max() to the combined limit Documentation/admin-guide/cgroup-v2.rst | 32 +++++ include/linux/memcontrol.h | 27 ++-- include/linux/page_counter.h | 88 +++++++++--- kernel/cgroup/dmem.c | 21 +-- mm/hugetlb_cgroup.c | 4 +- mm/memcontrol.c | 241 ++++++++++++++++++++++++++------ mm/page_counter.c | 61 +++++--- 7 files changed, 371 insertions(+), 103 deletions(-) --- base-commit: 1ed9cdd724d46119dd9adf0ffba2f2daaa3335ef change-id: 20260916-descriptive-name-0cf0d5346bcb Best regards, -- Jingxiang Zeng <[email protected]>