* [PATCH v2 01/11] zram: drop stale comment in write_incompressible_page()
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 02/11] zram: drop get_slot_handle() and set_slot_handle() Sergey Senozhatsky
` (10 subsequent siblings)
11 siblings, 0 replies; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zram doesn't do fast/slow-path handle allocation anymore, so
the comment in write_incompressible_page() is stale. Drop it.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
drivers/block/zram/zram_drv.c | 5 -----
1 file changed, 5 deletions(-)
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index 632045b65bdb..380615b73446 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -2271,11 +2271,6 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
unsigned long handle;
void *src;
- /*
- * This function is called from preemptible context so we don't need
- * to do optimistic and fallback to pessimistic handle allocation,
- * like we do for compressible pages.
- */
handle = zs_malloc(zram->mem_pool, PAGE_SIZE,
GFP_NOIO | __GFP_NOWARN |
__GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v2 02/11] zram: drop get_slot_handle() and set_slot_handle()
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 01/11] zram: drop stale comment in write_incompressible_page() Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 03/11] zsmalloc: return handle from obj_allocated() Sergey Senozhatsky
` (9 subsequent siblings)
11 siblings, 0 replies; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zram_table_entry ->handle is overloaded to store zsmalloc handle,
same-filled page pattern and writeback block index, so calling
set_slot_handle() or get_slot_handle() for blk_idx and same-filled
pattern is confusing.
Turn ->handle into a union of handle, element and blk_idx, drop
get_slot_handle() and set_slot_handle(), and open-code field
accesses. No functional change.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
drivers/block/zram/zram_drv.c | 43 +++++++++++++----------------------
drivers/block/zram/zram_drv.h | 6 ++++-
2 files changed, 21 insertions(+), 28 deletions(-)
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index 380615b73446..5c13addd22f6 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -111,17 +111,6 @@ static inline struct zram *dev_to_zram(struct device *dev)
return (struct zram *)dev_to_disk(dev)->private_data;
}
-static unsigned long get_slot_handle(struct zram *zram, unsigned long index)
-{
- return zram->table[index].handle;
-}
-
-static void set_slot_handle(struct zram *zram, unsigned long index,
- unsigned long handle)
-{
- zram->table[index].handle = handle;
-}
-
static bool test_slot_flag(struct zram *zram, unsigned long index,
enum zram_pageflags flag)
{
@@ -962,8 +951,8 @@ static int zram_writeback_complete(struct zram *zram, struct zram_wb_req *req)
if (test_slot_flag(zram, index, ZRAM_HUGE))
atomic64_dec(&zram->stats.huge_pages);
atomic64_sub(get_slot_size(zram, index), &zram->stats.compr_data_size);
- zs_free(zram->mem_pool, get_slot_handle(zram, index));
- set_slot_handle(zram, index, req->blk_idx);
+ zs_free(zram->mem_pool, zram->table[index].handle);
+ zram->table[index].blk_idx = req->blk_idx;
set_slot_flag(zram, index, ZRAM_WB);
out:
@@ -2064,7 +2053,7 @@ static void slot_free(struct zram *zram, unsigned long index)
if (test_slot_flag(zram, index, ZRAM_WB)) {
clear_slot_flag(zram, index, ZRAM_WB);
- zram_release_bdev_block(zram, get_slot_handle(zram, index));
+ zram_release_bdev_block(zram, zram->table[index].blk_idx);
goto out;
}
@@ -2078,7 +2067,7 @@ static void slot_free(struct zram *zram, unsigned long index)
goto out;
}
- handle = get_slot_handle(zram, index);
+ handle = zram->table[index].handle;
if (!handle)
return;
@@ -2088,7 +2077,7 @@ static void slot_free(struct zram *zram, unsigned long index)
&zram->stats.compr_data_size);
out:
atomic64_dec(&zram->stats.pages_stored);
- set_slot_handle(zram, index, 0);
+ zram->table[index].element = 0;
set_slot_size(zram, index, 0);
}
@@ -2098,7 +2087,7 @@ static int read_same_filled_page(struct zram *zram, struct page *page,
void *mem;
mem = kmap_local_page(page);
- zram_fill_page(mem, PAGE_SIZE, get_slot_handle(zram, index));
+ zram_fill_page(mem, PAGE_SIZE, zram->table[index].element);
kunmap_local(mem);
return 0;
}
@@ -2110,7 +2099,7 @@ static int read_incompressible_page(struct zram *zram, struct page *page,
unsigned long handle;
void *src, *dst;
- handle = get_slot_handle(zram, index);
+ handle = zram->table[index].handle;
zs_obj_read_sg_begin(zram->mem_pool, handle, sg, PAGE_SIZE);
/* an incompressible object never spans two pages */
src = kmap_local_page(sg_page(sg));
@@ -2134,7 +2123,7 @@ static int read_compressed_page(struct zram *zram, struct page *page,
void *dst;
int ret, prio;
- handle = get_slot_handle(zram, index);
+ handle = zram->table[index].handle;
size = get_slot_size(zram, index);
prio = get_slot_comp_priority(zram, index);
@@ -2161,7 +2150,7 @@ static int read_from_zspool_raw(struct zram *zram, struct page *page,
unsigned int size;
void *dst;
- handle = get_slot_handle(zram, index);
+ handle = zram->table[index].handle;
size = get_slot_size(zram, index);
/*
@@ -2188,7 +2177,7 @@ static int read_from_zspool(struct zram *zram, struct page *page,
unsigned long index)
{
if (test_slot_flag(zram, index, ZRAM_SAME) ||
- !get_slot_handle(zram, index))
+ !zram->table[index].handle)
return read_same_filled_page(zram, page, index);
if (!test_slot_flag(zram, index, ZRAM_HUGE))
@@ -2209,7 +2198,7 @@ static int zram_read_page(struct zram *zram, struct page *page,
ret = read_from_zspool(zram, page, index);
slot_unlock(zram, index);
} else {
- unsigned long blk_idx = get_slot_handle(zram, index);
+ unsigned long blk_idx = zram->table[index].blk_idx;
/*
* The slot should be unlocked before reading from the backing
@@ -2255,7 +2244,7 @@ static int write_same_filled_page(struct zram *zram, unsigned long fill,
slot_lock(zram, index);
slot_free(zram, index);
set_slot_flag(zram, index, ZRAM_SAME);
- set_slot_handle(zram, index, fill);
+ zram->table[index].element = fill;
mark_slot_accessed(zram, index);
slot_unlock(zram, index);
@@ -2289,7 +2278,7 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
slot_lock(zram, index);
slot_free(zram, index);
set_slot_flag(zram, index, ZRAM_HUGE);
- set_slot_handle(zram, index, handle);
+ zram->table[index].handle = handle;
set_slot_size(zram, index, PAGE_SIZE);
mark_slot_accessed(zram, index);
slot_unlock(zram, index);
@@ -2355,7 +2344,7 @@ static int zram_write_page(struct zram *zram, struct page *page,
slot_lock(zram, index);
slot_free(zram, index);
- set_slot_handle(zram, index, handle);
+ zram->table[index].handle = handle;
set_slot_size(zram, index, comp_len);
mark_slot_accessed(zram, index);
slot_unlock(zram, index);
@@ -2471,7 +2460,7 @@ static int recompress_slot(struct zram *zram, unsigned long index,
void *src;
int ret = 0;
- handle_old = get_slot_handle(zram, index);
+ handle_old = zram->table[index].handle;
if (!handle_old)
return -EINVAL;
@@ -2554,7 +2543,7 @@ static int recompress_slot(struct zram *zram, unsigned long index,
zcomp_cstrm_put(zstrm);
slot_free(zram, index);
- set_slot_handle(zram, index, handle_new);
+ zram->table[index].handle = handle_new;
set_slot_size(zram, index, comp_len_new);
set_slot_comp_priority(zram, index, prio);
diff --git a/drivers/block/zram/zram_drv.h b/drivers/block/zram/zram_drv.h
index c563a7c13a49..5416e25e64e8 100644
--- a/drivers/block/zram/zram_drv.h
+++ b/drivers/block/zram/zram_drv.h
@@ -78,7 +78,11 @@ enum zram_pageflags {
* of each entry and hence of the meta table.
*/
struct zram_table_entry {
- unsigned long handle;
+ union {
+ unsigned long handle;
+ unsigned long element;
+ unsigned long blk_idx;
+ };
union {
unsigned long __lock;
struct attr {
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v2 03/11] zsmalloc: return handle from obj_allocated()
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 01/11] zram: drop stale comment in write_incompressible_page() Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 02/11] zram: drop get_slot_handle() and set_slot_handle() Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 16:28 ` Yosry Ahmed
2026-11-10 5:48 ` [PATCH v2 04/11] zsmalloc: switch handle type to struct zs_handle Sergey Senozhatsky
` (8 subsequent siblings)
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky, Yosry Ahmed
Allocated handles are valid kernel addresses and never 0, so let
obj_allocated() return the handle directly (or 0 if unallocated)
instead of returning a bool.
No functional change.
Suggested-by: Yosry Ahmed <yosry@kernel.org>
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
mm/zsmalloc.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index 7ef80e0da626..dab71f582103 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -822,8 +822,7 @@ static unsigned long handle_to_obj(unsigned long handle)
return READ_ONCE(*(unsigned long *)handle);
}
-static inline bool obj_allocated(struct zpdesc *zpdesc, void *obj,
- unsigned long *phandle)
+static inline unsigned long obj_allocated(struct zpdesc *zpdesc, void *obj)
{
unsigned long handle;
struct zspage *zspage = get_zspage(zpdesc);
@@ -836,11 +835,10 @@ static inline bool obj_allocated(struct zpdesc *zpdesc, void *obj,
handle = *(unsigned long *)obj;
if (!(handle & OBJ_ALLOCATED_TAG))
- return false;
+ return 0;
/* Clear all tags before returning the handle */
- *phandle = handle & ~OBJ_TAG_MASK;
- return true;
+ return handle & ~OBJ_TAG_MASK;
}
static void reset_zpdesc(struct zpdesc *zpdesc)
@@ -1571,7 +1569,8 @@ static unsigned long find_alloced_obj(struct size_class *class,
offset += class->size * index;
while (offset < PAGE_SIZE) {
- if (obj_allocated(zpdesc, addr + offset, &handle))
+ handle = obj_allocated(zpdesc, addr + offset);
+ if (handle)
break;
offset += class->size;
@@ -1824,8 +1823,8 @@ static int zs_page_migrate(struct page *newpage, struct page *page,
for (addr = s_addr + offset; addr < s_addr + PAGE_SIZE;
addr += class->size) {
- if (obj_allocated(zpdesc, addr, &handle)) {
-
+ handle = obj_allocated(zpdesc, addr);
+ if (handle) {
old_obj = handle_to_obj(handle);
obj_to_location(old_obj, &dummy, &obj_idx);
new_obj = location_to_obj(newzpdesc, obj_idx,
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 03/11] zsmalloc: return handle from obj_allocated()
2026-11-10 5:48 ` [PATCH v2 03/11] zsmalloc: return handle from obj_allocated() Sergey Senozhatsky
@ 2026-11-10 16:28 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 16:28 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:08PM +0900, Sergey Senozhatsky wrote:
> Allocated handles are valid kernel addresses and never 0, so let
> obj_allocated() return the handle directly (or 0 if unallocated)
> instead of returning a bool.
>
> No functional change.
>
> Suggested-by: Yosry Ahmed <yosry@kernel.org>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
Reviewed-by: Yosry Ahmed <yosry@kernel.org>
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 04/11] zsmalloc: switch handle type to struct zs_handle
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (2 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 03/11] zsmalloc: return handle from obj_allocated() Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 16:35 ` Yosry Ahmed
2026-11-10 5:48 ` [PATCH v2 05/11] zram: check mem_limit before allocating zsmalloc object Sergey Senozhatsky
` (7 subsequent siblings)
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zsmalloc handle is an unsigned long that holds an address of a
handle_cachep object, so zsmalloc and its users have to cast it
back and forth. Introduce struct zs_handle and switch zsmalloc,
zram and zswap to struct zs_handle pointers. Functions that only
read the handle take a const pointer.
This is a preparation for embedding struct zs_handle into zram and
zswap entries. No functional change.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
drivers/block/zram/zram_drv.c | 28 ++++++------
drivers/block/zram/zram_drv.h | 2 +-
include/linux/zsmalloc.h | 13 +++---
mm/zsmalloc.c | 85 +++++++++++++++++++----------------
mm/zswap.c | 8 ++--
5 files changed, 73 insertions(+), 63 deletions(-)
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index 5c13addd22f6..2c2e11b2b663 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -2030,7 +2030,7 @@ static bool zram_meta_alloc(struct zram *zram, u64 disksize)
static void slot_free(struct zram *zram, unsigned long index)
{
- unsigned long handle;
+ struct zs_handle *handle;
#ifdef CONFIG_ZRAM_TRACK_ENTRY_ACTIME
zram->table[index].attr.ac_time = 0;
@@ -2096,7 +2096,7 @@ static int read_incompressible_page(struct zram *zram, struct page *page,
unsigned long index)
{
struct scatterlist sg[2];
- unsigned long handle;
+ struct zs_handle *handle;
void *src, *dst;
handle = zram->table[index].handle;
@@ -2118,7 +2118,7 @@ static int read_compressed_page(struct zram *zram, struct page *page,
enum zcomp_dstrm_mode mode = ZCOMP_DSTRM_EXCLUSIVE;
struct zcomp_dstrm *zstrm;
struct scatterlist sg[2];
- unsigned long handle;
+ struct zs_handle *handle;
unsigned int size;
void *dst;
int ret, prio;
@@ -2146,7 +2146,7 @@ static int read_from_zspool_raw(struct zram *zram, struct page *page,
unsigned long index)
{
struct scatterlist sg[2];
- unsigned long handle;
+ struct zs_handle *handle;
unsigned int size;
void *dst;
@@ -2257,14 +2257,14 @@ static int write_same_filled_page(struct zram *zram, unsigned long fill,
static int write_incompressible_page(struct zram *zram, struct page *page,
unsigned long index)
{
- unsigned long handle;
+ struct zs_handle *handle;
void *src;
handle = zs_malloc(zram->mem_pool, PAGE_SIZE,
GFP_NOIO | __GFP_NOWARN |
__GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
- if (IS_ERR_VALUE(handle))
- return PTR_ERR((void *)handle);
+ if (IS_ERR(handle))
+ return PTR_ERR(handle);
if (!zram_can_store_page(zram)) {
zs_free(zram->mem_pool, handle);
@@ -2295,7 +2295,7 @@ static int zram_write_page(struct zram *zram, struct page *page,
unsigned long index)
{
int ret = 0;
- unsigned long handle;
+ struct zs_handle *handle;
unsigned int comp_len;
void *mem;
struct zcomp_cstrm *zstrm;
@@ -2328,9 +2328,9 @@ static int zram_write_page(struct zram *zram, struct page *page,
handle = zs_malloc(zram->mem_pool, comp_len,
GFP_NOIO | __GFP_NOWARN |
__GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
- if (IS_ERR_VALUE(handle)) {
+ if (IS_ERR(handle)) {
zcomp_cstrm_put(zstrm);
- return PTR_ERR((void *)handle);
+ return PTR_ERR(handle);
}
if (!zram_can_store_page(zram)) {
@@ -2451,8 +2451,8 @@ static int recompress_slot(struct zram *zram, unsigned long index,
u32 threshold, u32 prio)
{
struct zcomp_cstrm *zstrm = NULL;
- unsigned long handle_old;
- unsigned long handle_new;
+ struct zs_handle *handle_old;
+ struct zs_handle *handle_new;
unsigned int comp_len_old;
unsigned int comp_len_new;
unsigned int class_index_old;
@@ -2534,9 +2534,9 @@ static int recompress_slot(struct zram *zram, unsigned long index,
GFP_NOIO | __GFP_NOWARN |
__GFP_HIGHMEM | __GFP_MOVABLE,
page_to_nid(page));
- if (IS_ERR_VALUE(handle_new)) {
+ if (IS_ERR(handle_new)) {
zcomp_cstrm_put(zstrm);
- return PTR_ERR((void *)handle_new);
+ return PTR_ERR(handle_new);
}
zs_obj_write(zram->mem_pool, handle_new, zstrm->buffer, comp_len_new);
diff --git a/drivers/block/zram/zram_drv.h b/drivers/block/zram/zram_drv.h
index 5416e25e64e8..5b211a9f314c 100644
--- a/drivers/block/zram/zram_drv.h
+++ b/drivers/block/zram/zram_drv.h
@@ -79,7 +79,7 @@ enum zram_pageflags {
*/
struct zram_table_entry {
union {
- unsigned long handle;
+ struct zs_handle *handle;
unsigned long element;
unsigned long blk_idx;
};
diff --git a/include/linux/zsmalloc.h b/include/linux/zsmalloc.h
index 5b7298a5026b..7fa500ae2653 100644
--- a/include/linux/zsmalloc.h
+++ b/include/linux/zsmalloc.h
@@ -22,14 +22,15 @@ struct zs_pool_stats {
};
struct zs_pool;
+struct zs_handle;
struct scatterlist;
struct zs_pool *zs_create_pool(const char *name);
void zs_destroy_pool(struct zs_pool *pool);
-unsigned long zs_malloc(struct zs_pool *pool, size_t size, gfp_t flags,
- const int nid);
-void zs_free(struct zs_pool *pool, unsigned long obj);
+struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t flags,
+ const int nid);
+void zs_free(struct zs_pool *pool, struct zs_handle *handle);
size_t zs_huge_class_size(struct zs_pool *pool);
@@ -40,10 +41,10 @@ unsigned int zs_lookup_class_index(struct zs_pool *pool, unsigned int size);
void zs_pool_stats(struct zs_pool *pool, struct zs_pool_stats *stats);
-void zs_obj_read_sg_begin(struct zs_pool *pool, unsigned long handle,
+void zs_obj_read_sg_begin(struct zs_pool *pool, const struct zs_handle *handle,
struct scatterlist *sg, size_t mem_len);
-void zs_obj_read_sg_end(struct zs_pool *pool, unsigned long handle);
-void zs_obj_write(struct zs_pool *pool, unsigned long handle,
+void zs_obj_read_sg_end(struct zs_pool *pool, const struct zs_handle *handle);
+void zs_obj_write(struct zs_pool *pool, const struct zs_handle *handle,
void *handle_mem, size_t mem_len);
extern const struct movable_operations zsmalloc_mops;
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index dab71f582103..3a6a7253fd01 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -264,6 +264,10 @@ struct link_free {
};
};
+struct zs_handle {
+ unsigned long __private obj;
+};
+
static struct kmem_cache *handle_cachep;
static struct kmem_cache *zspage_cachep;
@@ -438,16 +442,16 @@ static void init_deferred_free(struct zs_pool *pool) {}
static void SetZsPageMovable(struct zs_pool *pool, struct zspage *zspage) {}
#endif
-static unsigned long cache_alloc_handle(gfp_t gfp)
+static struct zs_handle *cache_alloc_handle(gfp_t gfp)
{
gfp = gfp & ~(__GFP_HIGHMEM | __GFP_MOVABLE);
- return (unsigned long)kmem_cache_alloc(handle_cachep, gfp);
+ return kmem_cache_alloc(handle_cachep, gfp);
}
-static void cache_free_handle(unsigned long handle)
+static void cache_free_handle(struct zs_handle *handle)
{
- kmem_cache_free(handle_cachep, (void *)handle);
+ kmem_cache_free(handle_cachep, handle);
}
static struct zspage *cache_alloc_zspage(gfp_t gfp)
@@ -466,9 +470,9 @@ static void cache_free_zspage(struct zspage *zspage)
* Pairs with READ_ONCE() in handle_to_obj(): zs_free() may read the
* handle locklessly, so prevent store tearing here.
*/
-static void record_obj(unsigned long handle, unsigned long obj)
+static void record_obj(struct zs_handle *handle, unsigned long obj)
{
- WRITE_ONCE(*(unsigned long *)handle, obj);
+ WRITE_ONCE(ACCESS_PRIVATE(handle, obj), obj);
}
/* Protected by class->lock */
@@ -817,12 +821,12 @@ static unsigned long location_to_obj(struct zpdesc *zpdesc, unsigned int obj_idx
return obj;
}
-static unsigned long handle_to_obj(unsigned long handle)
+static unsigned long handle_to_obj(const struct zs_handle *handle)
{
- return READ_ONCE(*(unsigned long *)handle);
+ return READ_ONCE(ACCESS_PRIVATE(handle, obj));
}
-static inline unsigned long obj_allocated(struct zpdesc *zpdesc, void *obj)
+static inline struct zs_handle *obj_allocated(struct zpdesc *zpdesc, void *obj)
{
unsigned long handle;
struct zspage *zspage = get_zspage(zpdesc);
@@ -835,10 +839,10 @@ static inline unsigned long obj_allocated(struct zpdesc *zpdesc, void *obj)
handle = *(unsigned long *)obj;
if (!(handle & OBJ_ALLOCATED_TAG))
- return 0;
+ return NULL;
/* Clear all tags before returning the handle */
- return handle & ~OBJ_TAG_MASK;
+ return (struct zs_handle *)(handle & ~OBJ_TAG_MASK);
}
static void reset_zpdesc(struct zpdesc *zpdesc)
@@ -1120,7 +1124,7 @@ unsigned long zs_get_total_pages(struct zs_pool *pool)
}
EXPORT_SYMBOL_GPL(zs_get_total_pages);
-void zs_obj_read_sg_begin(struct zs_pool *pool, unsigned long handle,
+void zs_obj_read_sg_begin(struct zs_pool *pool, const struct zs_handle *handle,
struct scatterlist *sg, size_t mem_len)
{
struct zspage *zspage;
@@ -1167,7 +1171,7 @@ void zs_obj_read_sg_begin(struct zs_pool *pool, unsigned long handle,
}
EXPORT_SYMBOL_GPL(zs_obj_read_sg_begin);
-void zs_obj_read_sg_end(struct zs_pool *pool, unsigned long handle)
+void zs_obj_read_sg_end(struct zs_pool *pool, const struct zs_handle *handle)
{
struct zspage *zspage;
struct zpdesc *zpdesc;
@@ -1182,7 +1186,7 @@ void zs_obj_read_sg_end(struct zs_pool *pool, unsigned long handle)
}
EXPORT_SYMBOL_GPL(zs_obj_read_sg_end);
-void zs_obj_write(struct zs_pool *pool, unsigned long handle,
+void zs_obj_write(struct zs_pool *pool, const struct zs_handle *handle,
void *handle_mem, size_t mem_len)
{
struct zspage *zspage;
@@ -1251,7 +1255,7 @@ size_t zs_huge_class_size(struct zs_pool *pool)
EXPORT_SYMBOL_GPL(zs_huge_class_size);
static unsigned long obj_malloc(struct zs_pool *pool,
- struct zspage *zspage, unsigned long handle)
+ struct zspage *zspage, struct zs_handle *handle)
{
int i, nr_zpdesc, offset;
unsigned long obj;
@@ -1278,9 +1282,10 @@ static unsigned long obj_malloc(struct zs_pool *pool,
set_freeobj(zspage, link->next >> OBJ_TAG_BITS);
if (likely(!ZsHugePage(zspage)))
/* record handle in the header of allocated chunk */
- link->handle = handle | OBJ_ALLOCATED_TAG;
+ link->handle = (unsigned long)handle | OBJ_ALLOCATED_TAG;
else
- zspage->first_zpdesc->handle = handle | OBJ_ALLOCATED_TAG;
+ zspage->first_zpdesc->handle =
+ (unsigned long)handle | OBJ_ALLOCATED_TAG;
kunmap_local(vaddr);
mod_zspage_inuse(zspage, 1);
@@ -1303,23 +1308,23 @@ static unsigned long obj_malloc(struct zs_pool *pool,
* otherwise an ERR_PTR().
* Allocation requests with size > ZS_MAX_ALLOC_SIZE will fail.
*/
-unsigned long zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
- const int nid)
+struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
+ const int nid)
{
- unsigned long handle;
+ struct zs_handle *handle;
struct size_class *class;
int newfg;
struct zspage *zspage;
if (unlikely(!size))
- return (unsigned long)ERR_PTR(-EINVAL);
+ return ERR_PTR(-EINVAL);
if (unlikely(size > ZS_MAX_ALLOC_SIZE))
- return (unsigned long)ERR_PTR(-ENOSPC);
+ return ERR_PTR(-ENOSPC);
handle = cache_alloc_handle(gfp);
if (!handle)
- return (unsigned long)ERR_PTR(-ENOMEM);
+ return ERR_PTR(-ENOMEM);
class = lookup_size_class(pool, size);
@@ -1340,7 +1345,7 @@ unsigned long zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
zspage = alloc_zspage(pool, class, gfp, nid);
if (!zspage) {
cache_free_handle(handle);
- return (unsigned long)ERR_PTR(-ENOMEM);
+ return ERR_PTR(-ENOMEM);
}
spin_lock(&class->lock);
@@ -1408,9 +1413,11 @@ static unsigned int obj_to_class_idx(unsigned long obj)
* encoding is disabled), fall back to pool->lock for the lookup.
*/
#if ZS_OBJ_CLASS_BITS > 0
-static inline void obj_class_get_and_lock(struct zs_pool *pool, unsigned long handle,
- unsigned long *objp, struct zspage **zspagep,
- struct size_class **classp)
+static inline void obj_class_get_and_lock(struct zs_pool *pool,
+ const struct zs_handle *handle,
+ unsigned long *objp,
+ struct zspage **zspagep,
+ struct size_class **classp)
__acquires(&(*classp)->lock)
{
struct zpdesc *f_zpdesc;
@@ -1426,9 +1433,11 @@ static inline void obj_class_get_and_lock(struct zs_pool *pool, unsigned long ha
*objp = obj;
}
#else
-static inline void obj_class_get_and_lock(struct zs_pool *pool, unsigned long handle,
- unsigned long *objp, struct zspage **zspagep,
- struct size_class **classp)
+static inline void obj_class_get_and_lock(struct zs_pool *pool,
+ const struct zs_handle *handle,
+ unsigned long *objp,
+ struct zspage **zspagep,
+ struct size_class **classp)
__acquires(&(*classp)->lock)
{
struct zpdesc *f_zpdesc;
@@ -1445,7 +1454,7 @@ static inline void obj_class_get_and_lock(struct zs_pool *pool, unsigned long ha
}
#endif
-void zs_free(struct zs_pool *pool, unsigned long handle)
+void zs_free(struct zs_pool *pool, struct zs_handle *handle)
{
struct zspage *zspage;
unsigned long obj;
@@ -1453,7 +1462,7 @@ void zs_free(struct zs_pool *pool, unsigned long handle)
int fullness;
struct zspage *zspage_to_free = NULL;
- if (IS_ERR_OR_NULL((void *)handle))
+ if (IS_ERR_OR_NULL(handle))
return;
obj_class_get_and_lock(pool, handle, &obj, &zspage, &class);
@@ -1557,12 +1566,12 @@ static void zs_object_copy(struct size_class *class, unsigned long dst,
* Find alloced object in zspage from index object and
* return handle.
*/
-static unsigned long find_alloced_obj(struct size_class *class,
- struct zpdesc *zpdesc, int *obj_idx)
+static struct zs_handle *find_alloced_obj(struct size_class *class,
+ struct zpdesc *zpdesc, int *obj_idx)
{
unsigned int offset;
int index = *obj_idx;
- unsigned long handle = 0;
+ struct zs_handle *handle = NULL;
void *addr = kmap_local_zpdesc(zpdesc);
offset = get_first_obj_offset(zpdesc);
@@ -1588,7 +1597,7 @@ static void migrate_zspage(struct zs_pool *pool, struct zspage *src_zspage,
struct zspage *dst_zspage)
{
unsigned long used_obj, free_obj;
- unsigned long handle;
+ struct zs_handle *handle;
int obj_idx = 0;
struct zpdesc *s_zpdesc = get_first_zpdesc(src_zspage);
struct size_class *class = pool->size_class[src_zspage->class];
@@ -1759,7 +1768,7 @@ static int zs_page_migrate(struct page *newpage, struct page *page,
struct zpdesc *zpdesc = page_zpdesc(page);
void *s_addr, *d_addr, *addr;
unsigned int offset;
- unsigned long handle;
+ struct zs_handle *handle;
unsigned long old_obj, new_obj;
unsigned int obj_idx;
@@ -2290,7 +2299,7 @@ static void zs_destroy_caches(void)
static int __init zs_init_caches(void)
{
- handle_cachep = kmem_cache_create("zs_handle", ZS_HANDLE_SIZE,
+ handle_cachep = kmem_cache_create("zs_handle", sizeof(struct zs_handle),
0, 0, NULL);
zspage_cachep = kmem_cache_create("zspage", sizeof(struct zspage),
0, 0, NULL);
diff --git a/mm/zswap.c b/mm/zswap.c
index 37d217860a6b..1160bd34e289 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -216,7 +216,7 @@ struct zswap_entry {
unsigned int length;
bool referenced;
u8 pool_idx;
- unsigned long handle;
+ struct zs_handle *handle;
struct obj_cgroup *objcg;
struct list_head lru;
};
@@ -889,7 +889,7 @@ static bool zswap_compress(struct folio *folio, long index,
struct scatterlist input, output;
int comp_ret = 0, alloc_ret = 0;
unsigned int dlen = PAGE_SIZE;
- unsigned long handle;
+ struct zs_handle *handle;
gfp_t gfp;
u8 *dst;
bool mapped = false;
@@ -950,8 +950,8 @@ static bool zswap_compress(struct folio *folio, long index,
gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
handle = zs_malloc(pool->zs_pool, dlen, gfp, folio_nid(folio));
- if (IS_ERR_VALUE(handle)) {
- alloc_ret = PTR_ERR((void *)handle);
+ if (IS_ERR(handle)) {
+ alloc_ret = PTR_ERR(handle);
goto unlock;
}
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 04/11] zsmalloc: switch handle type to struct zs_handle
2026-11-10 5:48 ` [PATCH v2 04/11] zsmalloc: switch handle type to struct zs_handle Sergey Senozhatsky
@ 2026-11-10 16:35 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 16:35 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:09PM +0900, Sergey Senozhatsky wrote:
> zsmalloc handle is an unsigned long that holds an address of a
> handle_cachep object, so zsmalloc and its users have to cast it
> back and forth. Introduce struct zs_handle and switch zsmalloc,
> zram and zswap to struct zs_handle pointers. Functions that only
> read the handle take a const pointer.
>
> This is a preparation for embedding struct zs_handle into zram and
> zswap entries. No functional change.
>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
> ---
> drivers/block/zram/zram_drv.c | 28 ++++++------
> drivers/block/zram/zram_drv.h | 2 +-
> include/linux/zsmalloc.h | 13 +++---
> mm/zsmalloc.c | 85 +++++++++++++++++++----------------
> mm/zswap.c | 8 ++--
zsmalloc and zswap changes LGTM with a small nit below:
Reviewed-by: Yosry Ahmed <yosry@kernel.org>
[..]
> diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
> index dab71f582103..3a6a7253fd01 100644
> --- a/mm/zsmalloc.c
> +++ b/mm/zsmalloc.c
> @@ -264,6 +264,10 @@ struct link_free {
> };
> };
>
> +struct zs_handle {
> + unsigned long __private obj;
> +};
nit: Update the definition of ZS_HANDLE_SIZE to sizeof(zs_handle)?
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 05/11] zram: check mem_limit before allocating zsmalloc object
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (3 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 04/11] zsmalloc: switch handle type to struct zs_handle Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 06/11] zram: allocate zsmalloc object under slot lock Sergey Senozhatsky
` (6 subsequent siblings)
11 siblings, 0 replies; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zram_can_store_page() is called after zs_malloc(), so once we reach
mem_limit every write that doesn't fit into a partial zspage allocates
a new zspage only to free it right away.
Check mem_limit in zram_write_page() before compressing the page and
allocating from zsmalloc, and update max_used_pages after zs_malloc()
succeeds. mem_limit was never a hard limit, the allocation that
crosses it happens before the check anyway, so now we can go over the
limit by up to one zspage per concurrent writer, and writes fail early
until enough slots are freed.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
Documentation/admin-guide/blockdev/zram.rst | 4 +++-
drivers/block/zram/zram_drv.c | 25 +++++++++------------
2 files changed, 13 insertions(+), 16 deletions(-)
diff --git a/Documentation/admin-guide/blockdev/zram.rst b/Documentation/admin-guide/blockdev/zram.rst
index 148b7cf3b924..db256c53b127 100644
--- a/Documentation/admin-guide/blockdev/zram.rst
+++ b/Documentation/admin-guide/blockdev/zram.rst
@@ -181,7 +181,9 @@ Set memory limit: Optional
Set memory limit by writing the value to sysfs node 'mem_limit'.
The value can be either in bytes or you can use mem suffixes.
-In addition, you could change the value in runtime.
+In addition, you could change the value in runtime. The limit is
+checked before zsmalloc allocation, so memory usage may slightly
+exceed it (by up to one zspage per concurrent writer).
Examples::
# limit /dev/zram0 with 50MB memory
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index 2c2e11b2b663..d38b20a22595 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -177,8 +177,9 @@ static void mark_slot_accessed(struct zram *zram, unsigned long index)
#endif
}
-static inline void update_used_max(struct zram *zram, const unsigned long pages)
+static inline void update_used_max(struct zram *zram)
{
+ unsigned long pages = zs_get_total_pages(zram->mem_pool);
unsigned long cur_max = atomic_long_read(&zram->stats.max_used_pages);
do {
@@ -190,12 +191,10 @@ static inline void update_used_max(struct zram *zram, const unsigned long pages)
static bool zram_can_store_page(struct zram *zram)
{
- unsigned long alloced_pages;
-
- alloced_pages = zs_get_total_pages(zram->mem_pool);
- update_used_max(zram, alloced_pages);
+ if (!zram->limit_pages)
+ return true;
- return !zram->limit_pages || alloced_pages <= zram->limit_pages;
+ return zs_get_total_pages(zram->mem_pool) <= zram->limit_pages;
}
#if PAGE_SIZE != 4096
@@ -2266,10 +2265,7 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
if (IS_ERR(handle))
return PTR_ERR(handle);
- if (!zram_can_store_page(zram)) {
- zs_free(zram->mem_pool, handle);
- return -ENOMEM;
- }
+ update_used_max(zram);
src = kmap_local_page(page);
zs_obj_write(zram->mem_pool, handle, src, PAGE_SIZE);
@@ -2308,6 +2304,9 @@ static int zram_write_page(struct zram *zram, struct page *page,
if (same_filled)
return write_same_filled_page(zram, element, index);
+ if (!zram_can_store_page(zram))
+ return -ENOMEM;
+
zstrm = zcomp_cstrm_get(zram->comps[ZRAM_PRIMARY_COMP]);
mem = kmap_local_page(page);
ret = zcomp_compress(zram->comps[ZRAM_PRIMARY_COMP], zstrm,
@@ -2333,11 +2332,7 @@ static int zram_write_page(struct zram *zram, struct page *page,
return PTR_ERR(handle);
}
- if (!zram_can_store_page(zram)) {
- zcomp_cstrm_put(zstrm);
- zs_free(zram->mem_pool, handle);
- return -ENOMEM;
- }
+ update_used_max(zram);
zs_obj_write(zram->mem_pool, handle, zstrm->buffer, comp_len);
zcomp_cstrm_put(zstrm);
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v2 06/11] zram: allocate zsmalloc object under slot lock
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (4 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 05/11] zram: check mem_limit before allocating zsmalloc object Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 5:48 ` [PATCH v2 07/11] zswap: do not free the entry if its pool is NULL Sergey Senozhatsky
` (5 subsequent siblings)
11 siblings, 0 replies; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zram_write_page() and write_incompressible_page() allocate and
write zsmalloc object first and take slot lock only to free the
old slot data and set the new handle. A subsequent patch makes
zsmalloc store object location directly in zram table entry, so
we need to free the slot before we allocate a new object for it.
Free the slot and allocate the object under slot lock.
recompress_slot() already allocates objects under it, and
GFP_NOIO reclaim cannot re-enter zram write path.
mem_limit is checked before slot_free() (previous patch), so hitting
the limit keeps the old slot data. zs_malloc() can still fail after
slot_free(), e.g. when we write from reclaim (PF_MEMALLOC), which is
swap-out, so the old slot data (if any) is stale anyway, or when the
writer is an OOM victim that has no reserves left. The slot is left
empty then, matching standard block write-failure semantics.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
drivers/block/zram/zram_drv.c | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index d38b20a22595..aa3ab88f1259 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -2259,11 +2259,15 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
struct zs_handle *handle;
void *src;
+ slot_lock(zram, index);
+ slot_free(zram, index);
handle = zs_malloc(zram->mem_pool, PAGE_SIZE,
GFP_NOIO | __GFP_NOWARN |
__GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
- if (IS_ERR(handle))
+ if (IS_ERR(handle)) {
+ slot_unlock(zram, index);
return PTR_ERR(handle);
+ }
update_used_max(zram);
@@ -2271,8 +2275,6 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
zs_obj_write(zram->mem_pool, handle, src, PAGE_SIZE);
kunmap_local(src);
- slot_lock(zram, index);
- slot_free(zram, index);
set_slot_flag(zram, index, ZRAM_HUGE);
zram->table[index].handle = handle;
set_slot_size(zram, index, PAGE_SIZE);
@@ -2324,11 +2326,14 @@ static int zram_write_page(struct zram *zram, struct page *page,
return write_incompressible_page(zram, page, index);
}
+ slot_lock(zram, index);
+ slot_free(zram, index);
handle = zs_malloc(zram->mem_pool, comp_len,
GFP_NOIO | __GFP_NOWARN |
__GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
if (IS_ERR(handle)) {
zcomp_cstrm_put(zstrm);
+ slot_unlock(zram, index);
return PTR_ERR(handle);
}
@@ -2337,8 +2342,6 @@ static int zram_write_page(struct zram *zram, struct page *page,
zs_obj_write(zram->mem_pool, handle, zstrm->buffer, comp_len);
zcomp_cstrm_put(zstrm);
- slot_lock(zram, index);
- slot_free(zram, index);
zram->table[index].handle = handle;
set_slot_size(zram, index, comp_len);
mark_slot_accessed(zram, index);
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* [PATCH v2 07/11] zswap: do not free the entry if its pool is NULL
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (5 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 06/11] zram: allocate zsmalloc object under slot lock Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 16:37 ` Yosry Ahmed
2026-11-10 5:48 ` [PATCH v2 08/11] zsmalloc: encode 1-based obj_idx so valid obj is never 0 Sergey Senozhatsky
` (4 subsequent siblings)
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky, Yosry Ahmed
zswap_entry_free() warns if the entry has no pool (which should never
happen), skips zs_free() and then frees the entry. A subsequent patch
embeds zsmalloc handle into zswap_entry, so zsmalloc object will point
to the entry, and we cannot free the entry while the object is still
there.
Bail out early and leak the entry, its cgroup charge and stats instead.
Suggested-by: Yosry Ahmed <yosry@kernel.org>
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
mm/zswap.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/mm/zswap.c b/mm/zswap.c
index 1160bd34e289..dc0400d7e1a3 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -787,10 +787,11 @@ static void zswap_entry_free(struct zswap_entry *entry)
struct zswap_pool *pool = zswap_entry_pool(entry);
zswap_lru_del(entry);
- if (!WARN_ON_ONCE(!pool)) {
- zs_free(pool->zs_pool, entry->handle);
- zswap_pool_put(pool);
- }
+ if (WARN_ON_ONCE(!pool))
+ return;
+
+ zs_free(pool->zs_pool, entry->handle);
+ zswap_pool_put(pool);
if (entry->objcg) {
obj_cgroup_uncharge_zswap(entry->objcg, entry->length);
obj_cgroup_put(entry->objcg);
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 07/11] zswap: do not free the entry if its pool is NULL
2026-11-10 5:48 ` [PATCH v2 07/11] zswap: do not free the entry if its pool is NULL Sergey Senozhatsky
@ 2026-11-10 16:37 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 16:37 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:12PM +0900, Sergey Senozhatsky wrote:
> zswap_entry_free() warns if the entry has no pool (which should never
> happen), skips zs_free() and then frees the entry. A subsequent patch
> embeds zsmalloc handle into zswap_entry, so zsmalloc object will point
> to the entry, and we cannot free the entry while the object is still
> there.
>
> Bail out early and leak the entry, its cgroup charge and stats instead.
>
> Suggested-by: Yosry Ahmed <yosry@kernel.org>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
Acked-by: Yosry Ahmed <yosry@kernel.org>
> ---
> mm/zswap.c | 9 +++++----
> 1 file changed, 5 insertions(+), 4 deletions(-)
>
> diff --git a/mm/zswap.c b/mm/zswap.c
> index 1160bd34e289..dc0400d7e1a3 100644
> --- a/mm/zswap.c
> +++ b/mm/zswap.c
> @@ -787,10 +787,11 @@ static void zswap_entry_free(struct zswap_entry *entry)
> struct zswap_pool *pool = zswap_entry_pool(entry);
>
> zswap_lru_del(entry);
> - if (!WARN_ON_ONCE(!pool)) {
> - zs_free(pool->zs_pool, entry->handle);
> - zswap_pool_put(pool);
> - }
> + if (WARN_ON_ONCE(!pool))
> + return;
> +
> + zs_free(pool->zs_pool, entry->handle);
> + zswap_pool_put(pool);
> if (entry->objcg) {
> obj_cgroup_uncharge_zswap(entry->objcg, entry->length);
> obj_cgroup_put(entry->objcg);
> --
> 2.56.0.506.g506a7eabc5-goog
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 08/11] zsmalloc: encode 1-based obj_idx so valid obj is never 0
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (6 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 07/11] zswap: do not free the entry if its pool is NULL Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 16:44 ` Yosry Ahmed
2026-11-10 5:48 ` [PATCH v2 09/11] zsmalloc: embed struct zs_handle into zram and zswap entries Sergey Senozhatsky
` (3 subsequent siblings)
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
The next patch embeds struct zs_handle into zram and zswap entries,
and a handle is considered allocated when its obj is not 0. But obj
can be 0: object 0 of a zspage whose first page is PFN 0 encodes to
0, and some platforms do give PFN 0 to the page allocator.
Store obj_idx + 1 in obj, so that a valid obj is never 0, and adjust
ZS_MIN_ALLOC_SIZE so that the largest index still fits. Unlike a
separate tag bit, this doesn't take any bits from obj. zsmalloc did
the same before handles moved to handle_cachep, see
commit 67296874eb1c ("staging: zsmalloc: Ensure handle is never 0 on
success").
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
mm/zsmalloc.c | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index 3a6a7253fd01..aa436df0cc4d 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -61,7 +61,8 @@
* Object location (<PFN>, <obj_idx>) is encoded as
* a single (unsigned long) handle value.
*
- * Note that object index <obj_idx> starts from 0.
+ * Note that object index <obj_idx> starts from 0, and is encoded as
+ * <obj_idx> + 1 so that a valid encoded obj value is never 0.
*
* This is made more complicated by various memory models and PAE.
*/
@@ -169,9 +170,6 @@
static_assert(ZS_OBJ_IDX_BITS >= ZS_PAGES_PER_ZSPAGE_BITS,
"zsmalloc: ZS_MIN_ALLOC_SIZE would exceed ZS_MAX_ALLOC_SIZE");
-/* ZS_MIN_ALLOC_SIZE must be multiple of ZS_ALIGN */
-#define ZS_MIN_ALLOC_SIZE \
- MAX(32, (ZS_MAX_PAGES_PER_ZSPAGE << PAGE_SHIFT >> ZS_OBJ_IDX_BITS))
/* each chunk includes extra space to keep handle */
#define ZS_MAX_ALLOC_SIZE PAGE_SIZE
@@ -189,6 +187,11 @@ static_assert(ZS_OBJ_IDX_BITS >= ZS_PAGES_PER_ZSPAGE_BITS,
* (reason above)
*/
#define ZS_SIZE_CLASS_DELTA (PAGE_SIZE >> CLASS_BITS)
+#define _ZS_MIN_ALLOC_RAW \
+ DIV_ROUND_UP(ZS_MAX_PAGES_PER_ZSPAGE << PAGE_SHIFT, ZS_OBJ_IDX_MASK)
+#define ZS_MIN_ALLOC_SIZE \
+ MIN(ZS_MAX_ALLOC_SIZE, \
+ MAX(32UL, ALIGN(_ZS_MIN_ALLOC_RAW, ZS_SIZE_CLASS_DELTA)))
#define ZS_SIZE_CLASSES (DIV_ROUND_UP(ZS_MAX_ALLOC_SIZE - ZS_MIN_ALLOC_SIZE, \
ZS_SIZE_CLASS_DELTA) + 1)
@@ -794,8 +797,10 @@ static struct zpdesc *get_next_zpdesc(struct zpdesc *zpdesc)
static void obj_to_location(unsigned long obj, struct zpdesc **zpdesc,
unsigned int *obj_idx)
{
+ VM_WARN_ON_ONCE(!(obj & ZS_OBJ_IDX_MASK));
+
*zpdesc = pfn_zpdesc(obj >> ZS_OBJ_PFN_SHIFT);
- *obj_idx = (obj & ZS_OBJ_IDX_MASK);
+ *obj_idx = (obj & ZS_OBJ_IDX_MASK) - 1;
}
static void obj_to_zpdesc(unsigned long obj, struct zpdesc **zpdesc)
@@ -814,9 +819,11 @@ static unsigned long location_to_obj(struct zpdesc *zpdesc, unsigned int obj_idx
{
unsigned long obj;
+ VM_WARN_ON_ONCE(obj_idx + 1 > ZS_OBJ_IDX_MASK);
+
obj = zpdesc_pfn(zpdesc) << ZS_OBJ_PFN_SHIFT;
obj |= (unsigned long)(class_idx & ZS_OBJ_CLASS_MASK) << ZS_OBJ_IDX_BITS;
- obj |= obj_idx & ZS_OBJ_IDX_MASK;
+ obj |= (obj_idx + 1) & ZS_OBJ_IDX_MASK;
return obj;
}
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 08/11] zsmalloc: encode 1-based obj_idx so valid obj is never 0
2026-11-10 5:48 ` [PATCH v2 08/11] zsmalloc: encode 1-based obj_idx so valid obj is never 0 Sergey Senozhatsky
@ 2026-11-10 16:44 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 16:44 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:13PM +0900, Sergey Senozhatsky wrote:
> The next patch embeds struct zs_handle into zram and zswap entries,
> and a handle is considered allocated when its obj is not 0. But obj
> can be 0: object 0 of a zspage whose first page is PFN 0 encodes to
> 0, and some platforms do give PFN 0 to the page allocator.
>
> Store obj_idx + 1 in obj, so that a valid obj is never 0, and adjust
> ZS_MIN_ALLOC_SIZE so that the largest index still fits. Unlike a
> separate tag bit, this doesn't take any bits from obj. zsmalloc did
> the same before handles moved to handle_cachep, see
> commit 67296874eb1c ("staging: zsmalloc: Ensure handle is never 0 on
> success").
>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
> ---
> mm/zsmalloc.c | 19 +++++++++++++------
> 1 file changed, 13 insertions(+), 6 deletions(-)
>
> diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
> index 3a6a7253fd01..aa436df0cc4d 100644
> --- a/mm/zsmalloc.c
> +++ b/mm/zsmalloc.c
> @@ -61,7 +61,8 @@
> * Object location (<PFN>, <obj_idx>) is encoded as
> * a single (unsigned long) handle value.
> *
> - * Note that object index <obj_idx> starts from 0.
> + * Note that object index <obj_idx> starts from 0, and is encoded as
> + * <obj_idx> + 1 so that a valid encoded obj value is never 0.
Well, this may be a stupid question, but why not make obj_idx start at
1?
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 09/11] zsmalloc: embed struct zs_handle into zram and zswap entries
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (7 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 08/11] zsmalloc: encode 1-based obj_idx so valid obj is never 0 Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 16:57 ` Yosry Ahmed
2026-11-10 5:48 ` [PATCH v2 10/11] zsmalloc: fold zs_obj_write() into zs_store() Sergey Senozhatsky
` (2 subsequent siblings)
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zsmalloc allocates an object from handle_cachep for every stored
object, just to have a stable address which compaction and page
migration can update when the object moves. Both zram and zswap
already have per-entry structures that hold the handle, so we basically
keep a pointer to a pointer, which costs 8 bytes per stored object,
slab alloc/free on every store and free, and one more memory load on
every access.
Make struct zs_handle public and embed it into zram_table_entry and
zswap_entry (their sizes don't change), so that zsmalloc writes object
location directly there, and remove handle_cachep. zs_malloc() now
takes a handle pointer and returns an error code, zs_free() resets the
handle.
Since compaction and migration update the handle in place, it must be
naturally aligned, must not be on the stack (zs_malloc() warns about
both), must not be copied, and must stay allocated until zs_free() or
zs_rebind_handle(). zram recompression allocates a new object before
it frees the old one, so it uses zram->recomp_handle, protected by
->dev_lock, and zs_rebind_handle() moves the new object to the slot
handle. zram recompression is the only user of zs_rebind_handle().
This saves 8 bytes per stored page, e.g. 2MB per 1GB of swapped out
4K pages. Since handle is per-entry the memory savings scale.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
Documentation/admin-guide/mm/slab.rst | 2 +-
Documentation/mm/zsmalloc.rst | 18 ++--
drivers/block/zram/zram_drv.c | 70 ++++++-------
drivers/block/zram/zram_drv.h | 6 +-
include/linux/zsmalloc.h | 29 +++++-
mm/zsmalloc.c | 144 ++++++++++++++++----------
mm/zswap.c | 22 ++--
7 files changed, 177 insertions(+), 114 deletions(-)
diff --git a/Documentation/admin-guide/mm/slab.rst b/Documentation/admin-guide/mm/slab.rst
index 14429ab90611..57e4a118a391 100644
--- a/Documentation/admin-guide/mm/slab.rst
+++ b/Documentation/admin-guide/mm/slab.rst
@@ -96,7 +96,7 @@ except some that are deemed too performance critical and don't need to be
debugged by specifying global debug options followed by a list of slab names
with "-" as options::
- slab_debug=FZ;-,zs_handle,zspage
+ slab_debug=FZ;-,zspage
The state of each debug option for a slab can be found in the respective files
under::
diff --git a/Documentation/mm/zsmalloc.rst b/Documentation/mm/zsmalloc.rst
index d2bbecd78e14..d6b21a0c0694 100644
--- a/Documentation/mm/zsmalloc.rst
+++ b/Documentation/mm/zsmalloc.rst
@@ -22,13 +22,17 @@ worst case, page is incompressible and is thus stored "as-is" i.e. in
uncompressed form). For allocation requests larger than this size, failure
is returned (see zs_malloc).
-Additionally, zs_malloc() does not return a dereferenceable pointer.
-Instead, it returns an opaque handle (unsigned long) which encodes actual
-location of the allocated object. The reason for this indirection is that
-zsmalloc does not keep zspages permanently mapped since that would cause
-issues on 32-bit systems where the VA region for kernel space mappings
-is very small. So, using the allocated memory should be done through the
-proper handle-based APIs.
+Additionally, zs_malloc() does not return a pointer; instead, it returns 0
+(or a negative error code) and stores the encoded location of the allocated
+object in the caller-provided ``struct zs_handle``. The reason for this
+indirection is that zsmalloc does not keep zspages permanently mapped since
+that would cause issues on 32-bit systems where the VA region for kernel
+space mappings is very small. zsmalloc also keeps a back-pointer to the
+provided ``struct zs_handle`` and updates it asynchronously during
+compaction and page migration, so the owner must not modify or copy the
+handle and must keep it allocated until ``zs_free()`` or
+``zs_rebind_handle()``. Using the allocated memory should be done through
+the proper handle-based APIs.
stat
====
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index aa3ab88f1259..7c3d04d9c5bc 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -950,7 +950,7 @@ static int zram_writeback_complete(struct zram *zram, struct zram_wb_req *req)
if (test_slot_flag(zram, index, ZRAM_HUGE))
atomic64_dec(&zram->stats.huge_pages);
atomic64_sub(get_slot_size(zram, index), &zram->stats.compr_data_size);
- zs_free(zram->mem_pool, zram->table[index].handle);
+ zs_free(zram->mem_pool, &zram->table[index].handle);
zram->table[index].blk_idx = req->blk_idx;
set_slot_flag(zram, index, ZRAM_WB);
@@ -2066,8 +2066,8 @@ static void slot_free(struct zram *zram, unsigned long index)
goto out;
}
- handle = zram->table[index].handle;
- if (!handle)
+ handle = &zram->table[index].handle;
+ if (!zs_handle_is_allocated(handle))
return;
zs_free(zram->mem_pool, handle);
@@ -2098,7 +2098,7 @@ static int read_incompressible_page(struct zram *zram, struct page *page,
struct zs_handle *handle;
void *src, *dst;
- handle = zram->table[index].handle;
+ handle = &zram->table[index].handle;
zs_obj_read_sg_begin(zram->mem_pool, handle, sg, PAGE_SIZE);
/* an incompressible object never spans two pages */
src = kmap_local_page(sg_page(sg));
@@ -2122,7 +2122,7 @@ static int read_compressed_page(struct zram *zram, struct page *page,
void *dst;
int ret, prio;
- handle = zram->table[index].handle;
+ handle = &zram->table[index].handle;
size = get_slot_size(zram, index);
prio = get_slot_comp_priority(zram, index);
@@ -2149,7 +2149,7 @@ static int read_from_zspool_raw(struct zram *zram, struct page *page,
unsigned int size;
void *dst;
- handle = zram->table[index].handle;
+ handle = &zram->table[index].handle;
size = get_slot_size(zram, index);
/*
@@ -2176,7 +2176,7 @@ static int read_from_zspool(struct zram *zram, struct page *page,
unsigned long index)
{
if (test_slot_flag(zram, index, ZRAM_SAME) ||
- !zram->table[index].handle)
+ !zs_handle_is_allocated(&zram->table[index].handle))
return read_same_filled_page(zram, page, index);
if (!test_slot_flag(zram, index, ZRAM_HUGE))
@@ -2256,27 +2256,27 @@ static int write_same_filled_page(struct zram *zram, unsigned long fill,
static int write_incompressible_page(struct zram *zram, struct page *page,
unsigned long index)
{
- struct zs_handle *handle;
void *src;
+ int ret;
slot_lock(zram, index);
slot_free(zram, index);
- handle = zs_malloc(zram->mem_pool, PAGE_SIZE,
- GFP_NOIO | __GFP_NOWARN |
- __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
- if (IS_ERR(handle)) {
+ ret = zs_malloc(zram->mem_pool, &zram->table[index].handle, PAGE_SIZE,
+ GFP_NOIO | __GFP_NOWARN |
+ __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
+ if (ret) {
slot_unlock(zram, index);
- return PTR_ERR(handle);
+ return ret;
}
update_used_max(zram);
src = kmap_local_page(page);
- zs_obj_write(zram->mem_pool, handle, src, PAGE_SIZE);
+ zs_obj_write(zram->mem_pool, &zram->table[index].handle, src,
+ PAGE_SIZE);
kunmap_local(src);
set_slot_flag(zram, index, ZRAM_HUGE);
- zram->table[index].handle = handle;
set_slot_size(zram, index, PAGE_SIZE);
mark_slot_accessed(zram, index);
slot_unlock(zram, index);
@@ -2293,7 +2293,6 @@ static int zram_write_page(struct zram *zram, struct page *page,
unsigned long index)
{
int ret = 0;
- struct zs_handle *handle;
unsigned int comp_len;
void *mem;
struct zcomp_cstrm *zstrm;
@@ -2328,21 +2327,21 @@ static int zram_write_page(struct zram *zram, struct page *page,
slot_lock(zram, index);
slot_free(zram, index);
- handle = zs_malloc(zram->mem_pool, comp_len,
- GFP_NOIO | __GFP_NOWARN |
- __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
- if (IS_ERR(handle)) {
+ ret = zs_malloc(zram->mem_pool, &zram->table[index].handle, comp_len,
+ GFP_NOIO | __GFP_NOWARN |
+ __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
+ if (ret) {
zcomp_cstrm_put(zstrm);
slot_unlock(zram, index);
- return PTR_ERR(handle);
+ return ret;
}
update_used_max(zram);
- zs_obj_write(zram->mem_pool, handle, zstrm->buffer, comp_len);
+ zs_obj_write(zram->mem_pool, &zram->table[index].handle, zstrm->buffer,
+ comp_len);
zcomp_cstrm_put(zstrm);
- zram->table[index].handle = handle;
set_slot_size(zram, index, comp_len);
mark_slot_accessed(zram, index);
slot_unlock(zram, index);
@@ -2449,8 +2448,6 @@ static int recompress_slot(struct zram *zram, unsigned long index,
u32 threshold, u32 prio)
{
struct zcomp_cstrm *zstrm = NULL;
- struct zs_handle *handle_old;
- struct zs_handle *handle_new;
unsigned int comp_len_old;
unsigned int comp_len_new;
unsigned int class_index_old;
@@ -2458,8 +2455,9 @@ static int recompress_slot(struct zram *zram, unsigned long index,
void *src;
int ret = 0;
- handle_old = zram->table[index].handle;
- if (!handle_old)
+ lockdep_assert_held_write(&zram->dev_lock);
+
+ if (!zs_handle_is_allocated(&zram->table[index].handle))
return -EINVAL;
comp_len_old = get_slot_size(zram, index);
@@ -2528,20 +2526,22 @@ static int recompress_slot(struct zram *zram, unsigned long index,
* modify zsmalloc API to return this information. For now, we will
* make do with the node of the page allocated for recompression.
*/
- handle_new = zs_malloc(zram->mem_pool, comp_len_new,
- GFP_NOIO | __GFP_NOWARN |
- __GFP_HIGHMEM | __GFP_MOVABLE,
- page_to_nid(page));
- if (IS_ERR(handle_new)) {
+ ret = zs_malloc(zram->mem_pool, &zram->recomp_handle, comp_len_new,
+ GFP_NOIO | __GFP_NOWARN |
+ __GFP_HIGHMEM | __GFP_MOVABLE,
+ page_to_nid(page));
+ if (ret) {
zcomp_cstrm_put(zstrm);
- return PTR_ERR(handle_new);
+ return ret;
}
- zs_obj_write(zram->mem_pool, handle_new, zstrm->buffer, comp_len_new);
+ zs_obj_write(zram->mem_pool, &zram->recomp_handle, zstrm->buffer,
+ comp_len_new);
zcomp_cstrm_put(zstrm);
slot_free(zram, index);
- zram->table[index].handle = handle_new;
+ zs_rebind_handle(zram->mem_pool, &zram->table[index].handle,
+ &zram->recomp_handle);
set_slot_size(zram, index, comp_len_new);
set_slot_comp_priority(zram, index, prio);
diff --git a/drivers/block/zram/zram_drv.h b/drivers/block/zram/zram_drv.h
index 5b211a9f314c..d64c99b74214 100644
--- a/drivers/block/zram/zram_drv.h
+++ b/drivers/block/zram/zram_drv.h
@@ -79,7 +79,7 @@ enum zram_pageflags {
*/
struct zram_table_entry {
union {
- struct zs_handle *handle;
+ struct zs_handle handle;
unsigned long element;
unsigned long blk_idx;
};
@@ -159,6 +159,10 @@ struct zram {
unsigned long bitmap_cursor;
unsigned long nr_pages;
#endif
+#ifdef CONFIG_ZRAM_MULTI_COMP
+ /* Pseudo slot for recompression, protected by ->dev_lock */
+ struct zs_handle recomp_handle;
+#endif
#ifdef CONFIG_ZRAM_MEMORY_TRACKING
struct dentry *debugfs_dir;
#endif
diff --git a/include/linux/zsmalloc.h b/include/linux/zsmalloc.h
index 7fa500ae2653..8273ae924153 100644
--- a/include/linux/zsmalloc.h
+++ b/include/linux/zsmalloc.h
@@ -14,6 +14,7 @@
#ifndef _ZS_MALLOC_H_
#define _ZS_MALLOC_H_
+#include <linux/compiler.h>
#include <linux/types.h>
struct zs_pool_stats {
@@ -21,16 +22,38 @@ struct zs_pool_stats {
atomic_long_t pages_compacted;
};
+/*
+ * zsmalloc writes object location into the handle, keeps a back-pointer to
+ * it, and updates it asynchronously when the object moves (compaction, page
+ * migration). Therefore the owner must not modify or copy the handle, must
+ * not put it on the stack or misalign it (e.g. inside a __packed struct),
+ * and must keep it allocated until zs_free() or zs_rebind_handle().
+ */
+struct zs_handle {
+ unsigned long __private obj;
+};
+
+static inline void zs_handle_init(struct zs_handle *handle)
+{
+ ACCESS_PRIVATE(handle, obj) = 0;
+}
+
+static inline bool zs_handle_is_allocated(const struct zs_handle *handle)
+{
+ return READ_ONCE(ACCESS_PRIVATE(handle, obj)) != 0;
+}
+
struct zs_pool;
-struct zs_handle;
struct scatterlist;
struct zs_pool *zs_create_pool(const char *name);
void zs_destroy_pool(struct zs_pool *pool);
-struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t flags,
- const int nid);
+int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
+ gfp_t flags, const int nid);
void zs_free(struct zs_pool *pool, struct zs_handle *handle);
+void zs_rebind_handle(struct zs_pool *pool, struct zs_handle *dst,
+ struct zs_handle *src);
size_t zs_huge_class_size(struct zs_pool *pool);
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index aa436df0cc4d..fd466139e158 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -22,14 +22,15 @@
* class->lock
* zspage->lock
*
- * When ZS_OBJ_CLASS_BITS > 0, zs_free() skips pool->lock; it picks
- * the size_class from obj's encoded class_idx and serializes against
- * page migration via class->lock.
+ * When ZS_OBJ_CLASS_BITS > 0, zs_free() and zs_rebind_handle() skip
+ * pool->lock; they pick the size_class from obj's encoded class_idx and
+ * serialize against page migration via class->lock.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
+#include <linux/sched/task_stack.h>
#include <linux/errno.h>
#include <linux/highmem.h>
#include <linux/string.h>
@@ -104,10 +105,10 @@
* |
* +-- ZS_OBJ_PFN_SHIFT
*
- * Encoding class_idx into obj lets zs_free() locate the size_class
- * without holding pool->lock; class_idx is invariant across page
- * migration (only PFN changes), so a lockless read of the obj value
- * always yields a valid class_idx.
+ * Encoding class_idx into obj lets zs_free() and zs_rebind_handle()
+ * locate the size_class without holding pool->lock; class_idx is
+ * invariant across page migration (only PFN changes), so a lockless
+ * read of the obj value always yields a valid class_idx.
*/
#define ZS_OBJ_PFN_SHIFT (BITS_PER_LONG - _PFN_BITS)
@@ -267,11 +268,6 @@ struct link_free {
};
};
-struct zs_handle {
- unsigned long __private obj;
-};
-
-static struct kmem_cache *handle_cachep;
static struct kmem_cache *zspage_cachep;
struct zs_pool {
@@ -445,18 +441,6 @@ static void init_deferred_free(struct zs_pool *pool) {}
static void SetZsPageMovable(struct zs_pool *pool, struct zspage *zspage) {}
#endif
-static struct zs_handle *cache_alloc_handle(gfp_t gfp)
-{
- gfp = gfp & ~(__GFP_HIGHMEM | __GFP_MOVABLE);
-
- return kmem_cache_alloc(handle_cachep, gfp);
-}
-
-static void cache_free_handle(struct zs_handle *handle)
-{
- kmem_cache_free(handle_cachep, handle);
-}
-
static struct zspage *cache_alloc_zspage(gfp_t gfp)
{
gfp = gfp & ~(__GFP_HIGHMEM | __GFP_MOVABLE);
@@ -470,8 +454,9 @@ static void cache_free_zspage(struct zspage *zspage)
}
/*
- * Pairs with READ_ONCE() in handle_to_obj(): zs_free() may read the
- * handle locklessly, so prevent store tearing here.
+ * Pairs with READ_ONCE() in handle_to_obj() and zs_handle_is_allocated():
+ * zs_free(), zs_rebind_handle(), and zs_handle_is_allocated() may read
+ * the handle locklessly, so prevent store tearing here.
*/
static void record_obj(struct zs_handle *handle, unsigned long obj)
{
@@ -1307,31 +1292,37 @@ static unsigned long obj_malloc(struct zs_pool *pool,
/**
* zs_malloc - Allocate block of given size from pool.
* @pool: pool to allocate from
+ * @handle: handle to store allocated object in
* @size: size of block to allocate
* @gfp: gfp flags when allocating object
* @nid: The preferred node id to allocate new zspage (if needed)
*
- * On success, handle to the allocated object is returned,
- * otherwise an ERR_PTR().
* Allocation requests with size > ZS_MAX_ALLOC_SIZE will fail.
+ * zsmalloc keeps a back-pointer to @handle and updates it asynchronously
+ * when the object moves, so @handle must be unsigned-long aligned, must
+ * not be on the stack or modified/copied by the caller, and must remain
+ * allocated until zs_free() or zs_rebind_handle().
+ *
+ * Return: 0 on success, negative error code otherwise.
*/
-struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
- const int nid)
+int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
+ gfp_t gfp, const int nid)
{
- struct zs_handle *handle;
struct size_class *class;
int newfg;
struct zspage *zspage;
+ if (WARN_ON_ONCE(!IS_ALIGNED((unsigned long)handle,
+ __alignof__(unsigned long)) ||
+ object_is_on_stack(handle) ||
+ zs_handle_is_allocated(handle)))
+ return -EINVAL;
+
if (unlikely(!size))
- return ERR_PTR(-EINVAL);
+ return -EINVAL;
if (unlikely(size > ZS_MAX_ALLOC_SIZE))
- return ERR_PTR(-ENOSPC);
-
- handle = cache_alloc_handle(gfp);
- if (!handle)
- return ERR_PTR(-ENOMEM);
+ return -ENOSPC;
class = lookup_size_class(pool, size);
@@ -1350,10 +1341,8 @@ struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
spin_unlock(&class->lock);
zspage = alloc_zspage(pool, class, gfp, nid);
- if (!zspage) {
- cache_free_handle(handle);
- return ERR_PTR(-ENOMEM);
- }
+ if (!zspage)
+ return -ENOMEM;
spin_lock(&class->lock);
obj_malloc(pool, zspage, handle);
@@ -1368,7 +1357,7 @@ struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
out:
spin_unlock(&class->lock);
- return handle;
+ return 0;
}
EXPORT_SYMBOL_GPL(zs_malloc);
@@ -1469,13 +1458,14 @@ void zs_free(struct zs_pool *pool, struct zs_handle *handle)
int fullness;
struct zspage *zspage_to_free = NULL;
- if (IS_ERR_OR_NULL(handle))
+ if (!zs_handle_is_allocated(handle))
return;
obj_class_get_and_lock(pool, handle, &obj, &zspage, &class);
class_stat_sub(class, ZS_OBJS_INUSE, 1);
obj_free(class->size, obj);
+ record_obj(handle, 0);
fullness = fix_fullness_group(class, zspage);
if (fullness == ZS_INUSE_RATIO_0) {
@@ -1495,10 +1485,59 @@ void zs_free(struct zs_pool *pool, struct zs_handle *handle)
__free_zspage_lockless(zspage_to_free);
atomic_long_sub(class->pages_per_zspage, &pool->pages_allocated);
}
- cache_free_handle(handle);
}
EXPORT_SYMBOL_GPL(zs_free);
+/**
+ * zs_rebind_handle() - Rebind an allocated object to a new handle.
+ * @pool: pool the object belongs to
+ * @dst: unallocated destination handle (must not be on the stack)
+ * @src: source handle that currently owns the object
+ *
+ * Rebinds the object from @src to @dst, updating the object header so
+ * compaction and page migration update @dst instead of @src, and resets
+ * @src.
+ */
+void zs_rebind_handle(struct zs_pool *pool, struct zs_handle *dst,
+ struct zs_handle *src)
+{
+ struct size_class *class;
+ struct zspage *zspage;
+ struct zpdesc *zpdesc;
+ struct link_free *link;
+ unsigned int obj_idx;
+ unsigned long obj;
+ unsigned long off;
+ void *vaddr;
+
+ /* Should never happen; bail out rather than corrupting handles */
+ if (WARN_ON_ONCE(!IS_ALIGNED((unsigned long)dst,
+ __alignof__(unsigned long)) ||
+ object_is_on_stack(dst) ||
+ zs_handle_is_allocated(dst) ||
+ !zs_handle_is_allocated(src)))
+ return;
+
+ obj_class_get_and_lock(pool, src, &obj, &zspage, &class);
+
+ if (likely(!ZsHugePage(zspage))) {
+ obj_to_location(obj, &zpdesc, &obj_idx);
+ off = offset_in_page(class->size * obj_idx);
+ vaddr = kmap_local_zpdesc(zpdesc);
+ link = (struct link_free *)(vaddr + off);
+ link->handle = (unsigned long)dst | OBJ_ALLOCATED_TAG;
+ kunmap_local(vaddr);
+ } else {
+ zspage->first_zpdesc->handle =
+ (unsigned long)dst | OBJ_ALLOCATED_TAG;
+ }
+
+ record_obj(dst, obj);
+ record_obj(src, 0);
+ spin_unlock(&class->lock);
+}
+EXPORT_SYMBOL_GPL(zs_rebind_handle);
+
static void zs_object_copy(struct size_class *class, unsigned long dst,
unsigned long src)
{
@@ -1795,7 +1834,8 @@ static int zs_page_migrate(struct page *newpage, struct page *page,
/*
* The pool migrate_lock protects against races between zpage migration
- * and zs_free(), but only when ZS_OBJ_CLASS_BITS does not apply.
+ * and zs_free() / zs_rebind_handle(), but only when ZS_OBJ_CLASS_BITS
+ * does not apply.
*/
write_lock(&pool->lock);
class = zspage_class(pool, zspage);
@@ -1974,9 +2014,9 @@ static unsigned long __zs_compact(struct zs_pool *pool,
unsigned long pages_freed = 0;
/*
- * Protect against races between zpage migration and zs_free()
- * (only when ZS_OBJ_CLASS_BITS does not apply), as well as
- * zpage allocation and free.
+ * Protect against races between zpage migration and zs_free() /
+ * zs_rebind_handle() (only when ZS_OBJ_CLASS_BITS does not apply),
+ * as well as zpage allocation and free.
*/
write_lock(&pool->lock);
spin_lock(&class->lock);
@@ -2298,23 +2338,17 @@ EXPORT_SYMBOL_GPL(zs_destroy_pool);
static void zs_destroy_caches(void)
{
- kmem_cache_destroy(handle_cachep);
- handle_cachep = NULL;
kmem_cache_destroy(zspage_cachep);
zspage_cachep = NULL;
}
static int __init zs_init_caches(void)
{
- handle_cachep = kmem_cache_create("zs_handle", sizeof(struct zs_handle),
- 0, 0, NULL);
zspage_cachep = kmem_cache_create("zspage", sizeof(struct zspage),
0, 0, NULL);
- if (!handle_cachep || !zspage_cachep) {
- zs_destroy_caches();
+ if (!zspage_cachep)
return -ENOMEM;
- }
return 0;
}
diff --git a/mm/zswap.c b/mm/zswap.c
index dc0400d7e1a3..241f9fdebaad 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -216,7 +216,7 @@ struct zswap_entry {
unsigned int length;
bool referenced;
u8 pool_idx;
- struct zs_handle *handle;
+ struct zs_handle handle;
struct obj_cgroup *objcg;
struct list_head lru;
};
@@ -770,6 +770,7 @@ static struct zswap_entry *zswap_entry_cache_alloc(gfp_t gfp, int nid)
entry = kmem_cache_alloc_node(zswap_entry_cache, gfp, nid);
if (!entry)
return NULL;
+ zs_handle_init(&entry->handle);
return entry;
}
@@ -790,7 +791,7 @@ static void zswap_entry_free(struct zswap_entry *entry)
if (WARN_ON_ONCE(!pool))
return;
- zs_free(pool->zs_pool, entry->handle);
+ zs_free(pool->zs_pool, &entry->handle);
zswap_pool_put(pool);
if (entry->objcg) {
obj_cgroup_uncharge_zswap(entry->objcg, entry->length);
@@ -890,7 +891,6 @@ static bool zswap_compress(struct folio *folio, long index,
struct scatterlist input, output;
int comp_ret = 0, alloc_ret = 0;
unsigned int dlen = PAGE_SIZE;
- struct zs_handle *handle;
gfp_t gfp;
u8 *dst;
bool mapped = false;
@@ -950,14 +950,12 @@ static bool zswap_compress(struct folio *folio, long index,
}
gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
- handle = zs_malloc(pool->zs_pool, dlen, gfp, folio_nid(folio));
- if (IS_ERR(handle)) {
- alloc_ret = PTR_ERR(handle);
+ alloc_ret = zs_malloc(pool->zs_pool, &entry->handle, dlen, gfp,
+ folio_nid(folio));
+ if (alloc_ret)
goto unlock;
- }
- zs_obj_write(pool->zs_pool, handle, dst, dlen);
- entry->handle = handle;
+ zs_obj_write(pool->zs_pool, &entry->handle, dst, dlen);
entry->length = dlen;
unlock:
@@ -982,7 +980,7 @@ static bool __zswap_decompress(struct zswap_entry *entry,
struct scatterlist output;
int ret = 0, dlen;
- zs_obj_read_sg_begin(pool->zs_pool, entry->handle, input, entry->length);
+ zs_obj_read_sg_begin(pool->zs_pool, &entry->handle, input, entry->length);
/* zswap entries of length PAGE_SIZE are not compressed. */
if (entry->length == PAGE_SIZE) {
@@ -1004,7 +1002,7 @@ static bool __zswap_decompress(struct zswap_entry *entry,
dlen = req->dlen;
}
- zs_obj_read_sg_end(pool->zs_pool, entry->handle);
+ zs_obj_read_sg_end(pool->zs_pool, &entry->handle);
if (!ret && dlen == PAGE_SIZE)
return true;
@@ -1608,7 +1606,7 @@ static bool zswap_store_page(struct folio *folio, long index,
return true;
store_failed:
- zs_free(pool->zs_pool, entry->handle);
+ zs_free(pool->zs_pool, &entry->handle);
compress_failed:
zswap_entry_cache_free(entry);
return false;
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 09/11] zsmalloc: embed struct zs_handle into zram and zswap entries
2026-11-10 5:48 ` [PATCH v2 09/11] zsmalloc: embed struct zs_handle into zram and zswap entries Sergey Senozhatsky
@ 2026-11-10 16:57 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 16:57 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:14PM +0900, Sergey Senozhatsky wrote:
> zsmalloc allocates an object from handle_cachep for every stored
> object, just to have a stable address which compaction and page
> migration can update when the object moves. Both zram and zswap
> already have per-entry structures that hold the handle, so we basically
> keep a pointer to a pointer, which costs 8 bytes per stored object,
> slab alloc/free on every store and free, and one more memory load on
> every access.
>
> Make struct zs_handle public and embed it into zram_table_entry and
> zswap_entry (their sizes don't change), so that zsmalloc writes object
> location directly there, and remove handle_cachep. zs_malloc() now
> takes a handle pointer and returns an error code, zs_free() resets the
> handle.
>
> Since compaction and migration update the handle in place, it must be
> naturally aligned, must not be on the stack (zs_malloc() warns about
> both), must not be copied, and must stay allocated until zs_free() or
> zs_rebind_handle(). zram recompression allocates a new object before
> it frees the old one, so it uses zram->recomp_handle, protected by
> ->dev_lock, and zs_rebind_handle() moves the new object to the slot
> handle. zram recompression is the only user of zs_rebind_handle().
>
> This saves 8 bytes per stored page, e.g. 2MB per 1GB of swapped out
> 4K pages. Since handle is per-entry the memory savings scale.
>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
> ---
> Documentation/admin-guide/mm/slab.rst | 2 +-
> Documentation/mm/zsmalloc.rst | 18 ++--
> drivers/block/zram/zram_drv.c | 70 ++++++-------
> drivers/block/zram/zram_drv.h | 6 +-
> include/linux/zsmalloc.h | 29 +++++-
> mm/zsmalloc.c | 144 ++++++++++++++++----------
> mm/zswap.c | 22 ++--
> 7 files changed, 177 insertions(+), 114 deletions(-)
zsmalloc and zswap bits LGTM with a small nit below:
Reviewed-by: Yosry Ahmed <yosry@kernel.org>
[..]
>
> /*
> - * Pairs with READ_ONCE() in handle_to_obj(): zs_free() may read the
> - * handle locklessly, so prevent store tearing here.
> + * Pairs with READ_ONCE() in handle_to_obj() and zs_handle_is_allocated():
> + * zs_free(), zs_rebind_handle(), and zs_handle_is_allocated() may read
> + * the handle locklessly, so prevent store tearing here.
> */
> static void record_obj(struct zs_handle *handle, unsigned long obj)
> {
> @@ -1307,31 +1292,37 @@ static unsigned long obj_malloc(struct zs_pool *pool,
> /**
> * zs_malloc - Allocate block of given size from pool.
> * @pool: pool to allocate from
> + * @handle: handle to store allocated object in
> * @size: size of block to allocate
> * @gfp: gfp flags when allocating object
> * @nid: The preferred node id to allocate new zspage (if needed)
> *
> - * On success, handle to the allocated object is returned,
> - * otherwise an ERR_PTR().
> * Allocation requests with size > ZS_MAX_ALLOC_SIZE will fail.
> + * zsmalloc keeps a back-pointer to @handle and updates it asynchronously
> + * when the object moves, so @handle must be unsigned-long aligned, must
> + * not be on the stack or modified/copied by the caller, and must remain
> + * allocated until zs_free() or zs_rebind_handle().
> + *
> + * Return: 0 on success, negative error code otherwise.
> */
> -struct zs_handle *zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp,
> - const int nid)
> +int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
> + gfp_t gfp, const int nid)
> {
> - struct zs_handle *handle;
> struct size_class *class;
> int newfg;
> struct zspage *zspage;
>
> + if (WARN_ON_ONCE(!IS_ALIGNED((unsigned long)handle,
> + __alignof__(unsigned long)) ||
> + object_is_on_stack(handle) ||
> + zs_handle_is_allocated(handle)))
Nit: Put this in a helper to reuse in zs_rebind_handle().
> + return -EINVAL;
> +
[..]
> +/**
> + * zs_rebind_handle() - Rebind an allocated object to a new handle.
> + * @pool: pool the object belongs to
> + * @dst: unallocated destination handle (must not be on the stack)
> + * @src: source handle that currently owns the object
> + *
> + * Rebinds the object from @src to @dst, updating the object header so
> + * compaction and page migration update @dst instead of @src, and resets
> + * @src.
> + */
> +void zs_rebind_handle(struct zs_pool *pool, struct zs_handle *dst,
> + struct zs_handle *src)
> +{
> + struct size_class *class;
> + struct zspage *zspage;
> + struct zpdesc *zpdesc;
> + struct link_free *link;
> + unsigned int obj_idx;
> + unsigned long obj;
> + unsigned long off;
> + void *vaddr;
> +
> + /* Should never happen; bail out rather than corrupting handles */
> + if (WARN_ON_ONCE(!IS_ALIGNED((unsigned long)dst,
> + __alignof__(unsigned long)) ||
> + object_is_on_stack(dst) ||
> + zs_handle_is_allocated(dst) ||
> + !zs_handle_is_allocated(src)))
> + return;
[..]
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 10/11] zsmalloc: fold zs_obj_write() into zs_store()
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (8 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 09/11] zsmalloc: embed struct zs_handle into zram and zswap entries Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 17:00 ` Yosry Ahmed
2026-11-10 5:48 ` [PATCH v2 11/11] zsmalloc: populate new zspage outside class->lock in zs_store() Sergey Senozhatsky
2026-11-10 16:16 ` [PATCH v2 00/11] zsmalloc: redesign object alloc API Yosry Ahmed
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky
zram and zswap always call zs_obj_write() right after zs_malloc(), and
zs_obj_write() has to take pool->lock and look up object location
again, which zs_malloc() just did.
Replace zs_malloc() and zs_obj_write() with one single zs_store()
call.
zs_store() takes zspage read lock under class->lock, before it drops
class->lock to copy the data, while zs_obj_write() used to take it
under pool->lock. Compaction and page migration already take zspage
lock under class->lock, so this follows the existing pool->lock ->
class->lock -> zspage->lock order. We don't need pool->lock there,
zs_store() doesn't look up the object via the handle, it already has
the zspage from the allocation.
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
Documentation/core-api/memory-allocation.rst | 2 +-
Documentation/mm/zsmalloc.rst | 4 +-
drivers/block/zram/zram_drv.c | 42 +++++---------
include/linux/zsmalloc.h | 6 +-
mm/zsmalloc.c | 58 +++++++++-----------
mm/zswap.c | 5 +-
6 files changed, 47 insertions(+), 70 deletions(-)
diff --git a/Documentation/core-api/memory-allocation.rst b/Documentation/core-api/memory-allocation.rst
index 648222b6c857..5069ac72b857 100644
--- a/Documentation/core-api/memory-allocation.rst
+++ b/Documentation/core-api/memory-allocation.rst
@@ -9,7 +9,7 @@ allocate small chunks using `kmalloc` or `kmem_cache_alloc` families,
large virtually contiguous areas using `vmalloc` and its derivatives,
or you can directly request pages from the page allocator with
`alloc_pages`. It is also possible to use more specialized allocators,
-for instance `cma_alloc` or `zs_malloc`.
+for instance `cma_alloc` or `zs_store`.
Most of the memory allocation APIs use GFP flags to express how that
memory should be allocated. The GFP acronym stands for "get free
diff --git a/Documentation/mm/zsmalloc.rst b/Documentation/mm/zsmalloc.rst
index d6b21a0c0694..034daae0d37e 100644
--- a/Documentation/mm/zsmalloc.rst
+++ b/Documentation/mm/zsmalloc.rst
@@ -20,9 +20,9 @@ For simplicity, zsmalloc can only allocate objects of size up to PAGE_SIZE
since this satisfies the requirements of all its current users (in the
worst case, page is incompressible and is thus stored "as-is" i.e. in
uncompressed form). For allocation requests larger than this size, failure
-is returned (see zs_malloc).
+is returned (see zs_store).
-Additionally, zs_malloc() does not return a pointer; instead, it returns 0
+Additionally, zs_store() does not return a pointer; instead, it returns 0
(or a negative error code) and stores the encoded location of the allocated
object in the caller-provided ``struct zs_handle``. The reason for this
indirection is that zsmalloc does not keep zspages permanently mapped since
diff --git a/drivers/block/zram/zram_drv.c b/drivers/block/zram/zram_drv.c
index 7c3d04d9c5bc..42852a4116e0 100644
--- a/drivers/block/zram/zram_drv.c
+++ b/drivers/block/zram/zram_drv.c
@@ -2261,9 +2261,11 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
slot_lock(zram, index);
slot_free(zram, index);
- ret = zs_malloc(zram->mem_pool, &zram->table[index].handle, PAGE_SIZE,
- GFP_NOIO | __GFP_NOWARN |
- __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
+ src = kmap_local_page(page);
+ ret = zs_store(zram->mem_pool, &zram->table[index].handle, src,
+ PAGE_SIZE, GFP_NOIO | __GFP_NOWARN |
+ __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
+ kunmap_local(src);
if (ret) {
slot_unlock(zram, index);
return ret;
@@ -2271,11 +2273,6 @@ static int write_incompressible_page(struct zram *zram, struct page *page,
update_used_max(zram);
- src = kmap_local_page(page);
- zs_obj_write(zram->mem_pool, &zram->table[index].handle, src,
- PAGE_SIZE);
- kunmap_local(src);
-
set_slot_flag(zram, index, ZRAM_HUGE);
set_slot_size(zram, index, PAGE_SIZE);
mark_slot_accessed(zram, index);
@@ -2327,21 +2324,18 @@ static int zram_write_page(struct zram *zram, struct page *page,
slot_lock(zram, index);
slot_free(zram, index);
- ret = zs_malloc(zram->mem_pool, &zram->table[index].handle, comp_len,
- GFP_NOIO | __GFP_NOWARN |
- __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
+ ret = zs_store(zram->mem_pool, &zram->table[index].handle,
+ zstrm->buffer, comp_len,
+ GFP_NOIO | __GFP_NOWARN |
+ __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
+ zcomp_cstrm_put(zstrm);
if (ret) {
- zcomp_cstrm_put(zstrm);
slot_unlock(zram, index);
return ret;
}
update_used_max(zram);
- zs_obj_write(zram->mem_pool, &zram->table[index].handle, zstrm->buffer,
- comp_len);
- zcomp_cstrm_put(zstrm);
-
set_slot_size(zram, index, comp_len);
mark_slot_accessed(zram, index);
slot_unlock(zram, index);
@@ -2526,18 +2520,12 @@ static int recompress_slot(struct zram *zram, unsigned long index,
* modify zsmalloc API to return this information. For now, we will
* make do with the node of the page allocated for recompression.
*/
- ret = zs_malloc(zram->mem_pool, &zram->recomp_handle, comp_len_new,
- GFP_NOIO | __GFP_NOWARN |
- __GFP_HIGHMEM | __GFP_MOVABLE,
- page_to_nid(page));
- if (ret) {
- zcomp_cstrm_put(zstrm);
- return ret;
- }
-
- zs_obj_write(zram->mem_pool, &zram->recomp_handle, zstrm->buffer,
- comp_len_new);
+ ret = zs_store(zram->mem_pool, &zram->recomp_handle, zstrm->buffer,
+ comp_len_new, GFP_NOIO | __GFP_NOWARN |
+ __GFP_HIGHMEM | __GFP_MOVABLE, page_to_nid(page));
zcomp_cstrm_put(zstrm);
+ if (ret)
+ return ret;
slot_free(zram, index);
zs_rebind_handle(zram->mem_pool, &zram->table[index].handle,
diff --git a/include/linux/zsmalloc.h b/include/linux/zsmalloc.h
index 8273ae924153..8971eef333ba 100644
--- a/include/linux/zsmalloc.h
+++ b/include/linux/zsmalloc.h
@@ -49,8 +49,8 @@ struct scatterlist;
struct zs_pool *zs_create_pool(const char *name);
void zs_destroy_pool(struct zs_pool *pool);
-int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
- gfp_t flags, const int nid);
+int zs_store(struct zs_pool *pool, struct zs_handle *handle, const void *src,
+ size_t size, gfp_t flags, const int nid);
void zs_free(struct zs_pool *pool, struct zs_handle *handle);
void zs_rebind_handle(struct zs_pool *pool, struct zs_handle *dst,
struct zs_handle *src);
@@ -67,8 +67,6 @@ void zs_pool_stats(struct zs_pool *pool, struct zs_pool_stats *stats);
void zs_obj_read_sg_begin(struct zs_pool *pool, const struct zs_handle *handle,
struct scatterlist *sg, size_t mem_len);
void zs_obj_read_sg_end(struct zs_pool *pool, const struct zs_handle *handle);
-void zs_obj_write(struct zs_pool *pool, const struct zs_handle *handle,
- void *handle_mem, size_t mem_len);
extern const struct movable_operations zsmalloc_mops;
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index fd466139e158..09ce8c294c5e 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -1178,54 +1178,37 @@ void zs_obj_read_sg_end(struct zs_pool *pool, const struct zs_handle *handle)
}
EXPORT_SYMBOL_GPL(zs_obj_read_sg_end);
-void zs_obj_write(struct zs_pool *pool, const struct zs_handle *handle,
- void *handle_mem, size_t mem_len)
+static void obj_write(struct size_class *class, struct zspage *zspage,
+ unsigned long obj, const void *src, size_t size)
{
- struct zspage *zspage;
struct zpdesc *zpdesc;
- unsigned long obj, off;
+ unsigned long off;
unsigned int obj_idx;
- struct size_class *class;
- /* Guarantee we can get zspage from handle safely */
- read_lock(&pool->lock);
- obj = handle_to_obj(handle);
obj_to_location(obj, &zpdesc, &obj_idx);
- zspage = get_zspage(zpdesc);
-
- /* Make sure migration doesn't move any pages in this zspage */
- zspage_read_lock(zspage);
- read_unlock(&pool->lock);
-
- class = zspage_class(pool, zspage);
off = offset_in_page(class->size * obj_idx);
if (!ZsHugePage(zspage))
off += ZS_HANDLE_SIZE;
- if (off + mem_len <= PAGE_SIZE) {
+ if (off + size <= PAGE_SIZE) {
/* this object is contained entirely within a page */
void *dst = kmap_local_zpdesc(zpdesc);
- memcpy(dst + off, handle_mem, mem_len);
+ memcpy(dst + off, src, size);
kunmap_local(dst);
} else {
/* this object spans two pages */
size_t sizes[2];
sizes[0] = PAGE_SIZE - off;
- sizes[1] = mem_len - sizes[0];
+ sizes[1] = size - sizes[0];
- memcpy_to_page(zpdesc_page(zpdesc), off,
- handle_mem, sizes[0]);
+ memcpy_to_page(zpdesc_page(zpdesc), off, src, sizes[0]);
zpdesc = get_next_zpdesc(zpdesc);
- memcpy_to_page(zpdesc_page(zpdesc), 0,
- handle_mem + sizes[0], sizes[1]);
+ memcpy_to_page(zpdesc_page(zpdesc), 0, src + sizes[0], sizes[1]);
}
-
- zspage_read_unlock(zspage);
}
-EXPORT_SYMBOL_GPL(zs_obj_write);
/**
* zs_huge_class_size() - Returns the size (in bytes) of the first huge
@@ -1290,9 +1273,10 @@ static unsigned long obj_malloc(struct zs_pool *pool,
/**
- * zs_malloc - Allocate block of given size from pool.
+ * zs_store() - Allocate block of given size from pool and copy data into it.
* @pool: pool to allocate from
* @handle: handle to store allocated object in
+ * @src: data to copy into the allocated object
* @size: size of block to allocate
* @gfp: gfp flags when allocating object
* @nid: The preferred node id to allocate new zspage (if needed)
@@ -1305,10 +1289,11 @@ static unsigned long obj_malloc(struct zs_pool *pool,
*
* Return: 0 on success, negative error code otherwise.
*/
-int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
- gfp_t gfp, const int nid)
+int zs_store(struct zs_pool *pool, struct zs_handle *handle, const void *src,
+ size_t size, gfp_t gfp, const int nid)
{
struct size_class *class;
+ unsigned long obj;
int newfg;
struct zspage *zspage;
@@ -1330,7 +1315,7 @@ int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
spin_lock(&class->lock);
zspage = find_get_zspage(class);
if (likely(zspage)) {
- obj_malloc(pool, zspage, handle);
+ obj = obj_malloc(pool, zspage, handle);
/* Now move the zspage to another fullness group, if required */
fix_fullness_group(class, zspage);
class_stat_add(class, ZS_OBJS_INUSE, 1);
@@ -1345,7 +1330,7 @@ int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
return -ENOMEM;
spin_lock(&class->lock);
- obj_malloc(pool, zspage, handle);
+ obj = obj_malloc(pool, zspage, handle);
newfg = get_fullness_group(class, zspage);
insert_zspage(class, zspage, newfg);
atomic_long_add(class->pages_per_zspage, &pool->pages_allocated);
@@ -1355,11 +1340,15 @@ int zs_malloc(struct zs_pool *pool, struct zs_handle *handle, size_t size,
/* We completely set up zspage so mark them as movable */
SetZsPageMovable(pool, zspage);
out:
+ zspage_read_lock(zspage);
spin_unlock(&class->lock);
+ obj_write(class, zspage, obj, src, size);
+ zspage_read_unlock(zspage);
+
return 0;
}
-EXPORT_SYMBOL_GPL(zs_malloc);
+EXPORT_SYMBOL_GPL(zs_store);
static void obj_free(int class_size, unsigned long obj)
{
@@ -1844,7 +1833,10 @@ static int zs_page_migrate(struct page *newpage, struct page *page,
* the class lock protects zpage alloc/free in the zspage.
*/
spin_lock(&class->lock);
- /* the zspage write_lock protects zpage access via zs_obj_read/write() */
+ /*
+ * The zspage write_lock protects zpage access via zs_obj_read_sg_*()
+ * and zs_store().
+ */
if (!zspage_write_trylock(zspage)) {
spin_unlock(&class->lock);
write_unlock(&pool->lock);
@@ -2238,7 +2230,7 @@ struct zs_pool *zs_create_pool(const char *name)
huge_class_size = size;
/*
* The object uses ZS_HANDLE_SIZE bytes to store the
- * handle. We need to subtract it, because zs_malloc()
+ * handle. We need to subtract it, because zs_store()
* unconditionally adds handle size before it performs
* size class search - so object may be smaller than
* huge class size, yet it still can end up in the huge
diff --git a/mm/zswap.c b/mm/zswap.c
index 241f9fdebaad..a2e0314db1cf 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -950,12 +950,11 @@ static bool zswap_compress(struct folio *folio, long index,
}
gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
- alloc_ret = zs_malloc(pool->zs_pool, &entry->handle, dlen, gfp,
- folio_nid(folio));
+ alloc_ret = zs_store(pool->zs_pool, &entry->handle, dst, dlen, gfp,
+ folio_nid(folio));
if (alloc_ret)
goto unlock;
- zs_obj_write(pool->zs_pool, &entry->handle, dst, dlen);
entry->length = dlen;
unlock:
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 10/11] zsmalloc: fold zs_obj_write() into zs_store()
2026-11-10 5:48 ` [PATCH v2 10/11] zsmalloc: fold zs_obj_write() into zs_store() Sergey Senozhatsky
@ 2026-11-10 17:00 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 17:00 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:15PM +0900, Sergey Senozhatsky wrote:
> zram and zswap always call zs_obj_write() right after zs_malloc(), and
> zs_obj_write() has to take pool->lock and look up object location
> again, which zs_malloc() just did.
>
> Replace zs_malloc() and zs_obj_write() with one single zs_store()
> call.
>
> zs_store() takes zspage read lock under class->lock, before it drops
> class->lock to copy the data, while zs_obj_write() used to take it
> under pool->lock. Compaction and page migration already take zspage
> lock under class->lock, so this follows the existing pool->lock ->
> class->lock -> zspage->lock order. We don't need pool->lock there,
> zs_store() doesn't look up the object via the handle, it already has
> the zspage from the allocation.
>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
> ---
> Documentation/core-api/memory-allocation.rst | 2 +-
> Documentation/mm/zsmalloc.rst | 4 +-
> drivers/block/zram/zram_drv.c | 42 +++++---------
> include/linux/zsmalloc.h | 6 +-
> mm/zsmalloc.c | 58 +++++++++-----------
> mm/zswap.c | 5 +-
> 6 files changed, 47 insertions(+), 70 deletions(-)
zswap and zsmalloc bits LGTM:
Reviewed-by: Yosry Ahmed <yosry@kernel.org>
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 11/11] zsmalloc: populate new zspage outside class->lock in zs_store()
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (9 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 10/11] zsmalloc: fold zs_obj_write() into zs_store() Sergey Senozhatsky
@ 2026-11-10 5:48 ` Sergey Senozhatsky
2026-11-10 17:03 ` Yosry Ahmed
2026-11-10 16:16 ` [PATCH v2 00/11] zsmalloc: redesign object alloc API Yosry Ahmed
11 siblings, 1 reply; 20+ messages in thread
From: Sergey Senozhatsky @ 2026-11-10 5:48 UTC (permalink / raw)
To: Andrew Morton
Cc: Nhat Pham, Yosry Ahmed, Johannes Weiner, Brian Geffon,
Minchan Kim, Richard Chang, linux-kernel, linux-block, linux-mm,
Sergey Senozhatsky, Yosry Ahmed
A new zspage is not on class fullness lists and is not movable until
zs_store() calls insert_zspage() and SetZsPageMovable(), so neither
other zs_store() callers nor compaction and page migration can see it.
Nobody else has the handle yet either.
So for a new zspage allocate the object and copy the data before we
take class->lock, and don't take zspage read lock. This makes
class->lock critical section shorter and saves a zspage lock/unlock.
Suggested-by: Yosry Ahmed <yosry@kernel.org>
Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
---
mm/zsmalloc.c | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/mm/zsmalloc.c b/mm/zsmalloc.c
index 09ce8c294c5e..2f4adbae82a5 100644
--- a/mm/zsmalloc.c
+++ b/mm/zsmalloc.c
@@ -1319,8 +1319,12 @@ int zs_store(struct zs_pool *pool, struct zs_handle *handle, const void *src,
/* Now move the zspage to another fullness group, if required */
fix_fullness_group(class, zspage);
class_stat_add(class, ZS_OBJS_INUSE, 1);
+ zspage_read_lock(zspage);
+ spin_unlock(&class->lock);
- goto out;
+ obj_write(class, zspage, obj, src, size);
+ zspage_read_unlock(zspage);
+ return 0;
}
spin_unlock(&class->lock);
@@ -1329,8 +1333,15 @@ int zs_store(struct zs_pool *pool, struct zs_handle *handle, const void *src,
if (!zspage)
return -ENOMEM;
- spin_lock(&class->lock);
+ /*
+ * New zspage is not in class->fullness_list or movable yet, so we
+ * can allocate the object and copy data before taking class->lock
+ * and without zspage_read_lock().
+ */
obj = obj_malloc(pool, zspage, handle);
+ obj_write(class, zspage, obj, src, size);
+
+ spin_lock(&class->lock);
newfg = get_fullness_group(class, zspage);
insert_zspage(class, zspage, newfg);
atomic_long_add(class->pages_per_zspage, &pool->pages_allocated);
@@ -1339,13 +1350,8 @@ int zs_store(struct zs_pool *pool, struct zs_handle *handle, const void *src,
/* We completely set up zspage so mark them as movable */
SetZsPageMovable(pool, zspage);
-out:
- zspage_read_lock(zspage);
spin_unlock(&class->lock);
- obj_write(class, zspage, obj, src, size);
- zspage_read_unlock(zspage);
-
return 0;
}
EXPORT_SYMBOL_GPL(zs_store);
--
2.56.0.506.g506a7eabc5-goog
^ permalink raw reply [flat|nested] 20+ messages in thread* Re: [PATCH v2 11/11] zsmalloc: populate new zspage outside class->lock in zs_store()
2026-11-10 5:48 ` [PATCH v2 11/11] zsmalloc: populate new zspage outside class->lock in zs_store() Sergey Senozhatsky
@ 2026-11-10 17:03 ` Yosry Ahmed
0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 17:03 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Tue, Nov 10, 2026 at 02:48:16PM +0900, Sergey Senozhatsky wrote:
> A new zspage is not on class fullness lists and is not movable until
> zs_store() calls insert_zspage() and SetZsPageMovable(), so neither
> other zs_store() callers nor compaction and page migration can see it.
> Nobody else has the handle yet either.
>
> So for a new zspage allocate the object and copy the data before we
> take class->lock, and don't take zspage read lock. This makes
> class->lock critical section shorter and saves a zspage lock/unlock.
>
> Suggested-by: Yosry Ahmed <yosry@kernel.org>
> Signed-off-by: Sergey Senozhatsky <senozhatsky@chromium.org>
LGTM:
Reviewed-by: Yosry Ahmed <yosry@kernel.org>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v2 00/11] zsmalloc: redesign object alloc API
2026-11-10 5:48 [PATCH v2 00/11] zsmalloc: redesign object alloc API Sergey Senozhatsky
` (10 preceding siblings ...)
2026-11-10 5:48 ` [PATCH v2 11/11] zsmalloc: populate new zspage outside class->lock in zs_store() Sergey Senozhatsky
@ 2026-11-10 16:16 ` Yosry Ahmed
11 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-11-10 16:16 UTC (permalink / raw)
To: Sergey Senozhatsky
Cc: Andrew Morton, Nhat Pham, Yosry Ahmed, Johannes Weiner,
Brian Geffon, Minchan Kim, Richard Chang, linux-kernel,
linux-block, linux-mm
On Mon, Nov 9, 2026 at 9:48 PM Sergey Senozhatsky
<senozhatsky@chromium.org> wrote:
>
> Both zram and zswap already have per-entry structures that store zsmalloc
> handles, so allocating a separate handle_cachep object per stored page is
> redundant. This series embeds struct zs_handle directly into zram and
> zswap entries and drops handle_cachep, saving 8 bytes and a slab
> alloc/free per stored object (2MB per 1GB of swapped out 4K pages).
>
> It also folds zs_malloc() and zs_obj_write() into zs_store() so we don't
> take pool->lock and look up object location again right after allocation.
>
> A simple zram test on a 24-vCPU VM
> ==================================
> 165 fewer kernel instructions and 0.7-1.8% fewer kernel cycles per write;
> lz4 single writer +2.3% IOPS / -2.6% average latency;
> 24 writers +0.9-+1.0% IOPS; no regressions.
> Memory: 8 bytes less per stored page.
>
> v1 -> v2:
> -- addressed Yosry's v1 feedback (too lengthy to mention here, Yosry
> posted over a million comments, or something very close to that):
> split-ed some patches, squashed the other ones, added handle
> alignment run-time check, added 0 PFN defense, etc.
I will take this as a good thing :p
^ permalink raw reply [flat|nested] 20+ messages in thread