* [PATCH v4 1/3] block: introduce bdev_flush_by_dev()
2026-09-14 10:31 [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Julian Sun
@ 2026-09-14 10:31 ` Julian Sun
2026-09-14 10:31 ` [PATCH v4 2/3] memcg,writeback: flush foreign bdev mappings separately from owner wbs Julian Sun
` (2 subsequent siblings)
3 siblings, 0 replies; 7+ messages in thread
From: Julian Sun @ 2026-09-14 10:31 UTC (permalink / raw)
To: linux-block, linux-fsdevel
Cc: axboe, hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
willy, jack, rostedt, tj, akpm
Add bdev_flush_by_dev() to submit page-cache writeback for a device
identified by dev_t without requiring callers to hold a persistent
device reference.
A subsequent patch will use this helper to flush foreign bdev mappings.
Signed-off-by: Julian Sun <sunjunchao@bytedance.com>
---
block/bdev.c | 24 ++++++++++++++++++++++++
include/linux/blkdev.h | 1 +
2 files changed, 25 insertions(+)
diff --git a/block/bdev.c b/block/bdev.c
index cd8323083740..da1fc56926fd 100644
--- a/block/bdev.c
+++ b/block/bdev.c
@@ -264,6 +264,30 @@ int sync_blockdev_nowait(struct block_device *bdev)
}
EXPORT_SYMBOL_GPL(sync_blockdev_nowait);
+/**
+ * bdev_flush_by_dev - attempt writeback of a block device's page cache
+ * @dev: target block device number
+ *
+ * Skip closed devices or devices whose open_mutex is busy.
+ * May sleep while submitting writeback; does not wait for all I/O to complete.
+ */
+void bdev_flush_by_dev(dev_t dev)
+{
+ struct block_device *bdev;
+
+ bdev = blkdev_get_no_open(dev, false);
+ if (!bdev)
+ return;
+
+ /* Prevent the device from closing between the check and writeback. */
+ if (mutex_trylock(&bdev->bd_disk->open_mutex)) {
+ if (atomic_read(&bdev->bd_openers))
+ sync_blockdev_nowait(bdev);
+ mutex_unlock(&bdev->bd_disk->open_mutex);
+ }
+ blkdev_put_no_open(bdev);
+}
+
/*
* Write out and wait upon all the dirty data associated with a block
* device via its mapping. Does not take the superblock lock.
diff --git a/include/linux/blkdev.h b/include/linux/blkdev.h
index 4f7905c3412b..7a5f3e2f17d6 100644
--- a/include/linux/blkdev.h
+++ b/include/linux/blkdev.h
@@ -1710,6 +1710,7 @@ void invalidate_bdev(struct block_device *bdev);
int sync_blockdev(struct block_device *bdev);
int sync_blockdev_range(struct block_device *bdev, loff_t lstart, loff_t lend);
int sync_blockdev_nowait(struct block_device *bdev);
+void bdev_flush_by_dev(dev_t dev);
void sync_bdevs(bool wait);
void bdev_statx(const struct path *path, struct kstat *stat, u32 request_mask);
void printk_all_partitions(void);
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH v4 2/3] memcg,writeback: flush foreign bdev mappings separately from owner wbs
2026-09-14 10:31 [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Julian Sun
2026-09-14 10:31 ` [PATCH v4 1/3] block: introduce bdev_flush_by_dev() Julian Sun
@ 2026-09-14 10:31 ` Julian Sun
2026-09-14 10:31 ` [PATCH v4 3/3] writeback: record bdev targets in foreign writeback tracepoints Julian Sun
2026-09-15 3:59 ` [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Andrew Morton
3 siblings, 0 replies; 7+ messages in thread
From: Julian Sun @ 2026-09-14 10:31 UTC (permalink / raw)
To: linux-block, linux-fsdevel
Cc: axboe, hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
willy, jack, rostedt, tj, akpm
Bdev inodes are routinely shared by multiple memcgs. Recording their
owner wb as foreign can flush unrelated file data when a source memcg
enters dirty throttling.
Record bdev device numbers separately and queue best-effort work on
memcg_bdev_frn_flusher to write only their mappings. Keep the existing
foreign-wb mechanism for non-bdev inodes.
Bdev foreign flushes can be triggered frequently. Use a dedicated
workqueue to avoid interfering with tasks on existing workqueues. If
allocation fails, retain the existing foreign-wb path.
Use fixed per-memcg slots, with a shared lock protecting device records
and in-flight state. Tracking is best effort: record only dev_t without
holding device or inode references. Skip closed devices or devices whose
open_mutex is busy. Device removal and device-number reuse may race with
lookup.
Fixes: 97b27821b485 ("writeback, memcg: Implement foreign dirty flushing")
Suggested-by: Jan Kara <jack@suse.cz>
Signed-off-by: Julian Sun <sunjunchao@bytedance.com>
---
include/linux/memcontrol.h | 15 ++++++
mm/memcontrol.c | 106 ++++++++++++++++++++++++++++++++++---
2 files changed, 113 insertions(+), 8 deletions(-)
diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
index 7d1c0ce189a8..d44d3ce51b50 100644
--- a/include/linux/memcontrol.h
+++ b/include/linux/memcontrol.h
@@ -176,6 +176,18 @@ struct memcg_cgwb_frn {
struct wb_completion done; /* tracks in-flight foreign writebacks */
};
+/*
+ * frn_lock protects dev and inflight.
+ * No extra memcg reference is taken: this work is embedded in the memcg,
+ * and mem_cgroup_css_free() waits for it to finish before freeing the memcg.
+ */
+struct bdev_frn_flush_ctx {
+ struct work_struct work;
+ dev_t dev;
+ bool inflight;
+ struct mem_cgroup *memcg;
+};
+
/*
* Bucket for arbitrarily byte-sized objects charged to a memory
* cgroup. The bucket can be reparented in one piece when the cgroup
@@ -279,6 +291,9 @@ struct mem_cgroup {
#ifdef CONFIG_CGROUP_WRITEBACK
struct wb_domain cgwb_domain;
struct memcg_cgwb_frn cgwb_frn[MEMCG_CGWB_FRN_CNT];
+ struct bdev_frn_flush_ctx bdev_frn[MEMCG_CGWB_FRN_CNT];
+ /* Nests inside mapping->i_pages. */
+ spinlock_t frn_lock;
#endif
#ifdef CONFIG_LRU_GEN_WALKS_MMU
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 1271d390b617..aac8c893d7d2 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -39,6 +39,7 @@
#include <linux/smp.h>
#include <linux/page-flags.h>
#include <linux/backing-dev.h>
+#include <linux/blkdev.h>
#include <linux/bit_spinlock.h>
#include <linux/rcupdate.h>
#include <linux/limits.h>
@@ -104,6 +105,7 @@ static struct kmem_cache *memcg_pn_cachep;
#ifdef CONFIG_CGROUP_WRITEBACK
static DECLARE_WAIT_QUEUE_HEAD(memcg_cgwb_frn_waitq);
+static struct workqueue_struct *memcg_bdev_frn_flusher __ro_after_init;
#endif
static inline bool task_is_dying(void)
@@ -3845,6 +3847,41 @@ void mem_cgroup_wb_stats(struct bdi_writeback *wb, unsigned long *pfilepages,
}
}
+/*
+ * Bdev inodes are commonly shared by many memcgs. Flushing their owner wb
+ * can write unrelated file data, so record device numbers separately and
+ * flush only the bdev mappings. Tracking is best effort: record only dev_t
+ * without holding device or inode references, so it may race with device
+ * removal and re-addition.
+ */
+static void mem_cgroup_track_foreign_bdev(struct mem_cgroup *memcg, dev_t dev)
+{
+ struct bdev_frn_flush_ctx *ctx;
+ unsigned long flags;
+ int i;
+
+ /*
+ * __folio_mark_dirty() takes mapping->i_pages with xa_lock_irqsave()
+ * before reaching this helper. Use irqsave here as well so frn_lock's
+ * IRQ protection does not depend on that outer locking.
+ */
+ spin_lock_irqsave(&memcg->frn_lock, flags);
+ for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++) {
+ if (memcg->bdev_frn[i].dev == dev)
+ goto out;
+ }
+
+ for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++) {
+ ctx = &memcg->bdev_frn[i];
+ if (ctx->inflight)
+ continue;
+ ctx->dev = dev;
+ break;
+ }
+out:
+ spin_unlock_irqrestore(&memcg->frn_lock, flags);
+}
+
/*
* Foreign dirty flushing
*
@@ -3875,17 +3912,17 @@ void mem_cgroup_wb_stats(struct bdi_writeback *wb, unsigned long *pfilepages,
* most recent foreign dirtying events and initiating remote flushes on
* them when local writeback isn't enough to keep the memory clean enough.
*
- * The following two functions implement such mechanism. When a foreign
- * page - a page whose memcg and writeback ownerships don't match - is
- * dirtied, mem_cgroup_track_foreign_dirty() records the inode owning
- * bdi_writeback on the page owning memcg. When balance_dirty_pages()
- * decides that the memcg needs to sleep due to high dirty ratio, it calls
+ * For non-bdev inodes, when a foreign page - a page whose memcg and
+ * writeback ownerships don't match - is dirtied,
+ * mem_cgroup_track_foreign_dirty() records the inode owning bdi_writeback
+ * on the page owning memcg. When balance_dirty_pages() decides that the
+ * memcg needs to sleep due to high dirty ratio, it calls
* mem_cgroup_flush_foreign() which queues writeback on the recorded
* foreign bdi_writebacks which haven't expired. Both the numbers of
* recorded bdi_writebacks and concurrent in-flight foreign writebacks are
* limited to MEMCG_CGWB_FRN_CNT.
*
- * The mechanism only remembers IDs and doesn't hold any object references.
+ * These wb records only remember IDs and don't hold any object references.
* As being wrong occasionally doesn't matter, updates and accesses to the
* records are lockless and racy.
*/
@@ -3898,9 +3935,17 @@ void mem_cgroup_track_foreign_dirty_slowpath(struct folio *folio,
u64 oldest_at = now;
int oldest = -1;
int i;
+ struct address_space *mapping = folio_mapping(folio);
+ struct inode *bdev_inode = mapping ? mapping->host : NULL;
trace_track_foreign_dirty(folio, wb);
+ if (memcg_bdev_frn_flusher && bdev_inode &&
+ sb_is_blkdev_sb(bdev_inode->i_sb)) {
+ mem_cgroup_track_foreign_bdev(memcg, bdev_inode->i_rdev);
+ return;
+ }
+
/*
* Pick the slot to use. If there is already a slot for @wb, keep
* using it. If not replace the oldest one which isn't being
@@ -3941,6 +3986,25 @@ void mem_cgroup_track_foreign_dirty_slowpath(struct folio *folio,
}
}
+static void bdev_frn_flush_work(struct work_struct *work)
+{
+ struct bdev_frn_flush_ctx *ctx =
+ container_of(work, struct bdev_frn_flush_ctx, work);
+ unsigned long flags;
+
+ bdev_flush_by_dev(ctx->dev);
+
+ /*
+ * The dirty tracking path takes frn_lock while holding mapping->i_pages.
+ * Disable local IRQs here to avoid deadlocks with I/O completion
+ * handlers that also take mapping->i_pages.
+ */
+ spin_lock_irqsave(&ctx->memcg->frn_lock, flags);
+ ctx->inflight = false;
+ ctx->dev = 0;
+ spin_unlock_irqrestore(&ctx->memcg->frn_lock, flags);
+}
+
/* issue foreign writeback flushes for recorded foreign dirtying events */
void mem_cgroup_flush_foreign(struct bdi_writeback *wb)
{
@@ -3949,6 +4013,18 @@ void mem_cgroup_flush_foreign(struct bdi_writeback *wb)
u64 now = jiffies_64;
int i;
+ for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++) {
+ struct bdev_frn_flush_ctx *ctx = &memcg->bdev_frn[i];
+ unsigned long flags;
+
+ spin_lock_irqsave(&memcg->frn_lock, flags);
+ if (!ctx->inflight && ctx->dev) {
+ ctx->inflight = true;
+ queue_work(memcg_bdev_frn_flusher, &ctx->work);
+ }
+ spin_unlock_irqrestore(&memcg->frn_lock, flags);
+ }
+
for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++) {
struct memcg_cgwb_frn *frn = &memcg->cgwb_frn[i];
@@ -4202,9 +4278,15 @@ static struct mem_cgroup *mem_cgroup_alloc(struct mem_cgroup *parent)
memcg->kmemcg_id = -1;
#ifdef CONFIG_CGROUP_WRITEBACK
INIT_LIST_HEAD(&memcg->cgwb_list);
- for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++)
+ for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++) {
+ struct bdev_frn_flush_ctx *ctx = &memcg->bdev_frn[i];
+
memcg->cgwb_frn[i].done =
__WB_COMPLETION_INIT(&memcg_cgwb_frn_waitq);
+ INIT_WORK(&ctx->work, bdev_frn_flush_work);
+ ctx->memcg = memcg;
+ }
+ spin_lock_init(&memcg->frn_lock);
#endif
lru_gen_init_memcg(memcg);
return memcg;
@@ -4386,8 +4468,10 @@ static void mem_cgroup_css_free(struct cgroup_subsys_state *css)
int __maybe_unused i;
#ifdef CONFIG_CGROUP_WRITEBACK
- for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++)
+ for (i = 0; i < MEMCG_CGWB_FRN_CNT; i++) {
wb_wait_for_completion(&memcg->cgwb_frn[i].done);
+ flush_work(&memcg->bdev_frn[i].work);
+ }
#endif
if (cgroup_subsys_on_dfl(memory_cgrp_subsys) && !cgroup_memory_nosocket)
static_branch_dec(&memcg_sockets_enabled_key);
@@ -5703,6 +5787,12 @@ int __init mem_cgroup_init(void)
memcg_wq = alloc_workqueue("memcg", WQ_PERCPU, 0);
WARN_ON(!memcg_wq);
+#ifdef CONFIG_CGROUP_WRITEBACK
+ memcg_bdev_frn_flusher = alloc_workqueue("memcg_bdev_frn_flusher",
+ WQ_UNBOUND | WQ_MEM_RECLAIM, 0);
+ WARN_ON(!memcg_bdev_frn_flusher);
+#endif
+
for_each_possible_cpu(cpu) {
INIT_WORK(&per_cpu_ptr(&memcg_stock, cpu)->work,
drain_local_memcg_stock);
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH v4 3/3] writeback: record bdev targets in foreign writeback tracepoints
2026-09-14 10:31 [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Julian Sun
2026-09-14 10:31 ` [PATCH v4 1/3] block: introduce bdev_flush_by_dev() Julian Sun
2026-09-14 10:31 ` [PATCH v4 2/3] memcg,writeback: flush foreign bdev mappings separately from owner wbs Julian Sun
@ 2026-09-14 10:31 ` Julian Sun
2026-09-15 3:59 ` [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Andrew Morton
3 siblings, 0 replies; 7+ messages in thread
From: Julian Sun @ 2026-09-14 10:31 UTC (permalink / raw)
To: linux-block, linux-fsdevel
Cc: axboe, hannes, mhocko, roman.gushchin, shakeel.butt, muchun.song,
willy, jack, rostedt, tj, akpm
Add dev to track_foreign_dirty and flush_foreign so traces can
distinguish foreign-wb tracking from the bdev-only path. Append the
field to each event and print it as major:minor; zero denotes the
normal foreign-wb path.
Signed-off-by: Julian Sun <sunjunchao@bytedance.com>
---
include/trace/events/writeback.h | 22 ++++++++++++++--------
mm/memcontrol.c | 9 ++++++---
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/include/trace/events/writeback.h b/include/trace/events/writeback.h
index 13ee076ccd16..ffd1b8df4231 100644
--- a/include/trace/events/writeback.h
+++ b/include/trace/events/writeback.h
@@ -273,9 +273,9 @@ TRACE_EVENT(inode_switch_wbs,
TRACE_EVENT(track_foreign_dirty,
- TP_PROTO(struct folio *folio, struct bdi_writeback *wb),
+ TP_PROTO(struct folio *folio, struct bdi_writeback *wb, dev_t dev),
- TP_ARGS(folio, wb),
+ TP_ARGS(folio, wb, dev),
TP_STRUCT__entry(
__array(char, name, 32)
@@ -284,6 +284,7 @@ TRACE_EVENT(track_foreign_dirty,
__field(u64, cgroup_ino)
__field(u64, page_cgroup_ino)
__field(unsigned int, memcg_id)
+ __field(dev_t, dev)
),
TP_fast_assign(
@@ -295,34 +296,37 @@ TRACE_EVENT(track_foreign_dirty,
__entry->ino = inode ? inode->i_ino : 0;
__entry->memcg_id = wb->memcg_css->id;
__entry->cgroup_ino = __trace_wb_assign_cgroup(wb);
+ __entry->dev = dev;
rcu_read_lock();
__entry->page_cgroup_ino = cgroup_ino(folio_memcg(folio)->css.cgroup);
rcu_read_unlock();
),
- TP_printk("bdi %s[%llu]: ino=%llu memcg_id=%u cgroup_ino=%llu page_cgroup_ino=%llu",
+ TP_printk("bdi %s[%llu]: ino=%llu memcg_id=%u cgroup_ino=%llu page_cgroup_ino=%llu dev=%u:%u",
__entry->name,
__entry->bdi_id,
__entry->ino,
__entry->memcg_id,
__entry->cgroup_ino,
- __entry->page_cgroup_ino
+ __entry->page_cgroup_ino,
+ MAJOR(__entry->dev), MINOR(__entry->dev)
)
);
TRACE_EVENT(flush_foreign,
TP_PROTO(struct bdi_writeback *wb, unsigned int frn_bdi_id,
- unsigned int frn_memcg_id),
+ unsigned int frn_memcg_id, dev_t dev),
- TP_ARGS(wb, frn_bdi_id, frn_memcg_id),
+ TP_ARGS(wb, frn_bdi_id, frn_memcg_id, dev),
TP_STRUCT__entry(
__array(char, name, 32)
__field(u64, cgroup_ino)
__field(unsigned int, frn_bdi_id)
__field(unsigned int, frn_memcg_id)
+ __field(dev_t, dev)
),
TP_fast_assign(
@@ -330,13 +334,15 @@ TRACE_EVENT(flush_foreign,
__entry->cgroup_ino = __trace_wb_assign_cgroup(wb);
__entry->frn_bdi_id = frn_bdi_id;
__entry->frn_memcg_id = frn_memcg_id;
+ __entry->dev = dev;
),
- TP_printk("bdi %s: cgroup_ino=%llu frn_bdi_id=%u frn_memcg_id=%u",
+ TP_printk("bdi %s: cgroup_ino=%llu frn_bdi_id=%u frn_memcg_id=%u dev=%u:%u",
__entry->name,
__entry->cgroup_ino,
__entry->frn_bdi_id,
- __entry->frn_memcg_id
+ __entry->frn_memcg_id,
+ MAJOR(__entry->dev), MINOR(__entry->dev)
)
);
#endif
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index aac8c893d7d2..0784f6e40aec 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -3938,14 +3938,15 @@ void mem_cgroup_track_foreign_dirty_slowpath(struct folio *folio,
struct address_space *mapping = folio_mapping(folio);
struct inode *bdev_inode = mapping ? mapping->host : NULL;
- trace_track_foreign_dirty(folio, wb);
-
if (memcg_bdev_frn_flusher && bdev_inode &&
sb_is_blkdev_sb(bdev_inode->i_sb)) {
+ trace_track_foreign_dirty(folio, wb, bdev_inode->i_rdev);
mem_cgroup_track_foreign_bdev(memcg, bdev_inode->i_rdev);
return;
}
+ trace_track_foreign_dirty(folio, wb, 0);
+
/*
* Pick the slot to use. If there is already a slot for @wb, keep
* using it. If not replace the oldest one which isn't being
@@ -4020,6 +4021,7 @@ void mem_cgroup_flush_foreign(struct bdi_writeback *wb)
spin_lock_irqsave(&memcg->frn_lock, flags);
if (!ctx->inflight && ctx->dev) {
ctx->inflight = true;
+ trace_flush_foreign(wb, 0, 0, ctx->dev);
queue_work(memcg_bdev_frn_flusher, &ctx->work);
}
spin_unlock_irqrestore(&memcg->frn_lock, flags);
@@ -4037,7 +4039,8 @@ void mem_cgroup_flush_foreign(struct bdi_writeback *wb)
if (time_after64(frn->at, now - intv) &&
atomic_read(&frn->done.cnt) == 1) {
frn->at = 0;
- trace_flush_foreign(wb, frn->bdi_id, frn->memcg_id);
+ trace_flush_foreign(wb, frn->bdi_id, frn->memcg_id,
+ 0);
cgroup_writeback_by_id(frn->bdi_id, frn->memcg_id,
WB_REASON_FOREIGN_FLUSH,
&frn->done);
--
2.39.5
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately
2026-09-14 10:31 [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Julian Sun
` (2 preceding siblings ...)
2026-09-14 10:31 ` [PATCH v4 3/3] writeback: record bdev targets in foreign writeback tracepoints Julian Sun
@ 2026-09-15 3:59 ` Andrew Morton
2026-09-15 5:35 ` Julian Sun
3 siblings, 1 reply; 7+ messages in thread
From: Andrew Morton @ 2026-09-15 3:59 UTC (permalink / raw)
To: Julian Sun
Cc: linux-block, linux-fsdevel, axboe, hannes, mhocko, roman.gushchin,
shakeel.butt, muchun.song, willy, jack, rostedt, tj
On Mon, 14 Sep 2026 18:31:16 +0800 Julian Sun <sunjunchao@bytedance.com> wrote:
> Bdev inodes are routinely shared by multiple memcgs. Recording their
> owner wb as foreign can cause dirty throttling in one memcg to trigger
> writeback of unrelated file data belonging to that wb.
>
> On one production machine, bdev mapping snapshots showed as little
> as about 10 MiB of dirty data. A queue snapshot of its owner wb showed
> 599 foreign-flush works, with an average requested writeback budget of
> 43 GiB per work.
>
> In other words, a small amount of foreign dirtying in the bdev mapping
> can trigger owner-wide writeback requests, whose budgets averaged
> 43 GiB in this queue snapshot.
What is the overall runtime effect of this? IOW, can we see some
before-and-after numbers?
> Following Jan's suggestion, this series records foreign bdev targets
> separately and flushes their mappings instead of their owner wbs. This
> preserves a way for dirty throttling to initiate bdev writeback while
> avoiding owner-wide writeback triggered by these records. The existing
> foreign-wb mechanism remains unchanged for other inodes.
>
> Tracking is best effort: each memcg keeps a bounded set of device
> numbers without persistent device or inode references. Closed devices
> and devices with a busy open_mutex are skipped. Device removal and
> device-number reuse may race with lookup.
>
> Flushes run asynchronously on a dedicated workqueue to isolate these
> frequently triggered tasks from existing workqueue users. If workqueue
> allocation fails, the existing foreign-wb mechanism remains in use.
>
> Julian Sun (3):
> block: introduce bdev_flush_by_dev()
> memcg,writeback: flush foreign bdev mappings separately from owner wbs
> writeback: record bdev targets in foreign writeback tracepoints
>
> block/bdev.c | 24 +++++++
> include/linux/blkdev.h | 1 +
> include/linux/memcontrol.h | 15 ++++
> include/trace/events/writeback.h | 22 +++---
> mm/memcontrol.c | 113 ++++++++++++++++++++++++++++---
I'd consider this a memcg patch, and not only due to the line counts.
Please cc linux-mm on memcg patchsets.
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately
2026-09-15 3:59 ` [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately Andrew Morton
@ 2026-09-15 5:35 ` Julian Sun
2026-09-15 6:06 ` Andrew Morton
0 siblings, 1 reply; 7+ messages in thread
From: Julian Sun @ 2026-09-15 5:35 UTC (permalink / raw)
To: Andrew Morton
Cc: linux-block, linux-fsdevel, axboe, hannes, mhocko, roman.gushchin,
shakeel.butt, muchun.song, willy, jack, rostedt, tj, linux-mm
On 9/15/26 11:59 AM, Andrew Morton wrote:
> On Mon, 14 Sep 2026 18:31:16 +0800 Julian Sun <sunjunchao@bytedance.com> wrote:
>
>> Bdev inodes are routinely shared by multiple memcgs. Recording their
>> owner wb as foreign can cause dirty throttling in one memcg to trigger
>> writeback of unrelated file data belonging to that wb.
>>
>> On one production machine, bdev mapping snapshots showed as little
>> as about 10 MiB of dirty data. A queue snapshot of its owner wb showed
>> 599 foreign-flush works, with an average requested writeback budget of
>> 43 GiB per work.
>>
>> In other words, a small amount of foreign dirtying in the bdev mapping
>> can trigger owner-wide writeback requests, whose budgets averaged
>> 43 GiB in this queue snapshot.
>
> What is the overall runtime effect of this? IOW, can we see some
> before-and-after numbers?
I tested this series using a QEMU/KVM VM with 4 vCPUs, 8 GiB RAM,
cgroup v2 and ext4. Virtual NVMe write bandwidth was capped at
200 MiB/s.
A fio job repeatedly overwrote a preinitialized 256 MiB file using
64 KiB buffered writes, at 64 MiB/s, for 9 GiB of logical writes.
Four memcgs, each with memory.high=32 MiB and io.max=2 MiB/s, generated
metadata and buffered writes to trigger dirty throttling and foreign
flushes. Concurrently, foreground fio performed 16 GiB of sequential
direct writes using 1 MiB requests and queue depth 32. Normal periodic
writeback remained enabled.
Three comparison pairs, with alternating execution order, produced:
Run Kernel File I/O (GiB) Time (s) BW (MiB/s) p99 (ms)
1 base 7.19 114.74 142.79 1434
1 patched 1.57 88.62 184.87 480
change -78.22% -22.76% +29.47% -66.52%
2 base 5.13 103.59 158.16 1434
2 patched 1.74 88.65 184.81 497
change -66.06% -14.42% +16.85% -65.35%
3 base 6.14 109.27 149.94 1434
3 patched 1.73 88.68 184.75 480
change -71.77% -18.85% +23.22% -66.52%
File I/O is completed block I/O matching the overwritten file's extents,
including final sync, not requested writeback budgets. The other metrics
are for the foreground workload.
Across these runs, mean file write I/O decreased by 72.7%, foreground
bandwidth increased by 23.0%, and completion time decreased by 18.8%.
The baseline varied noticeably: runs with more writes to the overwritten
file also took longer to complete the foreground workload. With this
series, foreground completion time stayed within 88.62-88.68 seconds.
This is consistent with preserving overwrite coalescing and leaving more
device bandwidth available for the foreground workload.
>
>> Following Jan's suggestion, this series records foreign bdev targets
>> separately and flushes their mappings instead of their owner wbs. This
>> preserves a way for dirty throttling to initiate bdev writeback while
>> avoiding owner-wide writeback triggered by these records. The existing
>> foreign-wb mechanism remains unchanged for other inodes.
>>
>> Tracking is best effort: each memcg keeps a bounded set of device
>> numbers without persistent device or inode references. Closed devices
>> and devices with a busy open_mutex are skipped. Device removal and
>> device-number reuse may race with lookup.
>>
>> Flushes run asynchronously on a dedicated workqueue to isolate these
>> frequently triggered tasks from existing workqueue users. If workqueue
>> allocation fails, the existing foreign-wb mechanism remains in use.
>>
>> Julian Sun (3):
>> block: introduce bdev_flush_by_dev()
>> memcg,writeback: flush foreign bdev mappings separately from owner wbs
>> writeback: record bdev targets in foreign writeback tracepoints
>>
>> block/bdev.c | 24 +++++++
>> include/linux/blkdev.h | 1 +
>> include/linux/memcontrol.h | 15 ++++
>> include/trace/events/writeback.h | 22 +++---
>> mm/memcontrol.c | 113 ++++++++++++++++++++++++++++---
>
> I'd consider this a memcg patch, and not only due to the line counts.
>
> Please cc linux-mm on memcg patchsets.
Sure, will do.
Thanks,
--
Julian Sun <sunjunchao@bytedance.com>
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH v4 0/3] memcg,writeback: flush foreign bdev mappings separately
2026-09-15 5:35 ` Julian Sun
@ 2026-09-15 6:06 ` Andrew Morton
0 siblings, 0 replies; 7+ messages in thread
From: Andrew Morton @ 2026-09-15 6:06 UTC (permalink / raw)
To: Julian Sun
Cc: linux-block, linux-fsdevel, axboe, hannes, mhocko, roman.gushchin,
shakeel.butt, muchun.song, willy, jack, rostedt, tj, linux-mm
On Tue, 15 Sep 2026 13:35:18 +0800 Julian Sun <sunjunchao@bytedance.com> wrote:
> >
> > What is the overall runtime effect of this? IOW, can we see some
> > before-and-after numbers?
>
> I tested this series using a QEMU/KVM VM with 4 vCPUs, 8 GiB RAM,
> cgroup v2 and ext4. Virtual NVMe write bandwidth was capped at
> 200 MiB/s.
>
> A fio job repeatedly overwrote a preinitialized 256 MiB file using
> 64 KiB buffered writes, at 64 MiB/s, for 9 GiB of logical writes.
> Four memcgs, each with memory.high=32 MiB and io.max=2 MiB/s, generated
> metadata and buffered writes to trigger dirty throttling and foreign
> flushes. Concurrently, foreground fio performed 16 GiB of sequential
> direct writes using 1 MiB requests and queue depth 32. Normal periodic
> writeback remained enabled.
>
> Three comparison pairs, with alternating execution order, produced:
>
> Run Kernel File I/O (GiB) Time (s) BW (MiB/s) p99 (ms)
> 1 base 7.19 114.74 142.79 1434
> 1 patched 1.57 88.62 184.87 480
> change -78.22% -22.76% +29.47% -66.52%
>
> 2 base 5.13 103.59 158.16 1434
> 2 patched 1.74 88.65 184.81 497
> change -66.06% -14.42% +16.85% -65.35%
>
> 3 base 6.14 109.27 149.94 1434
> 3 patched 1.73 88.68 184.75 480
> change -71.77% -18.85% +23.22% -66.52%
>
> File I/O is completed block I/O matching the overwritten file's extents,
> including final sync, not requested writeback budgets. The other metrics
> are for the foreground workload.
>
> Across these runs, mean file write I/O decreased by 72.7%, foreground
> bandwidth increased by 23.0%, and completion time decreased by 18.8%.
Thanks. This is super-important info so please condense, summarize and
emphasize this result in the [0/N].
How real-world is this test case? I assume people won't see gains like
this in anything they normally do? I assume this is long-standing
behavior, so if misbehavior like this was regularly being observed then
we'd have fixed it by now.
> >
> > I'd consider this a memcg patch, and not only due to the line counts.
> >
> > Please cc linux-mm on memcg patchsets.
> Sure, will do.
Great. Please update that [0/N] as indicated - help readers understand
the expected real-world impacts. Then send us a v5 and I'll get to
work helping to deliver this to our users.
^ permalink raw reply [flat|nested] 7+ messages in thread