Skip to contentSkip to conversation

memcg: charge before adding to swapcache on swapin

PATCH Superseded Shakeel Butt 2 participants 4 messages Feb 19, 2021 – Feb 22, 2021

Subsystems (5)

v1 of 4 Superseded Latest is v4 →

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 memcg: charge before adding to swapcache on swapin

Currently the kernel adds the page, allocated for swapin, to the swapcache before charging the page. This is fine but now we want a per-memcg swapcache stat which is essential for folks who wants to transparently migrate from cgroup v1's memsw to cgroup v2's memory and swap counters.

To correctly maintain the per-memcg swapcache stat, one option which this patch has adopted is to charge the page before adding it to swapcache. One challenge in this option is the failure case of add_to_swap_cache() on which we need to undo the mem_cgroup_charge(). Specifically undoing mem_cgroup_uncharge_swap() is not simple.

This patch circumvent this specific issue by removing the failure path of add_to_swap_cache() by providing __GFP_NOFAIL. Please note that in this specific situation ENOMEM was the only possible failure of add_to_swap_cache() which is removed by using __GFP_NOFAIL.

Another option was to use __mod_memcg_lruvec_state(NR_SWAPCACHE) in mem_cgroup_charge() but then we need to take of the do_swap_page() case where synchronous swap devices bypass the swapcache. The do_swap_page() already does hackery to set and reset PageSwapCache bit to make mem_cgroup_charge() execute the swap accounting code and then we would need to add additional parameter to tell to not touch NR_SWAPCACHE stat as that code patch bypass swapcache.

This patch added memcg charging API explicitly foe swapin pages and cleaned up do_swap_page() to not set and reset PageSwapCache bit.

---

Andrew, please couple this patch with "mm: memcg: add swapcache stat for memcg v2" patch and there is no urgency for these two to be in 5.12.

 include/linux/memcontrol.h |   8 +++
 mm/memcontrol.c            | 100 ++++++++++++++++++++++---------------
 mm/memory.c                |   8 +--
 mm/swap_state.c            |  16 +++---
 scripts/cc-version.sh      |   0
 5 files changed, 77 insertions(+), 55 deletions(-)
 mode change 100644 => 100755 scripts/cc-version.sh
include/linux/memcontrol.h +8 -0
index e6dc793d587d..f3af65caddc6 100644
@@ -596,6 +596,8 @@ static inline bool mem_cgroup_below_min(struct mem_cgroup *memcg)
 }
 
 int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp_mask);
+int mem_cgroup_charge_swapin_page(struct page *page, struct mm_struct *mm,
+				  gfp_t gfp, swp_entry_t entry);
 
 void mem_cgroup_uncharge(struct page *page);
 void mem_cgroup_uncharge_list(struct list_head *page_list);
@@ -1141,6 +1143,12 @@ static inline int mem_cgroup_charge(struct page *page, struct mm_struct *mm,
 	return 0;
 }
 
+static inline int mem_cgroup_charge_swapin_page(struct page *page,
+			struct mm_struct *mm, gfp_t gfp, swp_entry_t entry);
+{
+	return 0;
+}
+
 static inline void mem_cgroup_uncharge(struct page *page)
 {
 }
mm/memcontrol.c +59 -41

138 lines not on this page · raw message

mm/memory.c +2 -6
index c8e357627318..fb39af50f62d 100644
@@ -3311,13 +3311,9 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 
 				__SetPageLocked(page);
 				__SetPageSwapBacked(page);
-				set_page_private(page, entry.val);
 
-				/* Tell memcg to use swap ownership records */
-				SetPageSwapCache(page);
-				err = mem_cgroup_charge(page, vma->vm_mm,
-							GFP_KERNEL);
-				ClearPageSwapCache(page);
+				err = mem_cgroup_charge_swapin_page(page,
+						vma->vm_mm, GFP_KERNEL, entry);
 				if (err) {
 					ret = VM_FAULT_OOM;
 					goto out_page;
mm/swap_state.c +8 -8
index 3cdee7b11da9..816218545a48 100644
@@ -497,16 +497,15 @@ struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 	__SetPageLocked(page);
 	__SetPageSwapBacked(page);
 
-	/* May fail (-ENOMEM) if XArray node allocation failed. */
-	if (add_to_swap_cache(page, entry, gfp_mask & GFP_RECLAIM_MASK, &shadow)) {
-		put_swap_page(page, entry);
+	if (mem_cgroup_charge_swapin_page(page, NULL, gfp_mask, entry))
 		goto fail_unlock;
-	}
 
-	if (mem_cgroup_charge(page, NULL, gfp_mask)) {
-		delete_from_swap_cache(page);
-		goto fail_unlock;
-	}
+	/*
+	 * Use __GFP_NOFAIL to not worry about undoing the changes done by
+	 * mem_cgroup_charge_swapin_page() on failure of add_to_swap_cache().
+	 */
+	add_to_swap_cache(page, entry,
+			  (gfp_mask|__GFP_NOFAIL) & GFP_RECLAIM_MASK, &shadow);
 
 	if (shadow)
 		workingset_refault(page, shadow);
@@ -517,6 +516,7 @@ struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 	return page;
 
 fail_unlock:
+	put_swap_page(page, entry);
 	unlock_page(page);
 	put_page(page);
 	return NULL;
scripts/cc-version.sh +0 -0
old mode 100644
new mode 100755
Signature · 1 line
2.30.0.617.g56c4b15f3c-goog
#1 lore raw

On Fri, Feb 19, 2021 at 2:44 PM Shakeel Butt <[email protected]> wrote:
[snip]

Quoted 1 line
 mode change 100644 => 100755 scripts/cc-version.sh

[snip

scripts/cc-version.sh

Quoted 3 lines
scripts/cc-version.sh +0 -0
old mode 100644
new mode 100755

Please ignore these unintended mode changes. I will remove these in the next version.

maintainer
#2 lore raw
In reply to Shakeel Butt
Quoted 25 lines · Shakeel Butt

Currently the kernel adds the page, allocated for swapin, to the swapcache before charging the page. This is fine but now we want a per-memcg swapcache stat which is essential for folks who wants to transparently migrate from cgroup v1's memsw to cgroup v2's memory and swap counters.

To correctly maintain the per-memcg swapcache stat, one option which this patch has adopted is to charge the page before adding it to swapcache. One challenge in this option is the failure case of add_to_swap_cache() on which we need to undo the mem_cgroup_charge(). Specifically undoing mem_cgroup_uncharge_swap() is not simple.

This patch circumvent this specific issue by removing the failure path of add_to_swap_cache() by providing __GFP_NOFAIL. Please note that in this specific situation ENOMEM was the only possible failure of add_to_swap_cache() which is removed by using __GFP_NOFAIL.

Another option was to use __mod_memcg_lruvec_state(NR_SWAPCACHE) in mem_cgroup_charge() but then we need to take of the do_swap_page() case where synchronous swap devices bypass the swapcache. The do_swap_page() already does hackery to set and reset PageSwapCache bit to make mem_cgroup_charge() execute the swap accounting code and then we would need to add additional parameter to tell to not touch NR_SWAPCACHE stat as that code patch bypass swapcache.

Quoted 4 lines

This patch added memcg charging API explicitly foe swapin pages and cleaned up do_swap_page() to not set and reset PageSwapCache bit.

The patch makes sense to me. While it extends the charge interface, I actually quite like that it charges the page earlier - before putting it into wider circulation. It's a step in the right direction.

But IMO the semantics of mem_cgroup_charge_swapin_page() are a bit too
fickle: the __GFP_NOFAIL in add_to_swap_cache() works around it, but having a must-not-fail-after-this line makes the code tricky to work on and error prone.

It would be nicer to do a proper transaction sequence.

Quoted 11 lines
mm/swap_state.c +1 -4
@@ -497,16 +497,15 @@ struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 	__SetPageLocked(page);
 	__SetPageSwapBacked(page);
 
-	/* May fail (-ENOMEM) if XArray node allocation failed. */
-	if (add_to_swap_cache(page, entry, gfp_mask & GFP_RECLAIM_MASK, &shadow)) {
-		put_swap_page(page, entry);
+	if (mem_cgroup_charge_swapin_page(page, NULL, gfp_mask, entry))
 		goto fail_unlock;
-	}
 

mm/swap_state.c struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask, · line 508

Quoted 10 lines
mm/swap_state.c +6 -4
-	if (mem_cgroup_charge(page, NULL, gfp_mask)) {
-		delete_from_swap_cache(page);
-		goto fail_unlock;
-	}
+	/*
+	 * Use __GFP_NOFAIL to not worry about undoing the changes done by
+	 * mem_cgroup_charge_swapin_page() on failure of add_to_swap_cache().
+	 */
+	add_to_swap_cache(page, entry,
+			  (gfp_mask|__GFP_NOFAIL) & GFP_RECLAIM_MASK, &shadow);

How about:

	mem_cgroup_charge_swapin_page()
	add_to_swap_cache()
	mem_cgroup_finish_swapin_page()

where finish_swapin_page() only uncharges the swap entry (on cgroup1) once the swap->memory transition is complete?

Otherwise the patch looks good to me.

#3 lore raw
Quoted 67 lines · Johannes Weiner
Quoted 25 lines · Shakeel Butt

Currently the kernel adds the page, allocated for swapin, to the swapcache before charging the page. This is fine but now we want a per-memcg swapcache stat which is essential for folks who wants to transparently migrate from cgroup v1's memsw to cgroup v2's memory and swap counters.

To correctly maintain the per-memcg swapcache stat, one option which this patch has adopted is to charge the page before adding it to swapcache. One challenge in this option is the failure case of add_to_swap_cache() on which we need to undo the mem_cgroup_charge(). Specifically undoing mem_cgroup_uncharge_swap() is not simple.

This patch circumvent this specific issue by removing the failure path of add_to_swap_cache() by providing __GFP_NOFAIL. Please note that in this specific situation ENOMEM was the only possible failure of add_to_swap_cache() which is removed by using __GFP_NOFAIL.

Another option was to use __mod_memcg_lruvec_state(NR_SWAPCACHE) in mem_cgroup_charge() but then we need to take of the do_swap_page() case where synchronous swap devices bypass the swapcache. The do_swap_page() already does hackery to set and reset PageSwapCache bit to make mem_cgroup_charge() execute the swap accounting code and then we would need to add additional parameter to tell to not touch NR_SWAPCACHE stat as that code patch bypass swapcache.

Quoted 4 lines

This patch added memcg charging API explicitly foe swapin pages and cleaned up do_swap_page() to not set and reset PageSwapCache bit.

The patch makes sense to me. While it extends the charge interface, I actually quite like that it charges the page earlier - before putting it into wider circulation. It's a step in the right direction.

But IMO the semantics of mem_cgroup_charge_swapin_page() are a bit too
fickle: the __GFP_NOFAIL in add_to_swap_cache() works around it, but having a must-not-fail-after-this line makes the code tricky to work on and error prone.

It would be nicer to do a proper transaction sequence.

Quoted 11 lines
mm/swap_state.c +1 -4
@@ -497,16 +497,15 @@ struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
      __SetPageLocked(page);
      __SetPageSwapBacked(page);
 
-     /* May fail (-ENOMEM) if XArray node allocation failed. */
-     if (add_to_swap_cache(page, entry, gfp_mask & GFP_RECLAIM_MASK, &shadow)) {
-             put_swap_page(page, entry);
+     if (mem_cgroup_charge_swapin_page(page, NULL, gfp_mask, entry))
              goto fail_unlock;
-     }
 

mm/swap_state.c struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask, · line 508

Quoted 10 lines
mm/swap_state.c +6 -4
-     if (mem_cgroup_charge(page, NULL, gfp_mask)) {
-             delete_from_swap_cache(page);
-             goto fail_unlock;
-     }
+     /*
+      * Use __GFP_NOFAIL to not worry about undoing the changes done by
+      * mem_cgroup_charge_swapin_page() on failure of add_to_swap_cache().
+      */
+     add_to_swap_cache(page, entry,
+                       (gfp_mask|__GFP_NOFAIL) & GFP_RECLAIM_MASK, &shadow);

How about:

Quoted 8 lines
        mem_cgroup_charge_swapin_page()
        add_to_swap_cache()
        mem_cgroup_finish_swapin_page()

where finish_swapin_page() only uncharges the swap entry (on cgroup1) once the swap->memory transition is complete?

Otherwise the patch looks good to me.

Thanks for the review and yes this makes the code much more clear and maintainable.