[Devel] [PATCH RH8 2/5] mm/memcg: limit page cache in memcg hack

Alexander Mikhalitsyn alexander.mikhalitsyn at virtuozzo.com
Thu Jul 29 18:20:46 MSK 2021


From: Andrey Ryabinin <aryabinin at virtuozzo.com>

Add new memcg file - memory.cache.limit_in_bytes.
Used to limit page cache usage in cgroup.

https://jira.sw.ru/browse/PSBM-77547

Signed-off-by: Andrey Ryabinin <aryabinin at virtuozzo.com>

khorenko@: usecase:
imagine a system service which anon memory you don't want to limit
(in our case it's a vStorage cgroup which hosts CSes and MDSes, they can
consume memory in some range and we don't want to set a limit for max possible
consumption - too high, and we don't know the number of CSes on the node -
admin can add CSes dynamically. And we don't want to dynamically
increase/decrease the limit).

If the cgroup is "unlimited" it produces permanent memory pressure on the node
because it generates a lot of pagecache and other cgroups on the node are
affected (even taking into account the fact of proportional fair reclaim).

=> solution is to limit pagecache only, so this is implemented.

Signed-off-by: Andrey Ryabinin <aryabinin at virtuozzo.com>
(cherry picked from commit da9151c891819733762a178b4efd7e44766fb8b1)

Reworked:
now we have no charge/cancel/commit/uncharge memcg API (we only have charge/uncharge)
=> we have to track pages which was charged as page cache => additional flag was introduced
which implemented using mm/page_ext.c subsystem (see mm/page_vzext.c)

See ms commits:
0d1c2072 ("mm: memcontrol: switch to native NR_FILE_PAGES and NR_SHMEM counters")
3fea5a49 ("mm: memcontrol: convert page cache to a new mem_cgroup_charge() API")

https://jira.sw.ru/browse/PSBM-131957

Signed-off-by: Alexander Mikhalitsyn <alexander.mikhalitsyn at virtuozzo.com>
---
 include/linux/memcontrol.h   |   3 +
 include/linux/page_vzflags.h |  31 ++++++
 mm/filemap.c                 |   2 +-
 mm/memcontrol.c              | 188 ++++++++++++++++++++++++++++++-----
 4 files changed, 198 insertions(+), 26 deletions(-)
 create mode 100644 include/linux/page_vzflags.h

diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
index fdf93a2de456..2e053bd43050 100644
--- a/include/linux/memcontrol.h
+++ b/include/linux/memcontrol.h
@@ -285,6 +285,7 @@ struct mem_cgroup {
 	struct page_counter kmem;
 	struct page_counter tcpmem;
 	RH_KABI_DEPRECATE(unsigned long, high)
+	struct page_counter cache;
 #else
 	union {
 		struct page_counter swap;	/* v2 only */
@@ -294,6 +295,7 @@ struct mem_cgroup {
 	/* Legacy consumer-oriented counters */
 	struct page_counter kmem;		/* v1 only */
 	struct page_counter tcpmem;		/* v1 only */
+	struct page_counter cache;
 #endif
 
 	/* Range enforcement for interrupt charges */
@@ -510,6 +512,7 @@ enum mem_cgroup_protection mem_cgroup_protected(struct mem_cgroup *root,
 						struct mem_cgroup *memcg);
 
 int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp_mask);
+int mem_cgroup_charge_cache(struct page *page, struct mm_struct *mm, gfp_t gfp_mask);
 
 void mem_cgroup_uncharge(struct page *page);
 void mem_cgroup_uncharge_list(struct list_head *page_list);
diff --git a/include/linux/page_vzflags.h b/include/linux/page_vzflags.h
new file mode 100644
index 000000000000..64a06ba8c9f6
--- /dev/null
+++ b/include/linux/page_vzflags.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __LINUX_PAGE_VZFLAGS_H
+#define __LINUX_PAGE_VZFLAGS_H
+
+#include <linux/page_vzext.h>
+#include <linux/page-flags.h>
+
+enum vzpageflags {
+	PGVZ_pagecache,
+};
+
+#define TESTVZPAGEFLAG(uname, lname)				\
+static __always_inline int PageVz##uname(struct page *page)		\
+	{ return get_page_vzext(page) && test_bit(PGVZ_##lname, &get_page_vzext(page)->vzflags); }
+
+#define SETVZPAGEFLAG(uname, lname)				\
+static __always_inline void SetVzPage##uname(struct page *page)		\
+	{ if (get_page_vzext(page)) set_bit(PGVZ_##lname, &get_page_vzext(page)->vzflags); }
+
+#define CLEARVZPAGEFLAG(uname, lname)				\
+static __always_inline void ClearVzPage##uname(struct page *page)		\
+	{ if (get_page_vzext(page)) clear_bit(PGVZ_##lname, &get_page_vzext(page)->vzflags); }
+
+#define VZPAGEFLAG(uname, lname)					\
+	TESTVZPAGEFLAG(uname, lname)				\
+	SETVZPAGEFLAG(uname, lname)				\
+	CLEARVZPAGEFLAG(uname, lname)
+
+VZPAGEFLAG(PageCache, pagecache)
+
+#endif /* __LINUX_PAGE_VZFLAGS_H */
diff --git a/mm/filemap.c b/mm/filemap.c
index f1a56ac4ec41..3d4e912f0930 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -862,7 +862,7 @@ static int __add_to_page_cache_locked(struct page *page,
 	page->index = offset;
 
 	if (!huge) {
-		error = mem_cgroup_charge(page, current->mm, gfp);
+		error = mem_cgroup_charge_cache(page, current->mm, gfp);
 		if (error)
 			goto error;
 		charged = true;
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index d3aa1cf20796..ac0ece68a17a 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -42,6 +42,7 @@
 #include <linux/vm_event_item.h>
 #include <linux/smp.h>
 #include <linux/page-flags.h>
+#include <linux/page_vzflags.h>
 #include <linux/backing-dev.h>
 #include <linux/bit_spinlock.h>
 #include <linux/rcupdate.h>
@@ -225,6 +226,7 @@ enum res_type {
 	_OOM_TYPE,
 	_KMEM,
 	_TCP,
+	_CACHE,
 };
 
 #define MEMFILE_PRIVATE(x, val)	((x) << 16 | (val))
@@ -2799,7 +2801,7 @@ static bool kmem_reclaim_is_low(struct mem_cgroup *memcg)
 }
 
 static int try_charge(struct mem_cgroup *memcg, gfp_t gfp_mask, bool kmem_charge,
-		      unsigned int nr_pages)
+		      unsigned int nr_pages, bool cache_charge)
 {
 	unsigned int batch = max(MEMCG_CHARGE_BATCH, nr_pages);
 	int nr_retries = MAX_RECLAIM_RETRIES;
@@ -2817,13 +2819,21 @@ static int try_charge(struct mem_cgroup *memcg, gfp_t gfp_mask, bool kmem_charge
 	may_swap = true;
 	kmem_limit = false;
 	if (consume_stock(memcg, nr_pages)) {
-		if (!kmem_charge)
-			return 0;
-		if (page_counter_try_charge(&memcg->kmem, nr_pages, &counter))
-			return 0;
-		refill_stock(memcg, nr_pages);
+		if (kmem_charge && !page_counter_try_charge(
+				&memcg->kmem, nr_pages, &counter)) {
+			refill_stock(memcg, nr_pages);
+			goto charge;
+		}
+
+		if (cache_charge && !page_counter_try_charge(
+				&memcg->cache, nr_pages, &counter)) {
+			refill_stock(memcg, nr_pages);
+			goto charge;
+		}
+		return 0;
 	}
 
+charge:
 	mem_over_limit = NULL;
 	if (page_counter_try_charge(&memcg->memory, batch, &counter)) {
 		if (do_memsw_account() && !page_counter_try_charge(
@@ -2845,6 +2855,19 @@ static int try_charge(struct mem_cgroup *memcg, gfp_t gfp_mask, bool kmem_charge
 		}
 	}
 
+	if (!mem_over_limit && cache_charge) {
+		if (page_counter_try_charge(&memcg->cache, nr_pages, &counter))
+			goto done_restock;
+
+		may_swap = false;
+		mem_over_limit = mem_cgroup_from_counter(counter, cache);
+		page_counter_uncharge(&memcg->memory, batch);
+		if (do_memsw_account())
+			page_counter_uncharge(&memcg->memsw, batch);
+		if (kmem_charge)
+			page_counter_uncharge(&memcg->kmem, nr_pages);
+	}
+
 	if (!mem_over_limit)
 		goto done_restock;
 
@@ -2970,10 +2993,17 @@ static int try_charge(struct mem_cgroup *memcg, gfp_t gfp_mask, bool kmem_charge
 	page_counter_charge(&memcg->memory, nr_pages);
 	if (do_memsw_account())
 		page_counter_charge(&memcg->memsw, nr_pages);
+	if (kmem_charge)
+		page_counter_charge(&memcg->kmem, nr_pages);
+	if (cache_charge)
+		page_counter_charge(&memcg->cache, nr_pages);
 
 	return 0;
 
 done_restock:
+	if (cache_charge)
+		page_counter_charge(&memcg->cache, batch);
+
 	if (batch > nr_pages)
 		refill_stock(memcg, batch - nr_pages);
 
@@ -3200,7 +3230,7 @@ int __memcg_kmem_charge(struct mem_cgroup *memcg, gfp_t gfp,
 {
 	int ret;
 
-	ret = try_charge(memcg, gfp, true, nr_pages);
+	ret = try_charge(memcg, gfp, true, nr_pages, false);
 	if (ret)
 		return ret;
 
@@ -3411,7 +3441,7 @@ int memcg_charge_kmem(struct mem_cgroup *memcg, gfp_t gfp,
 {
 	int ret = 0;
 
-	ret = try_charge(memcg, gfp, true, nr_pages);
+	ret = try_charge(memcg, gfp, true, nr_pages, false);
 	return ret;
 }
 
@@ -3763,6 +3793,8 @@ static u64 mem_cgroup_read_u64(struct cgroup_subsys_state *css,
 		break;
 	case _TCP:
 		counter = &memcg->tcpmem;
+	case _CACHE:
+		counter = &memcg->cache;
 		break;
 	default:
 		BUG();
@@ -3985,6 +4017,43 @@ static int memcg_update_tcp_max(struct mem_cgroup *memcg, unsigned long max)
 	return ret;
 }
 
+static int memcg_update_cache_max(struct mem_cgroup *memcg,
+				  unsigned long limit)
+{
+	unsigned long nr_pages;
+	bool enlarge = false;
+	int ret;
+
+	do {
+		if (signal_pending(current)) {
+			ret = -EINTR;
+			break;
+		}
+		mutex_lock(&memcg_max_mutex);
+
+		if (limit > memcg->cache.max)
+			enlarge = true;
+
+		ret = page_counter_set_max(&memcg->cache, limit);
+		mutex_unlock(&memcg_max_mutex);
+
+		if (!ret)
+			break;
+
+		nr_pages = max_t(long, 1, page_counter_read(&memcg->cache) - limit);
+		if (!try_to_free_mem_cgroup_pages(memcg, nr_pages,
+						GFP_KERNEL, false)) {
+			ret = -EBUSY;
+			break;
+		}
+	} while (1);
+
+	if (!ret && enlarge)
+		memcg_oom_recover(memcg);
+
+	return ret;
+}
+
 /*
  * The user of this function is...
  * RES_LIMIT.
@@ -4019,6 +4088,8 @@ static ssize_t mem_cgroup_write(struct kernfs_open_file *of,
 			break;
 		case _TCP:
 			ret = memcg_update_tcp_max(memcg, nr_pages);
+		case _CACHE:
+			ret = memcg_update_cache_max(memcg, nr_pages);
 			break;
 		}
 		break;
@@ -4048,6 +4119,8 @@ static ssize_t mem_cgroup_reset(struct kernfs_open_file *of, char *buf,
 		break;
 	case _TCP:
 		counter = &memcg->tcpmem;
+	case _CACHE:
+		counter = &memcg->cache;
 		break;
 	default:
 		BUG();
@@ -5713,6 +5786,12 @@ static struct cftype mem_cgroup_legacy_files[] = {
 	{
 		.name = "pressure_level",
 	},
+	{
+		.name = "cache.limit_in_bytes",
+		.private = MEMFILE_PRIVATE(_CACHE, RES_LIMIT),
+		.write = mem_cgroup_write,
+		.read_u64 = mem_cgroup_read_u64,
+	},
 #ifdef CONFIG_NUMA
 	{
 		.name = "numa_stat",
@@ -6031,17 +6110,20 @@ mem_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)
 		page_counter_init(&memcg->swap, NULL);
 		page_counter_init(&memcg->kmem, NULL);
 		page_counter_init(&memcg->tcpmem, NULL);
+		page_counter_init(&memcg->cache, NULL);
 	} else if (parent->use_hierarchy) {
 		memcg->use_hierarchy = true;
 		page_counter_init(&memcg->memory, &parent->memory);
 		page_counter_init(&memcg->swap, &parent->swap);
 		page_counter_init(&memcg->kmem, &parent->kmem);
 		page_counter_init(&memcg->tcpmem, &parent->tcpmem);
+		page_counter_init(&memcg->cache, &parent->cache);
 	} else {
 		page_counter_init(&memcg->memory, &root_mem_cgroup->memory);
 		page_counter_init(&memcg->swap, &root_mem_cgroup->swap);
 		page_counter_init(&memcg->kmem, &root_mem_cgroup->kmem);
 		page_counter_init(&memcg->tcpmem, &root_mem_cgroup->tcpmem);
+		page_counter_init(&memcg->cache, &root_mem_cgroup->cache);
 		/*
 		 * Deeper hierachy with use_hierarchy == false doesn't make
 		 * much sense so let cgroup subsystem know about this
@@ -6170,6 +6252,7 @@ static void mem_cgroup_css_reset(struct cgroup_subsys_state *css)
 	page_counter_set_max(&memcg->swap, PAGE_COUNTER_MAX);
 	page_counter_set_max(&memcg->kmem, PAGE_COUNTER_MAX);
 	page_counter_set_max(&memcg->tcpmem, PAGE_COUNTER_MAX);
+	page_counter_set_max(&memcg->cache, PAGE_COUNTER_MAX);
 	page_counter_set_min(&memcg->memory, 0);
 	page_counter_set_low(&memcg->memory, 0);
 	page_counter_set_high(&memcg->memory, PAGE_COUNTER_MAX);
@@ -6185,7 +6268,8 @@ static int mem_cgroup_do_precharge(unsigned long count)
 	int ret;
 
 	/* Try a single bulk charge without reclaim first, kswapd may wake */
-	ret = try_charge(mc.to, GFP_KERNEL & ~__GFP_DIRECT_RECLAIM, false, count);
+	ret = try_charge(mc.to, GFP_KERNEL & ~__GFP_DIRECT_RECLAIM, false,
+			count, false);
 	if (!ret) {
 		mc.precharge += count;
 		return ret;
@@ -6193,7 +6277,8 @@ static int mem_cgroup_do_precharge(unsigned long count)
 
 	/* Try charges one by one with reclaim, but do not retry */
 	while (count--) {
-		ret = try_charge(mc.to, GFP_KERNEL | __GFP_NORETRY, false, 1);
+		ret = try_charge(mc.to, GFP_KERNEL | __GFP_NORETRY, false, 1, false);
+
 		if (ret)
 			return ret;
 		mc.precharge++;
@@ -7405,18 +7490,8 @@ enum mem_cgroup_protection mem_cgroup_protected(struct mem_cgroup *root,
 		return MEMCG_PROT_NONE;
 }
 
-/**
- * mem_cgroup_charge - charge a newly allocated page to a cgroup
- * @page: page to charge
- * @mm: mm context of the victim
- * @gfp_mask: reclaim mode
- *
- * Try to charge @page to the memcg that @mm belongs to, reclaiming
- * pages according to @gfp_mask if necessary.
- *
- * Returns 0 on success. Otherwise, an error code is returned.
- */
-int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp_mask)
+static int __mem_cgroup_charge(struct page *page, struct mm_struct *mm,
+			       gfp_t gfp_mask, bool cache_charge)
 {
 	unsigned int nr_pages = thp_nr_pages(page);
 	struct mem_cgroup *memcg = NULL;
@@ -7451,13 +7526,25 @@ int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp_mask)
 	if (!memcg)
 		memcg = get_mem_cgroup_from_mm(mm);
 
-	ret = try_charge(memcg, gfp_mask, false, nr_pages);
+	ret = try_charge(memcg, gfp_mask, false, nr_pages, cache_charge);
 	if (ret)
 		goto out_put;
 
 	css_get(&memcg->css);
 	commit_charge(page, memcg);
 
+	/*
+	 * Here we set extended flag (see page_vzflags.c)
+	 * on page which indicates that page is charged as
+	 * a "page cache" page.
+	 *
+	 * We always cleanup this flag on uncharging, it means
+	 * that during charging page we shoudn't have this flag set.
+	 */
+	BUG_ON(PageVzPageCache(page));
+	if (cache_charge)
+		SetVzPagePageCache(page);
+
 	local_irq_disable();
 	mem_cgroup_charge_statistics(memcg, page, nr_pages);
 	memcg_check_events(memcg, page);
@@ -7491,11 +7578,34 @@ int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp_mask)
 	return ret;
 }
 
+/**
+ * mem_cgroup_charge - charge a newly allocated page to a cgroup
+ * @page: page to charge
+ * @mm: mm context of the victim
+ * @gfp_mask: reclaim mode
+ *
+ * Try to charge @page to the memcg that @mm belongs to, reclaiming
+ * pages according to @gfp_mask if necessary.
+ *
+ * Returns 0 on success. Otherwise, an error code is returned.
+ */
+int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp_mask)
+{
+
+	return __mem_cgroup_charge(page, mm, gfp_mask, false);
+}
+
+int mem_cgroup_charge_cache(struct page *page, struct mm_struct *mm, gfp_t gfp_mask)
+{
+	return __mem_cgroup_charge(page, mm, gfp_mask, true);
+}
+
 struct uncharge_gather {
 	struct mem_cgroup *memcg;
 	unsigned long nr_pages;
 	unsigned long pgpgout;
 	unsigned long nr_kmem;
+	unsigned long nr_pgcache;
 	struct page *dummy_page;
 };
 
@@ -7514,6 +7624,9 @@ static void uncharge_batch(const struct uncharge_gather *ug)
 			page_counter_uncharge(&ug->memcg->memsw, ug->nr_pages);
 		if (!cgroup_subsys_on_dfl(memory_cgrp_subsys) && ug->nr_kmem)
 			page_counter_uncharge(&ug->memcg->kmem, ug->nr_kmem);
+		if (ug->nr_pgcache)
+			page_counter_uncharge(&ug->memcg->cache, ug->nr_pgcache);
+
 		memcg_oom_recover(ug->memcg);
 	}
 
@@ -7557,6 +7670,16 @@ static void uncharge_page(struct page *page, struct uncharge_gather *ug)
 	ug->nr_pages += nr_pages;
 
 	if (!PageKmemcg(page)) {
+		if (PageVzPageCache(page)) {
+			ug->nr_pgcache += nr_pages;
+			/*
+			 * If we are here, it means that page *will* be
+			 * uncharged anyway. We can safely clean
+			 * "page is charged as a page cache" flag here.
+			 */
+			ClearVzPagePageCache(page);
+		}
+
 		ug->pgpgout++;
 	} else {
 		ug->nr_kmem += nr_pages;
@@ -7672,6 +7795,21 @@ void mem_cgroup_migrate(struct page *oldpage, struct page *newpage)
 	if (do_memsw_account())
 		page_counter_charge(&memcg->memsw, nr_pages);
 
+	/*
+	 * copy_page_vzflags() called before mem_cgroup_migrate()
+	 * in migrate_page_states (mm/migrate.c)
+	 *
+	 * Let's check that all fine with flags:
+	 * from one point of view page cache pages is always
+	 * not anonimous and not swap backed;
+	 * from another point of view we must have
+	 * PageVzPageCache(page) ext flag set.
+	 */
+	WARN_ON((!PageAnon(newpage) && !PageSwapBacked(newpage)) !=
+		PageVzPageCache(newpage));
+	if (PageVzPageCache(newpage))
+		page_counter_charge(&memcg->cache, nr_pages);
+
 	css_get(&memcg->css);
 	commit_charge(newpage, memcg);
 
@@ -7753,10 +7891,10 @@ bool mem_cgroup_charge_skmem(struct mem_cgroup *memcg, unsigned int nr_pages)
 
 	mod_memcg_state(memcg, MEMCG_SOCK, nr_pages);
 
-	if (try_charge(memcg, gfp_mask, false, nr_pages) == 0)
+	if (try_charge(memcg, gfp_mask, false, nr_pages, false) == 0)
 		return true;
 
-	try_charge(memcg, gfp_mask|__GFP_NOFAIL, false, nr_pages);
+	try_charge(memcg, gfp_mask|__GFP_NOFAIL, false, nr_pages, false);
 	return false;
 }
 
-- 
2.28.0



More information about the Devel mailing list