Hi Mikulas,

I understand that the original motivation for dm-writecache is for use as a write buffer to absorb write bursts quickly onto non-volatile storage, and I think there was an implicit assumption that the size of the dm-writecache doesn't tend exceed the page cache size. In this scenario, recently-written data will indeed still be in the page cache and there isn't a need for dm-writecache to serve reads.

My use case is a system with small RAM (~32 GB) with huge dm-writecache (~1TB). Here, "recently-written" means within the last few days, and the writecache is so big that "recently-written" data won't fit in the page cache. It's intended to target the usage pattern of "New data is written to disk, manipulated a few times over a few days, and then archived". A huge dm-writecache basically lets me keep the last few days of work cached.

It's possible to achieve this effect by setting the watermark levels high and keeping the cache dirty. But keeping the cache dirty for multiple days increases the risk of data loss, and setting the watermark levels high reduces the ability to absorb a large write burst if it happens. Having a "clean" state allows cleaning the cache quickly while still having the "last 3 days of work are probably cached" behaviour.

On 9/21/26 04:22, Mikulas Patocka wrote:
Hi

I deliberately didn't do this when designing dm-writecache because it is
expected that reads of recently written data will be served from the page
cache (thus, they don't reach dm-writecache).

If you have some realistic workload that is improved by this patch (i.e.
not just fio of randon direct I/O requests), I could consider accepting
it.

Mikulas


On Mon, 7 Sep 2026, Henry Wong wrote:

In the current dm-writecache, there is a trade-off in setting the watermark 
levels
because cache blocks are evicted as soon as they have been written back:
Setting it low writes back sooner and keeps more free entries to absorb a 
larger write
burst, but setting it high retains more recently-written data to increase read 
hits.

This patch adds the ability to retain clean blocks in the rbtree so cache 
blocks can
serve read hits even after writing back. This allows writing back dirty data 
early to
make space for a large write burst AND using the entire cache capacity to 
service reads
until that write burst actually arrives.

This adds a new "clean" state to a cache block, but does not modify the 
eviction policy
(still roughly FIFO), and read hits do not affect when a cache block is 
invalidated and
recycled. Read hits are still opportunistic best-effort.

When a cache block is reused for a new write, it needs to be removed from the 
rbtree
and then re-inserted for the new block. The current behaviour 
(evict_clean_blocks=1)
removes the block from the rbtree in the writeback thread as soon as writeback 
is done.
This patch adds the option of deferring the rbtree removal until the cache 
block is
popped off the free list, immediately before reuse. The "clean" state 
represents a
cache block has has been put back onto the free list, but is still in the 
rbtree.

The write_in_progress bit has been removed and rolled into a new enum of cache 
states:
   WC_INVALID:            in free list,     not in rbtree
   WC_VALID_DIRTY:        not in free list, in rbtree, write_in_progress=0
   WC_VALID_WRITING_BACK: not in free list, in rbtree, write_in_progress=1
   WC_VALID_CLEAN:        in free list,     in rbtree (New state)

Signed-off-by: Henry Wong <[email protected]>
---
  .../admin-guide/device-mapper/writecache.rst  |  19 ++-
  drivers/md/Kconfig                            |   5 +-
  drivers/md/dm-writecache.c                    | 148 ++++++++++++------
  3 files changed, 123 insertions(+), 49 deletions(-)

diff --git a/Documentation/admin-guide/device-mapper/writecache.rst 
b/Documentation/admin-guide/device-mapper/writecache.rst
index 60c16b7fd5ac..d32551d986b0 100644
--- a/Documentation/admin-guide/device-mapper/writecache.rst
+++ b/Documentation/admin-guide/device-mapper/writecache.rst
@@ -2,9 +2,11 @@
  Writecache target
  =================
-The writecache target caches writes on persistent memory or on SSD. It
-doesn't cache reads because reads are supposed to be cached in page cache
-in normal RAM.
+The writecache target caches writes on persistent memory or on SSD.
+Although writecache is mainly intended to buffer bursts of writes and then
+drain them over time to slower storage, it also has read caching effects.
+Read requests that miss the in-memory page cache but hit recently-written
+data in the writecache will be served as a cache hit from the writecache.
When the device is constructed, the first sector should be zeroed or the
  first sector should contain valid superblock from previous invocation.
@@ -73,6 +75,17 @@ Constructor parameters:
        pause_writeback n       (default: 3000)
                pause writeback if there was some write I/O redirected to
                the origin volume in the last n milliseconds
+       evict_clean_blocks 0|1  (default: 1)
+               Under the default setting (1), cache blocks are evicted
+               from the writecache immediately after they write back to
+               the origin volume. Subsequent reads of these blocks will
+               be serviced directly from the origin volume.
+
+               When set to 0, blocks that have written back (clean) stay
+               in the cache until the block needs to be reused, allowing
+               these blocks to continue serving read requests for longer.
+               This increases the read caching effect, especially for very
+               large caches, even when watermark levels are set low.
Status: diff --git a/drivers/md/Kconfig b/drivers/md/Kconfig
index df27c7d066d2..b6d6d5c37e57 100644
--- a/drivers/md/Kconfig
+++ b/drivers/md/Kconfig
@@ -374,8 +374,9 @@ config DM_WRITECACHE
           It is intended for databases or other programs that need extremely
           low commit latency.
- The writecache target doesn't cache reads because reads are supposed
-          to be cached in standard RAM.
+          The writecache target is mainly intended to buffer bursts of writes,
+          but there are opportunistic read caching effects as well (hits on
+          recently-written data).
config DM_EBS
        tristate "Emulated block size target (EXPERIMENTAL)"
diff --git a/drivers/md/dm-writecache.c b/drivers/md/dm-writecache.c
index 68e3fb300fc1..6277656a2ef1 100644
--- a/drivers/md/dm-writecache.c
+++ b/drivers/md/dm-writecache.c
@@ -78,12 +78,22 @@ struct wc_memory_superblock {
        struct wc_memory_entry entries[];
  };
+typedef enum {
+       WC_INVALID,
+       WC_VALID_DIRTY,
+       WC_VALID_WRITING_BACK,
+       WC_VALID_CLEAN
+} wc_entry_state;
+
  struct wc_entry {
        struct rb_node rb_node;
-       struct list_head lru;
+       union {
+               struct rb_node freelist_rb_node;
+               struct list_head lru;
+       };
        u32 age; // jiffies
        unsigned short wc_list_contiguous;
-       bool write_in_progress;
+       wc_entry_state state : 8;
  #ifdef DM_WRITECACHE_HANDLE_HARDWARE_ERRORS
        uint64_t original_sector;
        uint64_t seq_count;
@@ -167,6 +177,7 @@ struct dm_writecache {
        bool cleaner_set:1;
        bool metadata_only:1;
        bool pause_set:1;
+       bool evict_clean_blocks:1;
unsigned int high_wm_percent_value;
        unsigned int low_wm_percent_value;
@@ -671,12 +682,8 @@ static void writecache_insert_entry(struct dm_writecache 
*wc, struct wc_entry *i
        rb_insert_color(&ins->rb_node, &wc->tree);
        list_add(&ins->lru, &wc->lru);
        ins->age = (u32)jiffies;
-}
-
-static void writecache_unlink(struct dm_writecache *wc, struct wc_entry *e)
-{
-       list_del(&e->lru);
-       rb_erase(&e->rb_node, &wc->tree);
+       BUG_ON(ins->state != WC_INVALID);
+       ins->state = WC_VALID_DIRTY;
  }
static void writecache_add_to_freelist(struct dm_writecache *wc, struct wc_entry *e)
@@ -688,13 +695,13 @@ static void writecache_add_to_freelist(struct 
dm_writecache *wc, struct wc_entry
                        wc->current_free = e;
                while (*node) {
                        parent = *node;
-                       if (&e->rb_node < *node)
+                       if (&e->freelist_rb_node < *node)
                                node = &parent->rb_left;
                        else
                                node = &parent->rb_right;
                }
-               rb_link_node(&e->rb_node, parent, node);
-               rb_insert_color(&e->rb_node, &wc->freetree);
+               rb_link_node(&e->freelist_rb_node, parent, node);
+               rb_insert_color(&e->freelist_rb_node, &wc->freetree);
        } else {
                list_add_tail(&e->lru, &wc->freelist);
        }
@@ -717,6 +724,31 @@ static void writecache_max_age_timer(struct timer_list *t)
        }
  }
+static void writecache_clean_entry(struct dm_writecache *wc, struct wc_entry *e)
+{
+       // Put entry on free list to allow reuse, but leave it in the rb tree 
to allow read hits on clean blocks
+       BUG_ON(e->state != WC_VALID_DIRTY && e->state != WC_VALID_WRITING_BACK);
+       e->state = WC_VALID_CLEAN;
+       list_del(&e->lru);
+       writecache_add_to_freelist(wc, e);
+       clear_seq_count(wc, e);
+       writecache_flush_region(wc, memory_entry(wc, e), sizeof(struct 
wc_memory_entry));
+       if (unlikely(waitqueue_active(&wc->freelist_wait)))
+               wake_up(&wc->freelist_wait);
+}
+
+static void writecache_invalidate_entry(struct dm_writecache *wc, struct 
wc_entry *e)
+{
+       // Make a clean or invalid entry invalid.
+       // - Clean:   Remove it from the rb tree in preparation for reusing the 
block.
+       // - Invalid: Do nothing, it is already invalid.
+       if (e->state == WC_VALID_CLEAN)
+               rb_erase(&e->rb_node, &wc->tree);
+       else
+               BUG_ON(e->state != WC_INVALID);
+       e->state = WC_INVALID;
+}
+
  static struct wc_entry *writecache_pop_from_freelist(struct dm_writecache 
*wc, sector_t expected_sector)
  {
        struct wc_entry *e;
@@ -729,11 +761,11 @@ static struct wc_entry 
*writecache_pop_from_freelist(struct dm_writecache *wc, s
                e = wc->current_free;
                if (expected_sector != (sector_t)-1 && 
unlikely(cache_sector(wc, e) != expected_sector))
                        return NULL;
-               next = rb_next(&e->rb_node);
-               rb_erase(&e->rb_node, &wc->freetree);
+               next = rb_next(&e->freelist_rb_node);
+               rb_erase(&e->freelist_rb_node, &wc->freetree);
                if (unlikely(!next))
                        next = rb_first(&wc->freetree);
-               wc->current_free = next ? container_of(next, struct wc_entry, 
rb_node) : NULL;
+               wc->current_free = next ? container_of(next, struct wc_entry, 
freelist_rb_node) : NULL;
        } else {
                if (unlikely(list_empty(&wc->freelist)))
                        return NULL;
@@ -743,6 +775,7 @@ static struct wc_entry *writecache_pop_from_freelist(struct 
dm_writecache *wc, s
                list_del(&e->lru);
        }
        wc->freelist_size--;
+       writecache_invalidate_entry(wc, e); // Remove clean block from rb tree 
if it is still there.
writecache_verify_watermark(wc); @@ -751,12 +784,8 @@ static struct wc_entry *writecache_pop_from_freelist(struct dm_writecache *wc, s static void writecache_free_entry(struct dm_writecache *wc, struct wc_entry *e)
  {
-       writecache_unlink(wc, e);
-       writecache_add_to_freelist(wc, e);
-       clear_seq_count(wc, e);
-       writecache_flush_region(wc, memory_entry(wc, e), sizeof(struct 
wc_memory_entry));
-       if (unlikely(waitqueue_active(&wc->freelist_wait)))
-               wake_up(&wc->freelist_wait);
+       writecache_clean_entry(wc, e);
+       writecache_invalidate_entry(wc, e);
  }
static void writecache_wait_on_freelist(struct dm_writecache *wc)
@@ -791,7 +820,7 @@ static void writecache_flush_entry(struct dm_writecache 
*wc, struct wc_entry *e)
static bool writecache_entry_is_committed(struct dm_writecache *wc, struct wc_entry *e)
  {
-       return read_seq_count(wc, e) < wc->seq_count;
+       return e->state == WC_VALID_CLEAN || read_seq_count(wc, e) < 
wc->seq_count;
  }
static void writecache_flush(struct dm_writecache *wc)
@@ -842,10 +871,13 @@ static void writecache_flush(struct dm_writecache *wc)
if (rb_node) {
                        e2 = container_of(rb_node, struct wc_entry, rb_node);
-                       if (read_original_sector(wc, e2) == read_original_sector(wc, e) 
&&
-                           likely(!e2->write_in_progress)) {
-                               writecache_free_entry(wc, e2);
-                               need_flush_after_free = true;
+                       if (read_original_sector(wc, e2) == 
read_original_sector(wc, e)) {
+                               if (likely(e2->state == WC_VALID_DIRTY)) {
+                                       writecache_free_entry(wc, e2);
+                                       need_flush_after_free = true;
+                               } else if (likely(e2->state == WC_VALID_CLEAN)) 
{
+                                       writecache_invalidate_entry(wc, e2); // 
invalidate doesn't update metadata, no flush needed.
+                               }
                        }
                }
                if (unlikely(e->lru.prev == &wc->lru))
@@ -893,7 +925,9 @@ static void writecache_discard(struct dm_writecache *wc, 
sector_t start, sector_
        while (read_original_sector(wc, e) < end) {
                struct rb_node *node = rb_next(&e->rb_node);
- if (likely(!e->write_in_progress)) {
+               if (likely(e->state == WC_VALID_CLEAN)) {
+                       writecache_invalidate_entry(wc, e);
+               } else if (likely(e->state == WC_VALID_DIRTY)) {
                        if (!discarded_something) {
                                if (!WC_MODE_PMEM(wc)) {
                                        writecache_wait_for_ios(wc, READ);
@@ -971,7 +1005,7 @@ static int writecache_alloc_entries(struct dm_writecache 
*wc)
        for (b = 0; b < wc->n_blocks; b++) {
                struct wc_entry *e = &wc->entries[b];
- e->write_in_progress = false;
+               e->state = WC_INVALID;
                cond_resched();
        }
@@ -1065,6 +1099,7 @@ static void writecache_resume(struct dm_target *ti)
  #endif
        for (b = 0; b < wc->n_blocks; b++) {
                struct wc_entry *e = &wc->entries[b];
+               e->state = WC_INVALID;
if (!writecache_entry_is_committed(wc, e)) {
                        if (read_seq_count(wc, e) != -1) {
@@ -1410,7 +1445,7 @@ static void writecache_bio_copy_ssd(struct dm_writecache 
*wc, struct bio *bio,
                        if (read_original_sector(wc, f) !=
                            read_original_sector(wc, e) + (wc->block_size >> 
SECTOR_SHIFT))
                                break;
-                       if (unlikely(f->write_in_progress))
+                       if (unlikely(f->state != WC_VALID_DIRTY))
                                break;
                        if (writecache_entry_is_committed(wc, f))
                                wc->overwrote_committed = true;
@@ -1448,20 +1483,24 @@ static enum wc_map_op writecache_map_write(struct 
dm_writecache *wc, struct bio
                        return WC_MAP_ERROR;
                }
                e = writecache_find_entry(wc, bio->bi_iter.bi_sector, 0);
-               if (e) {
+               if (e && (e->state != WC_VALID_CLEAN)) {
                        if (!writecache_entry_is_committed(wc, e)) {
                                wc->stats.write_hits_uncommitted++;
                                search_used = true;
                                goto bio_copy;
                        }
                        wc->stats.write_hits_committed++;
-                       if (!WC_MODE_PMEM(wc) && !e->write_in_progress) {
+                       if (!WC_MODE_PMEM(wc) && e->state != 
WC_VALID_WRITING_BACK) {
                                wc->overwrote_committed = true;
                                search_used = true;
                                goto bio_copy;
                        }
                        found_entry = true;
                } else {
+                       if (e && e->state == WC_VALID_CLEAN) {
+                               // Write hits a clean block. Drop the clean 
block and treat it as a miss.
+                               writecache_invalidate_entry(wc, e);
+                       }
                        if (unlikely(wc->cleaner) ||
                            (wc->metadata_only && !(bio->bi_opf & REQ_META)))
                                goto direct_write;
@@ -1690,11 +1729,17 @@ static void __writecache_endio_pmem(struct 
dm_writecache *wc, struct list_head *
                i = 0;
                do {
                        e = wb->wc_list[i];
-                       BUG_ON(!e->write_in_progress);
-                       e->write_in_progress = false;
+                       BUG_ON(e->state != WC_VALID_WRITING_BACK);
                        INIT_LIST_HEAD(&e->lru);
-                       if (!writecache_has_error(wc))
-                               writecache_free_entry(wc, e);
+
+                       if (likely(!writecache_has_error(wc))) {
+                               writecache_clean_entry(wc, e);
+                               // Invalidate clean blocks now, or defer until 
block leaves the free list.
+                               if (wc->evict_clean_blocks)
+                                       writecache_invalidate_entry(wc, e);
+                       } else {
+                               e->state = WC_VALID_DIRTY;
+                       }
                        BUG_ON(!wc->writeback_size);
                        wc->writeback_size--;
                        n_walked++;
@@ -1726,12 +1771,16 @@ static void __writecache_endio_ssd(struct dm_writecache 
*wc, struct list_head *l
e = c->e;
                do {
-                       BUG_ON(!e->write_in_progress);
-                       e->write_in_progress = false;
+                       BUG_ON(e->state != WC_VALID_WRITING_BACK);
                        INIT_LIST_HEAD(&e->lru);
-                       if (!writecache_has_error(wc))
-                               writecache_free_entry(wc, e);
-
+                       if (likely(!writecache_has_error(wc))) {
+                               writecache_clean_entry(wc, e);
+                               // Invalidate clean blocks now, or defer until 
block leaves the free list.
+                               if (wc->evict_clean_blocks)
+                                       writecache_invalidate_entry(wc, e);
+                       } else {
+                               e->state = WC_VALID_DIRTY;
+                       }
                        BUG_ON(!wc->writeback_size);
                        wc->writeback_size--;
                        e++;
@@ -2006,7 +2055,7 @@ static void writecache_writeback(struct work_struct *work)
                                e = g;
                } else
                        e = container_of(wc->lru.prev, struct wc_entry, lru);
-               BUG_ON(e->write_in_progress);
+               BUG_ON(e->state == WC_VALID_WRITING_BACK);
                if (unlikely(!writecache_entry_is_committed(wc, e)))
                        writecache_flush(wc);
@@ -2015,7 +2064,7 @@ static void writecache_writeback(struct work_struct *work)
                        f = container_of(node, struct wc_entry, rb_node);
                        if (unlikely(read_original_sector(wc, f) ==
                                     read_original_sector(wc, e))) {
-                               BUG_ON(!f->write_in_progress);
+                               BUG_ON(f->state != WC_VALID_WRITING_BACK);
                                list_move(&e->lru, &skipped);
                                cond_resched();
                                continue;
@@ -2024,7 +2073,8 @@ static void writecache_writeback(struct work_struct *work)
                wc->writeback_size++;
                list_move(&e->lru, &wbl.list);
                wbl.size++;
-               e->write_in_progress = true;
+               BUG_ON(e->state != WC_VALID_DIRTY);
+               e->state = WC_VALID_WRITING_BACK;
                e->wc_list_contiguous = 1;
f = e;
@@ -2042,7 +2092,7 @@ static void writecache_writeback(struct work_struct *work)
                        if (read_original_sector(wc, g) !=
                            read_original_sector(wc, f) + (wc->block_size >> 
SECTOR_SHIFT))
                                break;
-                       if (unlikely(g->write_in_progress))
+                       if (unlikely(g->state == WC_VALID_WRITING_BACK))
                                break;
                        if (unlikely(!writecache_entry_is_committed(wc, g)))
                                break;
@@ -2059,7 +2109,8 @@ static void writecache_writeback(struct work_struct *work)
                        wc->writeback_size++;
                        list_move(&g->lru, &wbl.list);
                        wbl.size++;
-                       g->write_in_progress = true;
+                       BUG_ON(g->state != WC_VALID_DIRTY);
+                       g->state = WC_VALID_WRITING_BACK;
                        g->wc_list_contiguous = BIO_MAX_VECS;
                        f = g;
                        e->wc_list_contiguous++;
@@ -2245,6 +2296,7 @@ static int writecache_ctr(struct dm_target *ti, unsigned 
int argc, char **argv)
mutex_init(&wc->lock);
        wc->max_age = MAX_AGE_UNSPECIFIED;
+       wc->evict_clean_blocks = true;
        writecache_poison_lists(wc);
        init_waitqueue_head(&wc->freelist_wait);
        timer_setup(&wc->autocommit_timer, writecache_autocommit_timer, 0);
@@ -2480,6 +2532,12 @@ static int writecache_ctr(struct dm_target *ti, unsigned 
int argc, char **argv)
                        wc->pause = msecs_to_jiffies(pause_msecs);
                        wc->pause_set = true;
                        wc->pause_value = pause_msecs;
+               } else if (!strcasecmp(string, "evict_clean_blocks") && opt_params 
>= 1) {
+                       unsigned int enable;
+                       string = dm_shift_arg(&as), opt_params--;
+                       if (sscanf(string, "%u%c", &enable, &dummy) != 1)
+                               goto invalid_optional;
+                       wc->evict_clean_blocks = (enable != 0);
                } else {
  invalid_optional:
                        r = -EINVAL;
@@ -2741,6 +2799,8 @@ static void writecache_status(struct dm_target *ti, 
status_type_t type,
                        DMEMIT(" metadata_only");
                if (wc->pause_set)
                        DMEMIT(" pause_writeback %u", wc->pause_value);
+               if (!wc->evict_clean_blocks)
+                       DMEMIT(" evict_clean_blocks %u", 
wc->evict_clean_blocks);
                break;
        case STATUSTYPE_IMA:
                *result = '\0';
--
2.52.0



Reply via email to