From: Kairui Song <ryncsn@gmail.com>
To: linux-mm@kvack.org
Cc: Andrew Morton <akpm@linux-foundation.org>,
Matthew Wilcox <willy@infradead.org>,
Johannes Weiner <hannes@cmpxchg.org>,
Roman Gushchin <roman.gushchin@linux.dev>,
Waiman Long <longman@redhat.com>,
Shakeel Butt <shakeel.butt@linux.dev>,
Michal Hocko <mhocko@suse.com>,
Chengming Zhou <zhouchengming@bytedance.com>,
Qi Zheng <zhengqi.arch@bytedance.com>,
Muchun Song <muchun.song@linux.dev>,
Kairui Song <kasong@tencent.com>
Subject: [PATCH v3 4/6] mm/list_lru: simplify reparenting and initial allocation
Date: Tue, 5 Nov 2024 01:52:55 +0800 [thread overview]
Message-ID: <20241104175257.60853-5-ryncsn@gmail.com> (raw)
In-Reply-To: <20241104175257.60853-1-ryncsn@gmail.com>
From: Kairui Song <kasong@tencent.com>
Currently, there is a lot of code for detecting reparent racing
using kmemcg_id as the synchronization flag. And an intermediate
table is required to record and compare the kmemcg_id.
We can simplify this by just checking the cgroup css status, skip
if cgroup is being offlined. On the reparenting side, ensure no
more allocation is on going and no further allocation will occur
by using the XArray lock as barrier.
Combined with a O(n^2) top-down walk for the allocation, we get rid
of the intermediate table allocation completely. Despite being O(n^2),
it should be actually faster because it's not practical to have a very
deep cgroup level, and in most cases the parent cgroup should have been
allocated already.
This also avoided changing kmemcg_id before reparenting, making
cgroups have a stable index for list_lru_memcg. After this change
it's possible that a dying cgroup will see a NULL value in XArray
corresponding to the kmemcg_id, because the kmemcg_id will point
to an empty slot. In such case, just fallback to use its parent.
As a result the code is simpler, following test also showed a
very slight performance gain (12 test runs):
prepare() {
mkdir /tmp/test-fs
modprobe brd rd_nr=1 rd_size=16777216
mkfs.xfs -f /dev/ram0
mount -t xfs /dev/ram0 /tmp/test-fs
for i in $(seq 10000); do
seq 8000 > "/tmp/test-fs/$i"
done
mkdir -p /sys/fs/cgroup/system.slice/bench/test/1
echo +memory > /sys/fs/cgroup/system.slice/bench/cgroup.subtree_control
echo +memory > /sys/fs/cgroup/system.slice/bench/test/cgroup.subtree_control
echo +memory > /sys/fs/cgroup/system.slice/bench/test/1/cgroup.subtree_control
echo 768M > /sys/fs/cgroup/system.slice/bench/memory.max
}
do_test() {
read_worker() {
mkdir -p "/sys/fs/cgroup/system.slice/bench/test/1/$1"
echo $BASHPID > "/sys/fs/cgroup/system.slice/bench/test/1/$1/cgroup.procs"
read -r __TMP < "/tmp/test-fs/$1";
}
read_in_all() {
for i in $(seq 10000); do
read_worker "$i" &
done; wait
}
echo 3 > /proc/sys/vm/drop_caches
time read_in_all
for i in $(seq 1 10000); do
rmdir "/sys/fs/cgroup/system.slice/bench/test/1/$i" &>/dev/null
done
}
Before:
real 0m3.498s user 0m11.037s sys 0m35.872s
real 1m33.860s user 0m11.593s sys 3m1.169s
real 1m31.883s user 0m11.265s sys 2m59.198s
real 1m32.394s user 0m11.294s sys 3m1.616s
real 1m31.017s user 0m11.379s sys 3m1.349s
real 1m31.931s user 0m11.295s sys 2m59.863s
real 1m32.758s user 0m11.254s sys 2m59.538s
real 1m35.198s user 0m11.145s sys 3m1.123s
real 1m30.531s user 0m11.393s sys 2m58.089s
real 1m31.142s user 0m11.333s sys 3m0.549s
After:
real 0m3.489s user 0m10.943s sys 0m36.036s
real 1m10.893s user 0m11.495s sys 2m38.545s
real 1m29.129s user 0m11.382s sys 3m1.601s
real 1m29.944s user 0m11.494s sys 3m1.575s
real 1m31.208s user 0m11.451s sys 2m59.693s
real 1m25.944s user 0m11.327s sys 2m56.394s
real 1m28.599s user 0m11.312s sys 3m0.162s
real 1m26.746s user 0m11.538s sys 2m55.462s
real 1m30.668s user 0m11.475s sys 3m2.075s
real 1m29.258s user 0m11.292s sys 3m0.780s
Which is slightly faster in real time.
Signed-off-by: Kairui Song <kasong@tencent.com>
---
mm/list_lru.c | 178 +++++++++++++++++++++-----------------------------
mm/zswap.c | 7 +-
2 files changed, 77 insertions(+), 108 deletions(-)
diff --git a/mm/list_lru.c b/mm/list_lru.c
index b54f092d4d65..172b16146e15 100644
--- a/mm/list_lru.c
+++ b/mm/list_lru.c
@@ -59,6 +59,20 @@ list_lru_from_memcg_idx(struct list_lru *lru, int nid, int idx)
}
return &lru->node[nid].lru;
}
+
+static inline struct list_lru_one *
+list_lru_from_memcg(struct list_lru *lru, int nid, struct mem_cgroup *memcg)
+{
+ struct list_lru_one *l;
+again:
+ l = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(memcg));
+ if (likely(l))
+ return l;
+
+ memcg = parent_mem_cgroup(memcg);
+ VM_WARN_ON(!css_is_dying(&memcg->css));
+ goto again;
+}
#else
static void list_lru_register(struct list_lru *lru)
{
@@ -83,6 +97,12 @@ list_lru_from_memcg_idx(struct list_lru *lru, int nid, int idx)
{
return &lru->node[nid].lru;
}
+
+static inline struct list_lru_one *
+list_lru_from_memcg(struct list_lru *lru, int nid, int idx)
+{
+ return &lru->node[nid].lru;
+}
#endif /* CONFIG_MEMCG */
/* The caller must ensure the memcg lifetime. */
@@ -94,7 +114,7 @@ bool list_lru_add(struct list_lru *lru, struct list_head *item, int nid,
spin_lock(&nlru->lock);
if (list_empty(item)) {
- l = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(memcg));
+ l = list_lru_from_memcg(lru, nid, memcg);
list_add_tail(item, &l->list);
/* Set shrinker bit if the first element was added */
if (!l->nr_items++)
@@ -133,7 +153,7 @@ bool list_lru_del(struct list_lru *lru, struct list_head *item, int nid,
spin_lock(&nlru->lock);
if (!list_empty(item)) {
- l = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(memcg));
+ l = list_lru_from_memcg(lru, nid, memcg);
list_del_init(item);
l->nr_items--;
nlru->nr_items--;
@@ -355,20 +375,6 @@ static struct list_lru_memcg *memcg_init_list_lru_one(gfp_t gfp)
return mlru;
}
-static void memcg_list_lru_free(struct list_lru *lru, int src_idx)
-{
- struct list_lru_memcg *mlru = xa_erase_irq(&lru->xa, src_idx);
-
- /*
- * The __list_lru_walk_one() can walk the list of this node.
- * We need kvfree_rcu() here. And the walking of the list
- * is under lru->node[nid]->lock, which can serve as a RCU
- * read-side critical section.
- */
- if (mlru)
- kvfree_rcu(mlru, rcu);
-}
-
static inline void memcg_init_list_lru(struct list_lru *lru, bool memcg_aware)
{
if (memcg_aware)
@@ -393,22 +399,18 @@ static void memcg_destroy_list_lru(struct list_lru *lru)
}
static void memcg_reparent_list_lru_node(struct list_lru *lru, int nid,
- int src_idx, struct mem_cgroup *dst_memcg)
+ struct list_lru_one *src,
+ struct mem_cgroup *dst_memcg)
{
struct list_lru_node *nlru = &lru->node[nid];
- int dst_idx = dst_memcg->kmemcg_id;
- struct list_lru_one *src, *dst;
+ struct list_lru_one *dst;
/*
* Since list_lru_{add,del} may be called under an IRQ-safe lock,
* we have to use IRQ-safe primitives here to avoid deadlock.
*/
spin_lock_irq(&nlru->lock);
-
- src = list_lru_from_memcg_idx(lru, nid, src_idx);
- if (!src)
- goto out;
- dst = list_lru_from_memcg_idx(lru, nid, dst_idx);
+ dst = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(dst_memcg));
list_splice_init(&src->list, &dst->list);
@@ -417,46 +419,43 @@ static void memcg_reparent_list_lru_node(struct list_lru *lru, int nid,
set_shrinker_bit(dst_memcg, nid, lru_shrinker_id(lru));
src->nr_items = 0;
}
-out:
spin_unlock_irq(&nlru->lock);
}
void memcg_reparent_list_lrus(struct mem_cgroup *memcg, struct mem_cgroup *parent)
{
- struct cgroup_subsys_state *css;
struct list_lru *lru;
- int src_idx = memcg->kmemcg_id, i;
-
- /*
- * Change kmemcg_id of this cgroup and all its descendants to the
- * parent's id, and then move all entries from this cgroup's list_lrus
- * to ones of the parent.
- */
- rcu_read_lock();
- css_for_each_descendant_pre(css, &memcg->css) {
- struct mem_cgroup *child;
-
- child = mem_cgroup_from_css(css);
- WRITE_ONCE(child->kmemcg_id, parent->kmemcg_id);
- }
- rcu_read_unlock();
+ int i;
- /*
- * With kmemcg_id set to parent, holding the lock of each list_lru_node
- * below can prevent list_lru_{add,del,isolate} from touching the lru,
- * safe to reparent.
- */
mutex_lock(&list_lrus_mutex);
list_for_each_entry(lru, &memcg_list_lrus, list) {
+ struct list_lru_memcg *mlru;
+ XA_STATE(xas, &lru->xa, memcg->kmemcg_id);
+
+ /*
+ * Lock the Xarray to ensure no on going list_lru_memcg
+ * allocation and further allocation will see css_is_dying().
+ */
+ xas_lock_irq(&xas);
+ mlru = xas_store(&xas, NULL);
+ xas_unlock_irq(&xas);
+ if (!mlru)
+ continue;
+
+ /*
+ * With Xarray value set to NULL, holding the lru lock below
+ * prevents list_lru_{add,del,isolate} from touching the lru,
+ * safe to reparent.
+ */
for_each_node(i)
- memcg_reparent_list_lru_node(lru, i, src_idx, parent);
+ memcg_reparent_list_lru_node(lru, i, &mlru->node[i], parent);
/*
* Here all list_lrus corresponding to the cgroup are guaranteed
* to remain empty, we can safely free this lru, any further
* memcg_list_lru_alloc() call will simply bail out.
*/
- memcg_list_lru_free(lru, src_idx);
+ kvfree_rcu(mlru, rcu);
}
mutex_unlock(&list_lrus_mutex);
}
@@ -472,77 +471,48 @@ static inline bool memcg_list_lru_allocated(struct mem_cgroup *memcg,
int memcg_list_lru_alloc(struct mem_cgroup *memcg, struct list_lru *lru,
gfp_t gfp)
{
- int i;
unsigned long flags;
- struct list_lru_memcg_table {
- struct list_lru_memcg *mlru;
- struct mem_cgroup *memcg;
- } *table;
+ struct list_lru_memcg *mlru;
+ struct mem_cgroup *pos, *parent;
XA_STATE(xas, &lru->xa, 0);
if (!list_lru_memcg_aware(lru) || memcg_list_lru_allocated(memcg, lru))
return 0;
gfp &= GFP_RECLAIM_MASK;
- table = kmalloc_array(memcg->css.cgroup->level, sizeof(*table), gfp);
- if (!table)
- return -ENOMEM;
-
/*
* Because the list_lru can be reparented to the parent cgroup's
* list_lru, we should make sure that this cgroup and all its
* ancestors have allocated list_lru_memcg.
*/
- for (i = 0; memcg; memcg = parent_mem_cgroup(memcg), i++) {
- if (memcg_list_lru_allocated(memcg, lru))
- break;
-
- table[i].memcg = memcg;
- table[i].mlru = memcg_init_list_lru_one(gfp);
- if (!table[i].mlru) {
- while (i--)
- kfree(table[i].mlru);
- kfree(table);
- return -ENOMEM;
+ do {
+ /*
+ * Keep finding the farest parent that wasn't populated
+ * until found memcg itself.
+ */
+ pos = memcg;
+ parent = parent_mem_cgroup(pos);
+ while (!memcg_list_lru_allocated(parent, lru)) {
+ pos = parent;
+ parent = parent_mem_cgroup(pos);
}
- }
-
- xas_lock_irqsave(&xas, flags);
- while (i--) {
- int index = READ_ONCE(table[i].memcg->kmemcg_id);
- struct list_lru_memcg *mlru = table[i].mlru;
- xas_set(&xas, index);
-retry:
- if (unlikely(index < 0 || xas_error(&xas) || xas_load(&xas))) {
- kfree(mlru);
- } else {
- xas_store(&xas, mlru);
- if (xas_error(&xas) == -ENOMEM) {
- xas_unlock_irqrestore(&xas, flags);
- if (xas_nomem(&xas, gfp))
- xas_set_err(&xas, 0);
- xas_lock_irqsave(&xas, flags);
- /*
- * The xas lock has been released, this memcg
- * can be reparented before us. So reload
- * memcg id. More details see the comments
- * in memcg_reparent_list_lrus().
- */
- index = READ_ONCE(table[i].memcg->kmemcg_id);
- if (index < 0)
- xas_set_err(&xas, 0);
- else if (!xas_error(&xas) && index != xas.xa_index)
- xas_set(&xas, index);
- goto retry;
+ mlru = memcg_init_list_lru_one(gfp);
+ if (!mlru)
+ return -ENOMEM;
+ xas_set(&xas, pos->kmemcg_id);
+ do {
+ xas_lock_irqsave(&xas, flags);
+ if (!xas_load(&xas) && !css_is_dying(&pos->css)) {
+ xas_store(&xas, mlru);
+ if (!xas_error(&xas))
+ mlru = NULL;
}
- }
- }
- /* xas_nomem() is used to free memory instead of memory allocation. */
- if (xas.xa_alloc)
- xas_nomem(&xas, gfp);
- xas_unlock_irqrestore(&xas, flags);
- kfree(table);
+ xas_unlock_irqrestore(&xas, flags);
+ } while (xas_nomem(&xas, gfp));
+ if (mlru)
+ kfree(mlru);
+ } while (pos != memcg && !css_is_dying(&pos->css));
return xas_error(&xas);
}
diff --git a/mm/zswap.c b/mm/zswap.c
index 162013952074..6910c37cb8ec 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -703,12 +703,11 @@ static void zswap_lru_add(struct list_lru *list_lru, struct zswap_entry *entry)
/*
* Note that it is safe to use rcu_read_lock() here, even in the face of
- * concurrent memcg offlining. Thanks to the memcg->kmemcg_id indirection
- * used in list_lru lookup, only two scenarios are possible:
+ * concurrent memcg offlining:
*
- * 1. list_lru_add() is called before memcg->kmemcg_id is updated. The
+ * 1. list_lru_add() is called before list_lru_memcg is erased. The
* new entry will be reparented to memcg's parent's list_lru.
- * 2. list_lru_add() is called after memcg->kmemcg_id is updated. The
+ * 2. list_lru_add() is called after list_lru_memcg is erased. The
* new entry will be added directly to memcg's parent's list_lru.
*
* Similar reasoning holds for list_lru_del().
--
2.47.0
next prev parent reply other threads:[~2024-11-04 17:55 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-11-04 17:52 [PATCH v3 0/6] mm/list_lru: Split list_lru lock into per-cgroup scope Kairui Song
2024-11-04 17:52 ` [PATCH v3 1/6] mm/list_lru: don't pass unnecessary key parameters Kairui Song
2024-11-04 17:52 ` [PATCH v3 2/6] mm/list_lru: don't export list_lru_add Kairui Song
2024-11-04 17:52 ` [PATCH v3 3/6] mm/list_lru: code clean up for reparenting Kairui Song
2024-11-04 17:52 ` Kairui Song [this message]
2024-11-04 17:52 ` [PATCH v3 5/6] mm/list_lru: split the lock to per-cgroup scope Kairui Song
2024-11-04 17:52 ` [PATCH v3 6/6] mm/list_lru: Simplify the list_lru walk callback function Kairui Song
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20241104175257.60853-5-ryncsn@gmail.com \
--to=ryncsn@gmail.com \
--cc=akpm@linux-foundation.org \
--cc=hannes@cmpxchg.org \
--cc=kasong@tencent.com \
--cc=linux-mm@kvack.org \
--cc=longman@redhat.com \
--cc=mhocko@suse.com \
--cc=muchun.song@linux.dev \
--cc=roman.gushchin@linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=willy@infradead.org \
--cc=zhengqi.arch@bytedance.com \
--cc=zhouchengming@bytedance.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox