2013-05-13 12:41:39

by Seth Jennings

[permalink] [raw]
Subject: [PATCHv11 0/4] zswap: compressed swap caching

This is the latest version of the zswap patchset for compressed swap caching.
This is submitted for merging into linux-next and inclusion in v3.11.

New in this Version:

Fixup symbol collision in lib/fault-inject.c with local definition of
debugfs functions (thanks Greg). Rebased to 3.10-rc1.

Useful References:

LSFMM: In-kernel memory compression
https://lwn.net/Articles/548109/

The zswap compressed swap cache
https://lwn.net/Articles/537422/

Zswap Overview:

Zswap is a lightweight compressed cache for swap pages. It takes
pages that are in the process of being swapped out and attempts to
compress them into a dynamically allocated RAM-based memory pool.
If this process is successful, the writeback to the swap device is
deferred and, in many cases, avoided completely. This results in
a significant I/O reduction and performance gains for systems that
are swapping.

The results of a kernel building benchmark indicate a
runtime reduction of 53% and an I/O reduction 76% with zswap vs normal
swapping with a kernel build under heavy memory pressure (see
Performance section for more).

Some addition performance metrics regarding the performance
improvements and I/O reductions that can be achieved using zswap as
measured by SPECjbb are provided here:
http://ibm.co/VCgHvM

These results include runs on x86 and new results on Power7+ with
hardware compression acceleration.

Of particular note is that zswap is able to evict pages from the compressed
cache, on an LRU basis, to the backing swap device when the compressed pool
reaches it size limit or the pool is unable to obtain additional pages
from the buddy allocator. This eviction functionality had been identified
as a requirement in prior community discussions.

Rationale:

Zswap provides compressed swap caching that basically trades CPU cycles
for reduced swap I/O. This trade-off can result in a significant
performance improvement as reads to/writes from to the compressed
cache almost always faster that reading from a swap device
which incurs the latency of an asynchronous block I/O read.

Some potential benefits:
* Desktop/laptop users with limited RAM capacities can mitigate the
performance impact of swapping.
* Overcommitted guests that share a common I/O resource can
dramatically reduce their swap I/O pressure, avoiding heavy
handed I/O throttling by the hypervisor. This allows more work
to get done with less impact to the guest workload and guests
sharing the I/O subsystem
* Users with SSDs as swap devices can extend the life of the device by
drastically reducing life-shortening writes.

Compressed swap is also provided in zcache, along with page cache
compression and RAM clustering through RAMSter. Zswap seeks to deliver
the benefit of swap compression to users in a discrete function.
This design decision is akin to Unix design philosophy of doing one
thing well, it leaves file cache compression and other features
for separate code.

Design:

Zswap receives pages for compression through the Frontswap API and
is able to evict pages from its own compressed pool on an LRU basis
and write them back to the backing swap device in the case that the
compressed pool is full or unable to secure additional pages from
the buddy allocator.

Zswap makes use of zbud for the managing the compressed memory pool.
Each allocation in zbud is not directly accessible by address. Rather,
a handle is return by the allocation routine and that handle must be
mapped before being accessed. The compressed memory pool grows on
demand and shrinks as compressed pages are freed. The pool is not
preallocated.

When a swap page is passed from frontswap to zswap, zswap maintains
a mapping of the swap entry, a combination of the swap type and swap
offset, to the zbud handle that references that compressed swap
page. This mapping is achieved with a red-black tree per swap type.
The swap offset is the search key for the tree nodes.

Zswap seeks to be simple in its policies. Sysfs attributes allow for
two user controlled policies:
* max_compression_ratio - Maximum compression ratio, as as percentage,
for an acceptable compressed page. Any page that does not compress
by at least this ratio will be rejected.
* max_pool_percent - The maximum percentage of memory that the compressed
pool can occupy.

To enabled zswap, the "enabled" attribute must be set to 1 at boot time.

Zswap allows the compressor to be selected at kernel boot time by
setting the “compressor” attribute. The default compressor is lzo.

A debugfs interface is provided for various statistic about pool size,
number of pages stored, and various counters for the reasons pages
are rejected.

Changelog:

v11:
* fixup symbol collision with lib/fault-inject.c (Greg)
* rebase v3.10-rc1

v10:
* replace zsmalloc with zbud (zsmalloc to come back as option in future dev)
* lru logic moved out of zswap into allocator
* simplified and improved writeback logic
* removed memory pool and tmpage pool as part of refactoring
* Rebase to (almost) v3.10-rc1

v9:
* Fix load-during-writeback race; double lru add (for real this time)
* checkpatch and comment fixes
* Fix __swap_writepage() return value check
* Move check for max outstanding writebacks (dedup some code)
* Rebase to v3.9-rc6

v8:
* Fix load-during-writeback race; double lru add
* checkpatch fixups
* s/NOWAIT/ATOMIC for tree allocation (Dave)
* Check __swap_writepage() for error before incr outstanding write count (Rob)
* Convert pcpu compression buffer alloc from alloc_page() to kmalloc() (Dave)
* Rebase to v3.9-rc5

v7:
* Decrease zswap_stored_pages during tree cleanup (Joonsoo)
* Move zswap_entry_cache_alloc() earlier during store (Joonsoo)
* Move type field from struct zswap_entry to struct zswap_tree
* Change to swapper_space array (-rc1 change)
* s/reset_page_mapcount/page_mapcount_reset in zsmalloc (-rc1 change)
* Rebase to v3.9-rc1

v6:
* fix access-after-free regression introduced in v5
(rb_erase() outside the lock)
* fix improper freeing of rbtree (Cody)
* fix comment typo (Ric)
* add comments about ZS_MM_WO usage and page mapping mode (Joonsoo)
* don't use page->object (Joonsoo)
* remove DEBUG (Joonsoo)
* rebase to v3.8

v5:
* zsmalloc patch converted from promotion to "new code" (for review only,
see note in [1/8])
* promote zsmalloc to mm/ instead of /lib
* add more documentation everywhere
* convert USE_PGTABLE_MAPPING to kconfig option, thanks to Minchan
* s/flush/writeback/
* #define pr_fmt() for formatting messages (Joe)
* checkpatch fixups
* lots of changes suggested Minchan

v4:
* Added Acks (Minchan)
* Separated flushing functionality into standalone patch
for easier review (Minchan)
* fix comment on zswap enabled attribute (Minchan)
* add TODO for dynamic mempool size (Minchan)
* and check for NULL in zswap_free_page() (Minchan)
* add missing zs_free() in error path (Minchan)
* TODO: add comments for flushing/refcounting (Minchan)

v3:
* Dropped the zsmalloc patches from the set, except the promotion patch
which has be converted to a rename patch (vs full diff). The dropped
patches have been Acked and are going into Greg's staging tree soon.
* Separated [PATCHv2 7/9] into two patches since it makes changes for two
different reasons (Minchan)
* Moved ZSWAP_MAX_OUTSTANDING_FLUSHES near the top in zswap.c (Rik)
* Rebase to v3.8-rc5. linux-next is a little volatile with the
swapper_space per type changes which will effect this patchset.
* TODO: Move some stats from debugfs to sysfs. Which ones? (Rik)

v2:
* Rename zswap_fs_* functions to zswap_frontswap_* to avoid
confusion with "filesystem"
* Add comment about what the tree lock protects
* Remove "#if 0" code (should have been done before)
* Break out changes to existing swap code into separate patch
* Fix blank line EOF warning on documentation file
* Rebase to next-20130107

Performance, Kernel Building:

Setup
========
Gentoo w/ kernel v3.7-rc7
Quad-core i5-2500 @ 3.3GHz
512MB DDR3 1600MHz (limited with mem=512m on boot)
Filesystem and swap on 80GB HDD (about 58MB/s with hdparm -t)
majflt are major page faults reported by the time command
pswpin/out is the delta of pswpin/out from /proc/vmstat before and after
the make -jN

Summary
========
* Zswap reduces I/O and improves performance at all swap pressure levels.

* Under heavy swaping at 24 threads, zswap reduced I/O by 76%, saving
over 1.5GB of I/O, and cut runtime in half.

Details
========
I/O (in pages)
base zswap change change
N pswpin pswpout majflt I/O sum pswpin pswpout majflt I/O sum %I/O MB
8 1 335 291 627 0 0 249 249 -60% 1
12 3688 14315 5290 23293 123 860 5954 6937 -70% 64
16 12711 46179 16803 75693 2936 7390 46092 56418 -25% 75
20 42178 133781 49898 225857 9460 28382 92951 130793 -42% 371
24 96079 357280 105242 558601 7719 18484 109309 135512 -76% 1653

Runtime (in seconds)
N base zswap %change
8 107 107 0%
12 128 110 -14%
16 191 179 -6%
20 371 240 -35%
24 570 267 -53%

%CPU utilization (out of 400% on 4 cpus)
N base zswap %change
8 317 319 1%
12 267 311 16%
16 179 191 7%
20 94 143 52%
24 60 128 113%

Seth Jennings (4):
debugfs: add get/set for atomic types
zbud: add to mm/
zswap: add to mm/
zswap: add documentation

Documentation/vm/zswap.txt | 72 ++++
fs/debugfs/file.c | 42 ++
include/linux/debugfs.h | 2 +
include/linux/zbud.h | 22 +
lib/fault-inject.c | 21 -
mm/Kconfig | 25 ++
mm/Makefile | 2 +
mm/zbud.c | 564 ++++++++++++++++++++++++++
mm/zswap.c | 952 ++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 1681 insertions(+), 21 deletions(-)
create mode 100644 Documentation/vm/zswap.txt
create mode 100644 include/linux/zbud.h
create mode 100644 mm/zbud.c
create mode 100644 mm/zswap.c

--
1.7.9.5


2013-05-13 12:40:30

by Seth Jennings

[permalink] [raw]
Subject: [PATCHv11 4/4] zswap: add documentation

This patch adds the documentation file for the zswap functionality

Signed-off-by: Seth Jennings <[email protected]>
---
Documentation/vm/zswap.txt | 72 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
create mode 100644 Documentation/vm/zswap.txt

diff --git a/Documentation/vm/zswap.txt b/Documentation/vm/zswap.txt
new file mode 100644
index 0000000..88384b3
--- /dev/null
+++ b/Documentation/vm/zswap.txt
@@ -0,0 +1,72 @@
+Overview:
+
+Zswap is a lightweight compressed cache for swap pages. It takes pages that are
+in the process of being swapped out and attempts to compress them into a
+dynamically allocated RAM-based memory pool. If this process is successful,
+the writeback to the swap device is deferred and, in many cases, avoided
+completely.  This results in a significant I/O reduction and performance gains
+for systems that are swapping.
+
+Zswap provides compressed swap caching that basically trades CPU cycles for
+reduced swap I/O.  This trade-off can result in a significant performance
+improvement as reads to/writes from to the compressed cache almost always
+faster that reading from a swap device which incurs the latency of an
+asynchronous block I/O read.
+
+Some potential benefits:
+* Desktop/laptop users with limited RAM capacities can mitigate the
+    performance impact of swapping.
+* Overcommitted guests that share a common I/O resource can
+    dramatically reduce their swap I/O pressure, avoiding heavy handed I/O
+ throttling by the hypervisor. This allows more work to get done with less
+ impact to the guest workload and guests sharing the I/O subsystem
+* Users with SSDs as swap devices can extend the life of the device by
+    drastically reducing life-shortening writes.
+
+Zswap evicts pages from compressed cache on an LRU basis to the backing swap
+device when the compressed pool reaches it size limit. This requirement had
+been identified in prior community discussions.
+
+To enabled zswap, the "enabled" attribute must be set to 1 at boot time. e.g.
+zswap.enabled=1
+
+Design:
+
+Zswap receives pages for compression through the Frontswap API and is able to
+evict pages from its own compressed pool on an LRU basis and write them back to
+the backing swap device in the case that the compressed pool is full.
+
+Zswap makes use of zbud for the managing the compressed memory pool. Each
+allocation in zbud is not directly accessible by address. Rather, a handle is
+return by the allocation routine and that handle must be mapped before being
+accessed. The compressed memory pool grows on demand and shrinks as compressed
+pages are freed. The pool is not preallocated.
+
+When a swap page is passed from frontswap to zswap, zswap maintains a mapping
+of the swap entry, a combination of the swap type and swap offset, to the zbud
+handle that references that compressed swap page. This mapping is achieved
+with a red-black tree per swap type. The swap offset is the search key for the
+tree nodes.
+
+During a page fault on a PTE that is a swap entry, frontswap calls the zswap
+load function to decompress the page into the page allocated by the page fault
+handler.
+
+Once there are no PTEs referencing a swap page stored in zswap (i.e. the count
+in the swap_map goes to 0) the swap code calls the zswap invalidate function,
+via frontswap, to free the compressed entry.
+
+Zswap seeks to be simple in its policies. Sysfs attributes allow for two user
+controlled policies:
+* max_compression_ratio - Maximum compression ratio, as as percentage,
+ for an acceptable compressed page. Any page that does not compress by at
+ least this ratio will be rejected.
+* max_pool_percent - The maximum percentage of memory that the compressed
+ pool can occupy.
+
+Zswap allows the compressor to be selected at kernel boot time by setting the
+“compressor” attribute. The default compressor is lzo. e.g.
+zswap.compressor=deflate
+
+A debugfs interface is provided for various statistic about pool size, number
+of pages stored, and various counters for the reasons pages are rejected.
--
1.7.9.5

2013-05-13 12:40:39

by Seth Jennings

[permalink] [raw]
Subject: [PATCHv11 3/4] zswap: add to mm/

zswap is a thin compression backend for frontswap. It receives pages from
frontswap and attempts to store them in a compressed memory pool, resulting in
an effective partial memory reclaim and dramatically reduced swap device I/O.

Additionally, in most cases, pages can be retrieved from this compressed store
much more quickly than reading from tradition swap devices resulting in faster
performance for many workloads.

It also has support for evicting swap pages that are currently compressed in
zswap to the swap device on an LRU(ish) basis. This functionality is very
important and make zswap a true cache in that, once the cache is full or can't
grow due to memory pressure, the oldest pages can be moved out of zswap to the
swap device so newer pages can be compressed and stored in zswap.

This patch adds the zswap driver to mm/

Signed-off-by: Seth Jennings <[email protected]>
---
mm/Kconfig | 15 +
mm/Makefile | 1 +
mm/zswap.c | 952 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 968 insertions(+)
create mode 100644 mm/zswap.c

diff --git a/mm/Kconfig b/mm/Kconfig
index 908f41b..4042e07 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -487,3 +487,18 @@ config ZBUD
While this design limits storage density, it has simple and
deterministic reclaim properties that make it preferable to a higher
density approach when reclaim will be used.
+
+config ZSWAP
+ bool "In-kernel swap page compression"
+ depends on FRONTSWAP && CRYPTO
+ select CRYPTO_LZO
+ select ZBUD
+ default n
+ help
+ Zswap is a backend for the frontswap mechanism in the VMM.
+ It receives pages from frontswap and attempts to store them
+ in a compressed memory pool, resulting in an effective
+ partial memory reclaim. In addition, pages and be retrieved
+ from this compressed store much faster than most tradition
+ swap devices resulting in reduced I/O and faster performance
+ for many workloads.
diff --git a/mm/Makefile b/mm/Makefile
index 95f0197..f008033 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -32,6 +32,7 @@ obj-$(CONFIG_HAVE_MEMBLOCK) += memblock.o
obj-$(CONFIG_BOUNCE) += bounce.o
obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
obj-$(CONFIG_FRONTSWAP) += frontswap.o
+obj-$(CONFIG_ZSWAP) += zswap.o
obj-$(CONFIG_HAS_DMA) += dmapool.o
obj-$(CONFIG_HUGETLBFS) += hugetlb.o
obj-$(CONFIG_NUMA) += mempolicy.o
diff --git a/mm/zswap.c b/mm/zswap.c
new file mode 100644
index 0000000..b1070ca
--- /dev/null
+++ b/mm/zswap.c
@@ -0,0 +1,952 @@
+/*
+ * zswap.c - zswap driver file
+ *
+ * zswap is a backend for frontswap that takes pages that are in the
+ * process of being swapped out and attempts to compress them and store
+ * them in a RAM-based memory pool. This results in a significant I/O
+ * reduction on the real swap device and, in the case of a slow swap
+ * device, can also improve workload performance.
+ *
+ * Copyright (C) 2012 Seth Jennings <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+*/
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/module.h>
+#include <linux/cpu.h>
+#include <linux/highmem.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/atomic.h>
+#include <linux/frontswap.h>
+#include <linux/rbtree.h>
+#include <linux/swap.h>
+#include <linux/crypto.h>
+#include <linux/mempool.h>
+#include <linux/zbud.h>
+
+#include <linux/mm_types.h>
+#include <linux/page-flags.h>
+#include <linux/swapops.h>
+#include <linux/writeback.h>
+#include <linux/pagemap.h>
+
+/*********************************
+* statistics
+**********************************/
+/* Number of memory pages used by the compressed pool */
+static atomic_t zswap_pool_pages = ATOMIC_INIT(0);
+/* The number of compressed pages currently stored in zswap */
+static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
+
+/*
+ * The statistics below are not protected from concurrent access for
+ * performance reasons so they may not be a 100% accurate. However,
+ * they do provide useful information on roughly how many times a
+ * certain event is occurring.
+*/
+static u64 zswap_pool_limit_hit;
+static u64 zswap_written_back_pages;
+static u64 zswap_reject_reclaim_fail;
+static u64 zswap_reject_compress_poor;
+static u64 zswap_reject_alloc_fail;
+static u64 zswap_reject_kmemcache_fail;
+static u64 zswap_duplicate_entry;
+
+/*********************************
+* tunables
+**********************************/
+/* Enable/disable zswap (disabled by default, fixed at boot for now) */
+static bool zswap_enabled;
+module_param_named(enabled, zswap_enabled, bool, 0);
+
+/* Compressor to be used by zswap (fixed at boot for now) */
+#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
+static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
+module_param_named(compressor, zswap_compressor, charp, 0);
+
+/* The maximum percentage of memory that the compressed pool can occupy */
+static unsigned int zswap_max_pool_percent = 20;
+module_param_named(max_pool_percent,
+ zswap_max_pool_percent, uint, 0644);
+
+/*
+ * Maximum compression ratio, as as percentage, for an acceptable
+ * compressed page. Any pages that do not compress by at least
+ * this ratio will be rejected.
+*/
+static unsigned int zswap_max_compression_ratio = 80;
+module_param_named(max_compression_ratio,
+ zswap_max_compression_ratio, uint, 0644);
+
+/*********************************
+* compression functions
+**********************************/
+/* per-cpu compression transforms */
+static struct crypto_comp * __percpu *zswap_comp_pcpu_tfms;
+
+enum comp_op {
+ ZSWAP_COMPOP_COMPRESS,
+ ZSWAP_COMPOP_DECOMPRESS
+};
+
+static int zswap_comp_op(enum comp_op op, const u8 *src, unsigned int slen,
+ u8 *dst, unsigned int *dlen)
+{
+ struct crypto_comp *tfm;
+ int ret;
+
+ tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, get_cpu());
+ switch (op) {
+ case ZSWAP_COMPOP_COMPRESS:
+ ret = crypto_comp_compress(tfm, src, slen, dst, dlen);
+ break;
+ case ZSWAP_COMPOP_DECOMPRESS:
+ ret = crypto_comp_decompress(tfm, src, slen, dst, dlen);
+ break;
+ default:
+ ret = -EINVAL;
+ }
+
+ put_cpu();
+ return ret;
+}
+
+static int __init zswap_comp_init(void)
+{
+ if (!crypto_has_comp(zswap_compressor, 0, 0)) {
+ pr_info("%s compressor not available\n", zswap_compressor);
+ /* fall back to default compressor */
+ zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
+ if (!crypto_has_comp(zswap_compressor, 0, 0))
+ /* can't even load the default compressor */
+ return -ENODEV;
+ }
+ pr_info("using %s compressor\n", zswap_compressor);
+
+ /* alloc percpu transforms */
+ zswap_comp_pcpu_tfms = alloc_percpu(struct crypto_comp *);
+ if (!zswap_comp_pcpu_tfms)
+ return -ENOMEM;
+ return 0;
+}
+
+static void zswap_comp_exit(void)
+{
+ /* free percpu transforms */
+ if (zswap_comp_pcpu_tfms)
+ free_percpu(zswap_comp_pcpu_tfms);
+}
+
+/*********************************
+* data structures
+**********************************/
+/*
+ * struct zswap_entry
+ *
+ * This structure contains the metadata for tracking a single compressed
+ * page within zswap.
+ *
+ * rbnode - links the entry into red-black tree for the appropriate swap type
+ * refcount - the number of outstanding reference to the entry. This is needed
+ * to protect against premature freeing of the entry by code
+ * concurent calls to load, invalidate, and writeback. The lock
+ * for the zswap_tree structure that contains the entry must
+ * be held while changing the refcount. Since the lock must
+ * be held, there is no reason to also make refcount atomic.
+ * type - the swap type for the entry. Used to map back to the zswap_tree
+ * structure that contains the entry.
+ * offset - the swap offset for the entry. Index into the red-black tree.
+ * handle - zsmalloc allocation handle that stores the compressed page data
+ * length - the length in bytes of the compressed page data. Needed during
+ * decompression
+ */
+struct zswap_entry {
+ struct rb_node rbnode;
+ pgoff_t offset;
+ int refcount;
+ unsigned int length;
+ unsigned long handle;
+};
+
+struct zswap_header {
+ swp_entry_t swpentry;
+};
+
+/*
+ * The tree lock in the zswap_tree struct protects a few things:
+ * - the rbtree
+ * - the refcount field of each entry in the tree
+ */
+struct zswap_tree {
+ struct rb_root rbroot;
+ spinlock_t lock;
+ struct zbud_pool *pool;
+ unsigned type;
+};
+
+static struct zswap_tree *zswap_trees[MAX_SWAPFILES];
+
+/*********************************
+* zswap entry functions
+**********************************/
+#define ZSWAP_KMEM_CACHE_NAME "zswap_entry_cache"
+static struct kmem_cache *zswap_entry_cache;
+
+static inline int zswap_entry_cache_create(void)
+{
+ zswap_entry_cache =
+ kmem_cache_create(ZSWAP_KMEM_CACHE_NAME,
+ sizeof(struct zswap_entry), 0, 0, NULL);
+ return (zswap_entry_cache == NULL);
+}
+
+static inline void zswap_entry_cache_destory(void)
+{
+ kmem_cache_destroy(zswap_entry_cache);
+}
+
+static inline struct zswap_entry *zswap_entry_cache_alloc(gfp_t gfp)
+{
+ struct zswap_entry *entry;
+ entry = kmem_cache_alloc(zswap_entry_cache, gfp);
+ if (!entry)
+ return NULL;
+ entry->refcount = 1;
+ return entry;
+}
+
+static inline void zswap_entry_cache_free(struct zswap_entry *entry)
+{
+ kmem_cache_free(zswap_entry_cache, entry);
+}
+
+static inline void zswap_entry_get(struct zswap_entry *entry)
+{
+ entry->refcount++;
+}
+
+static inline int zswap_entry_put(struct zswap_entry *entry)
+{
+ entry->refcount--;
+ return entry->refcount;
+}
+
+/*********************************
+* rbtree functions
+**********************************/
+static struct zswap_entry *zswap_rb_search(struct rb_root *root, pgoff_t offset)
+{
+ struct rb_node *node = root->rb_node;
+ struct zswap_entry *entry;
+
+ while (node) {
+ entry = rb_entry(node, struct zswap_entry, rbnode);
+ if (entry->offset > offset)
+ node = node->rb_left;
+ else if (entry->offset < offset)
+ node = node->rb_right;
+ else
+ return entry;
+ }
+ return NULL;
+}
+
+/*
+ * In the case that a entry with the same offset is found, it a pointer to
+ * the existing entry is stored in dupentry and the function returns -EEXIST
+*/
+static int zswap_rb_insert(struct rb_root *root, struct zswap_entry *entry,
+ struct zswap_entry **dupentry)
+{
+ struct rb_node **link = &root->rb_node, *parent = NULL;
+ struct zswap_entry *myentry;
+
+ while (*link) {
+ parent = *link;
+ myentry = rb_entry(parent, struct zswap_entry, rbnode);
+ if (myentry->offset > entry->offset)
+ link = &(*link)->rb_left;
+ else if (myentry->offset < entry->offset)
+ link = &(*link)->rb_right;
+ else {
+ *dupentry = myentry;
+ return -EEXIST;
+ }
+ }
+ rb_link_node(&entry->rbnode, parent, link);
+ rb_insert_color(&entry->rbnode, root);
+ return 0;
+}
+
+/*********************************
+* per-cpu code
+**********************************/
+static DEFINE_PER_CPU(u8 *, zswap_dstmem);
+
+static int __zswap_cpu_notifier(unsigned long action, unsigned long cpu)
+{
+ struct crypto_comp *tfm;
+ u8 *dst;
+
+ switch (action) {
+ case CPU_UP_PREPARE:
+ tfm = crypto_alloc_comp(zswap_compressor, 0, 0);
+ if (IS_ERR(tfm)) {
+ pr_err("can't allocate compressor transform\n");
+ return NOTIFY_BAD;
+ }
+ *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = tfm;
+ dst = kmalloc(PAGE_SIZE * 2, GFP_KERNEL);
+ if (!dst) {
+ pr_err("can't allocate compressor buffer\n");
+ crypto_free_comp(tfm);
+ *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
+ return NOTIFY_BAD;
+ }
+ per_cpu(zswap_dstmem, cpu) = dst;
+ break;
+ case CPU_DEAD:
+ case CPU_UP_CANCELED:
+ tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu);
+ if (tfm) {
+ crypto_free_comp(tfm);
+ *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
+ }
+ dst = per_cpu(zswap_dstmem, cpu);
+ kfree(dst);
+ per_cpu(zswap_dstmem, cpu) = NULL;
+ break;
+ default:
+ break;
+ }
+ return NOTIFY_OK;
+}
+
+static int zswap_cpu_notifier(struct notifier_block *nb,
+ unsigned long action, void *pcpu)
+{
+ unsigned long cpu = (unsigned long)pcpu;
+ return __zswap_cpu_notifier(action, cpu);
+}
+
+static struct notifier_block zswap_cpu_notifier_block = {
+ .notifier_call = zswap_cpu_notifier
+};
+
+static int zswap_cpu_init(void)
+{
+ unsigned long cpu;
+
+ get_online_cpus();
+ for_each_online_cpu(cpu)
+ if (__zswap_cpu_notifier(CPU_UP_PREPARE, cpu) != NOTIFY_OK)
+ goto cleanup;
+ register_cpu_notifier(&zswap_cpu_notifier_block);
+ put_online_cpus();
+ return 0;
+
+cleanup:
+ for_each_online_cpu(cpu)
+ __zswap_cpu_notifier(CPU_UP_CANCELED, cpu);
+ put_online_cpus();
+ return -ENOMEM;
+}
+
+/*********************************
+* helpers
+**********************************/
+static inline bool zswap_is_full(void)
+{
+ int pool_pages = atomic_read(&zswap_pool_pages);
+ return (totalram_pages * zswap_max_pool_percent / 100 < pool_pages);
+}
+
+/*
+ * Carries out the common pattern of freeing and entry's zsmalloc allocation,
+ * freeing the entry itself, and decrementing the number of stored pages.
+ */
+static void zswap_free_entry(struct zswap_tree *tree, struct zswap_entry *entry)
+{
+ zbud_free(tree->pool, entry->handle);
+ zswap_entry_cache_free(entry);
+ atomic_dec(&zswap_stored_pages);
+ atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
+}
+
+/*********************************
+* writeback code
+**********************************/
+/* return enum for zswap_get_swap_cache_page */
+enum zswap_get_swap_ret {
+ ZSWAP_SWAPCACHE_NEW,
+ ZSWAP_SWAPCACHE_EXIST,
+ ZSWAP_SWAPCACHE_NOMEM
+};
+
+/*
+ * zswap_get_swap_cache_page
+ *
+ * This is an adaption of read_swap_cache_async()
+ *
+ * This function tries to find a page with the given swap entry
+ * in the swapper_space address space (the swap cache). If the page
+ * is found, it is returned in retpage. Otherwise, a page is allocated,
+ * added to the swap cache, and returned in retpage.
+ *
+ * If success, the swap cache page is returned in retpage
+ * Returns 0 if page was already in the swap cache, page is not locked
+ * Returns 1 if the new page needs to be populated, page is locked
+ * Returns <0 on error
+ */
+static int zswap_get_swap_cache_page(swp_entry_t entry,
+ struct page **retpage)
+{
+ struct page *found_page, *new_page = NULL;
+ struct address_space *swapper_space = &swapper_spaces[swp_type(entry)];
+ int err;
+
+ *retpage = NULL;
+ do {
+ /*
+ * First check the swap cache. Since this is normally
+ * called after lookup_swap_cache() failed, re-calling
+ * that would confuse statistics.
+ */
+ found_page = find_get_page(swapper_space, entry.val);
+ if (found_page)
+ break;
+
+ /*
+ * Get a new page to read into from swap.
+ */
+ if (!new_page) {
+ new_page = alloc_page(GFP_KERNEL);
+ if (!new_page)
+ break; /* Out of memory */
+ }
+
+ /*
+ * call radix_tree_preload() while we can wait.
+ */
+ err = radix_tree_preload(GFP_KERNEL);
+ if (err)
+ break;
+
+ /*
+ * Swap entry may have been freed since our caller observed it.
+ */
+ err = swapcache_prepare(entry);
+ if (err == -EEXIST) { /* seems racy */
+ radix_tree_preload_end();
+ continue;
+ }
+ if (err) { /* swp entry is obsolete ? */
+ radix_tree_preload_end();
+ break;
+ }
+
+ /* May fail (-ENOMEM) if radix-tree node allocation failed. */
+ __set_page_locked(new_page);
+ SetPageSwapBacked(new_page);
+ err = __add_to_swap_cache(new_page, entry);
+ if (likely(!err)) {
+ radix_tree_preload_end();
+ lru_cache_add_anon(new_page);
+ *retpage = new_page;
+ return ZSWAP_SWAPCACHE_NEW;
+ }
+ radix_tree_preload_end();
+ ClearPageSwapBacked(new_page);
+ __clear_page_locked(new_page);
+ /*
+ * add_to_swap_cache() doesn't return -EEXIST, so we can safely
+ * clear SWAP_HAS_CACHE flag.
+ */
+ swapcache_free(entry, NULL);
+ } while (err != -ENOMEM);
+
+ if (new_page)
+ page_cache_release(new_page);
+ if (!found_page)
+ return ZSWAP_SWAPCACHE_NOMEM;
+ *retpage = found_page;
+ return ZSWAP_SWAPCACHE_EXIST;
+}
+
+/*
+ * Attempts to free and entry by adding a page to the swap cache,
+ * decompressing the entry data into the page, and issuing a
+ * bio write to write the page back to the swap device.
+ *
+ * This can be thought of as a "resumed writeback" of the page
+ * to the swap device. We are basically resuming the same swap
+ * writeback path that was intercepted with the frontswap_store()
+ * in the first place. After the page has been decompressed into
+ * the swap cache, the compressed version stored by zswap can be
+ * freed.
+ */
+static int zswap_writeback_entry(struct zbud_pool *pool, unsigned long handle)
+{
+ struct zswap_header *zhdr;
+ swp_entry_t swpentry;
+ struct zswap_tree *tree;
+ pgoff_t offset;
+ struct zswap_entry *entry;
+ struct page *page;
+ u8 *src, *dst;
+ unsigned int dlen;
+ int ret, refcount;
+ struct writeback_control wbc = {
+ .sync_mode = WB_SYNC_NONE,
+ };
+
+ /* extract swpentry from data */
+ zhdr = zbud_map(pool, handle);
+ swpentry = zhdr->swpentry; /* here */
+ zbud_unmap(pool, handle);
+ tree = zswap_trees[swp_type(swpentry)];
+ offset = swp_offset(swpentry);
+ BUG_ON(pool != tree->pool);
+
+ /* find and ref zswap entry */
+ spin_lock(&tree->lock);
+ entry = zswap_rb_search(&tree->rbroot, offset);
+ if (!entry) {
+ /* entry was invalidated */
+ spin_unlock(&tree->lock);
+ return 0;
+ }
+ zswap_entry_get(entry);
+ spin_unlock(&tree->lock);
+ BUG_ON(offset != entry->offset);
+
+ /* try to allocate swap cache page */
+ switch (zswap_get_swap_cache_page(swpentry, &page)) {
+ case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
+ ret = -ENOMEM;
+ goto fail;
+
+ case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
+ /* page is already in the swap cache, ignore for now */
+ page_cache_release(page);
+ ret = -EEXIST;
+ goto fail;
+
+ case ZSWAP_SWAPCACHE_NEW: /* page is locked */
+ /* decompress */
+ dlen = PAGE_SIZE;
+ src = (u8 *)zbud_map(tree->pool, entry->handle) +
+ sizeof(struct zswap_header);
+ dst = kmap_atomic(page);
+ ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
+ entry->length, dst, &dlen);
+ kunmap_atomic(dst);
+ zbud_unmap(tree->pool, entry->handle);
+ BUG_ON(ret);
+ BUG_ON(dlen != PAGE_SIZE);
+
+ /* page is up to date */
+ SetPageUptodate(page);
+ }
+
+ /* start writeback */
+ SetPageReclaim(page);
+ __swap_writepage(page, &wbc, end_swap_bio_write);
+ page_cache_release(page);
+ zswap_written_back_pages++;
+
+ spin_lock(&tree->lock);
+
+ /* drop local reference */
+ zswap_entry_put(entry);
+ /* drop the initial reference from entry creation */
+ refcount = zswap_entry_put(entry);
+
+ /*
+ * There are three possible values for refcount here:
+ * (1) refcount is 1, load is in progress, unlink from rbtree,
+ * load will free
+ * (2) refcount is 0, (normal case) entry is valid,
+ * remove from rbtree and free entry
+ * (3) refcount is -1, invalidate happened during writeback,
+ * free entry
+ */
+ if (refcount >= 0) {
+ /* no invalidate yet, remove from rbtree */
+ rb_erase(&entry->rbnode, &tree->rbroot);
+ }
+ spin_unlock(&tree->lock);
+ if (refcount <= 0) {
+ /* free the entry */
+ zswap_free_entry(tree, entry);
+ return 0;
+ }
+ return -EAGAIN;
+
+fail:
+ spin_lock(&tree->lock);
+ zswap_entry_put(entry);
+ spin_unlock(&tree->lock);
+ return ret;
+}
+
+/*********************************
+* frontswap hooks
+**********************************/
+/* attempts to compress and store an single page */
+static int zswap_frontswap_store(unsigned type, pgoff_t offset,
+ struct page *page)
+{
+ struct zswap_tree *tree = zswap_trees[type];
+ struct zswap_entry *entry, *dupentry;
+ int ret;
+ unsigned int dlen = PAGE_SIZE, len;
+ unsigned long handle;
+ char *buf;
+ u8 *src, *dst;
+ struct zswap_header *zhdr;
+
+ if (!tree) {
+ ret = -ENODEV;
+ goto reject;
+ }
+
+ /* reclaim space if needed */
+ if (zswap_is_full()) {
+ zswap_pool_limit_hit++;
+ if (zbud_reclaim_page(tree->pool, 8)) {
+ zswap_reject_reclaim_fail++;
+ ret = -ENOMEM;
+ goto reject;
+ }
+ }
+
+ /* allocate entry */
+ entry = zswap_entry_cache_alloc(GFP_KERNEL);
+ if (!entry) {
+ zswap_reject_kmemcache_fail++;
+ ret = -ENOMEM;
+ goto reject;
+ }
+
+ /* compress */
+ dst = get_cpu_var(zswap_dstmem);
+ src = kmap_atomic(page);
+ ret = zswap_comp_op(ZSWAP_COMPOP_COMPRESS, src, PAGE_SIZE, dst, &dlen);
+ kunmap_atomic(src);
+ if (ret) {
+ ret = -EINVAL;
+ goto freepage;
+ }
+ len = dlen + sizeof(struct zswap_header);
+ if ((len * 100 / PAGE_SIZE) > zswap_max_compression_ratio) {
+ zswap_reject_compress_poor++;
+ ret = -E2BIG;
+ goto freepage;
+ }
+
+ /* store */
+ ret = zbud_alloc(tree->pool, len, __GFP_NORETRY | __GFP_NOWARN,
+ &handle);
+ if (ret) {
+ zswap_reject_alloc_fail++;
+ goto freepage;
+ }
+ zhdr = zbud_map(tree->pool, handle);
+ zhdr->swpentry = swp_entry(type, offset);
+ buf = (u8 *)(zhdr + 1);
+ memcpy(buf, dst, dlen);
+ zbud_unmap(tree->pool, handle);
+ put_cpu_var(zswap_dstmem);
+
+ /* populate entry */
+ entry->offset = offset;
+ entry->handle = handle;
+ entry->length = dlen;
+
+ /* map */
+ spin_lock(&tree->lock);
+ do {
+ ret = zswap_rb_insert(&tree->rbroot, entry, &dupentry);
+ if (ret == -EEXIST) {
+ zswap_duplicate_entry++;
+ /* remove from rbtree */
+ rb_erase(&dupentry->rbnode, &tree->rbroot);
+ if (!zswap_entry_put(dupentry)) {
+ /* free */
+ zswap_free_entry(tree, dupentry);
+ }
+ }
+ } while (ret == -EEXIST);
+ spin_unlock(&tree->lock);
+
+ /* update stats */
+ atomic_inc(&zswap_stored_pages);
+ atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
+
+ return 0;
+
+freepage:
+ put_cpu_var(zswap_dstmem);
+ zswap_entry_cache_free(entry);
+reject:
+ return ret;
+}
+
+/*
+ * returns 0 if the page was successfully decompressed
+ * return -1 on entry not found or error
+*/
+static int zswap_frontswap_load(unsigned type, pgoff_t offset,
+ struct page *page)
+{
+ struct zswap_tree *tree = zswap_trees[type];
+ struct zswap_entry *entry;
+ u8 *src, *dst;
+ unsigned int dlen;
+ int refcount, ret;
+
+ /* find */
+ spin_lock(&tree->lock);
+ entry = zswap_rb_search(&tree->rbroot, offset);
+ if (!entry) {
+ /* entry was written back */
+ spin_unlock(&tree->lock);
+ return -1;
+ }
+ zswap_entry_get(entry);
+ spin_unlock(&tree->lock);
+
+ /* decompress */
+ dlen = PAGE_SIZE;
+ src = (u8 *)zbud_map(tree->pool, entry->handle) +
+ sizeof(struct zswap_header);
+ dst = kmap_atomic(page);
+ ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src, entry->length,
+ dst, &dlen);
+ kunmap_atomic(dst);
+ zbud_unmap(tree->pool, entry->handle);
+ BUG_ON(ret);
+
+ spin_lock(&tree->lock);
+ refcount = zswap_entry_put(entry);
+ if (likely(refcount)) {
+ spin_unlock(&tree->lock);
+ return 0;
+ }
+ spin_unlock(&tree->lock);
+
+ /*
+ * We don't have to unlink from the rbtree because
+ * zswap_writeback_entry() or zswap_frontswap_invalidate page()
+ * has already done this for us if we are the last reference.
+ */
+ /* free */
+
+ zswap_free_entry(tree, entry);
+
+ return 0;
+}
+
+/* invalidates a single page */
+static void zswap_frontswap_invalidate_page(unsigned type, pgoff_t offset)
+{
+ struct zswap_tree *tree = zswap_trees[type];
+ struct zswap_entry *entry;
+ int refcount;
+
+ /* find */
+ spin_lock(&tree->lock);
+ entry = zswap_rb_search(&tree->rbroot, offset);
+ if (!entry) {
+ /* entry was written back */
+ spin_unlock(&tree->lock);
+ return;
+ }
+
+ /* remove from rbtree */
+ rb_erase(&entry->rbnode, &tree->rbroot);
+
+ /* drop the initial reference from entry creation */
+ refcount = zswap_entry_put(entry);
+
+ spin_unlock(&tree->lock);
+
+ if (refcount) {
+ /* writeback in progress, writeback will free */
+ return;
+ }
+
+ /* free */
+ zswap_free_entry(tree, entry);
+}
+
+/* invalidates all pages for the given swap type */
+static void zswap_frontswap_invalidate_area(unsigned type)
+{
+ struct zswap_tree *tree = zswap_trees[type];
+ struct rb_node *node;
+ struct zswap_entry *entry;
+
+ if (!tree)
+ return;
+
+ /* walk the tree and free everything */
+ spin_lock(&tree->lock);
+ /*
+ * TODO: Even though this code should not be executed because
+ * the try_to_unuse() in swapoff should have emptied the tree,
+ * it is very wasteful to rebalance the tree after every
+ * removal when we are freeing the whole tree.
+ *
+ * If post-order traversal code is ever added to the rbtree
+ * implementation, it should be used here.
+ */
+ while ((node = rb_first(&tree->rbroot))) {
+ entry = rb_entry(node, struct zswap_entry, rbnode);
+ rb_erase(&entry->rbnode, &tree->rbroot);
+ zbud_free(tree->pool, entry->handle);
+ zswap_entry_cache_free(entry);
+ atomic_dec(&zswap_stored_pages);
+ }
+ tree->rbroot = RB_ROOT;
+ spin_unlock(&tree->lock);
+}
+
+static struct zbud_ops zswap_zbud_ops = {
+ .evict = zswap_writeback_entry
+};
+
+/* NOTE: this is called in atomic context from swapon and must not sleep */
+static void zswap_frontswap_init(unsigned type)
+{
+ struct zswap_tree *tree;
+
+ tree = kzalloc(sizeof(struct zswap_tree), GFP_ATOMIC);
+ if (!tree)
+ goto err;
+ tree->pool = zbud_create_pool(GFP_NOWAIT, &zswap_zbud_ops);
+ if (!tree->pool)
+ goto freetree;
+ tree->rbroot = RB_ROOT;
+ spin_lock_init(&tree->lock);
+ tree->type = type;
+ zswap_trees[type] = tree;
+ return;
+
+freetree:
+ kfree(tree);
+err:
+ pr_err("alloc failed, zswap disabled for swap type %d\n", type);
+}
+
+static struct frontswap_ops zswap_frontswap_ops = {
+ .store = zswap_frontswap_store,
+ .load = zswap_frontswap_load,
+ .invalidate_page = zswap_frontswap_invalidate_page,
+ .invalidate_area = zswap_frontswap_invalidate_area,
+ .init = zswap_frontswap_init
+};
+
+/*********************************
+* debugfs functions
+**********************************/
+#ifdef CONFIG_DEBUG_FS
+#include <linux/debugfs.h>
+
+static struct dentry *zswap_debugfs_root;
+
+static int __init zswap_debugfs_init(void)
+{
+ if (!debugfs_initialized())
+ return -ENODEV;
+
+ zswap_debugfs_root = debugfs_create_dir("zswap", NULL);
+ if (!zswap_debugfs_root)
+ return -ENOMEM;
+
+ debugfs_create_u64("pool_limit_hit", S_IRUGO,
+ zswap_debugfs_root, &zswap_pool_limit_hit);
+ debugfs_create_u64("reject_reclaim_fail", S_IRUGO,
+ zswap_debugfs_root, &zswap_reject_reclaim_fail);
+ debugfs_create_u64("reject_alloc_fail", S_IRUGO,
+ zswap_debugfs_root, &zswap_reject_alloc_fail);
+ debugfs_create_u64("reject_kmemcache_fail", S_IRUGO,
+ zswap_debugfs_root, &zswap_reject_kmemcache_fail);
+ debugfs_create_u64("reject_compress_poor", S_IRUGO,
+ zswap_debugfs_root, &zswap_reject_compress_poor);
+ debugfs_create_u64("written_back_pages", S_IRUGO,
+ zswap_debugfs_root, &zswap_written_back_pages);
+ debugfs_create_u64("duplicate_entry", S_IRUGO,
+ zswap_debugfs_root, &zswap_duplicate_entry);
+ debugfs_create_atomic_t("pool_pages", S_IRUGO,
+ zswap_debugfs_root, &zswap_pool_pages);
+ debugfs_create_atomic_t("stored_pages", S_IRUGO,
+ zswap_debugfs_root, &zswap_stored_pages);
+
+ return 0;
+}
+
+static void __exit zswap_debugfs_exit(void)
+{
+ debugfs_remove_recursive(zswap_debugfs_root);
+}
+#else
+static inline int __init zswap_debugfs_init(void)
+{
+ return 0;
+}
+
+static inline void __exit zswap_debugfs_exit(void) { }
+#endif
+
+/*********************************
+* module init and exit
+**********************************/
+static int __init init_zswap(void)
+{
+ if (!zswap_enabled)
+ return 0;
+
+ pr_info("loading zswap\n");
+ if (zswap_entry_cache_create()) {
+ pr_err("entry cache creation failed\n");
+ goto error;
+ }
+ if (zswap_comp_init()) {
+ pr_err("compressor initialization failed\n");
+ goto compfail;
+ }
+ if (zswap_cpu_init()) {
+ pr_err("per-cpu initialization failed\n");
+ goto pcpufail;
+ }
+ frontswap_register_ops(&zswap_frontswap_ops);
+ if (zswap_debugfs_init())
+ pr_warn("debugfs initialization failed\n");
+ return 0;
+pcpufail:
+ zswap_comp_exit();
+compfail:
+ zswap_entry_cache_destory();
+error:
+ return -ENOMEM;
+}
+/* must be late so crypto has time to come up */
+late_initcall(init_zswap);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Seth Jennings <[email protected]>");
+MODULE_DESCRIPTION("Compressed cache for swap pages");
--
1.7.9.5

2013-05-13 12:40:42

by Seth Jennings

[permalink] [raw]
Subject: [PATCHv11 1/4] debugfs: add get/set for atomic types

debugfs currently lack the ability to create attributes
that set/get atomic_t values.

This patch adds support for this through a new
debugfs_create_atomic_t() function.

Signed-off-by: Seth Jennings <[email protected]>
Acked-by: Greg Kroah-Hartman <[email protected]>
Acked-by: Mel Gorman <[email protected]>
---
fs/debugfs/file.c | 42 ++++++++++++++++++++++++++++++++++++++++++
include/linux/debugfs.h | 2 ++
lib/fault-inject.c | 21 ---------------------
3 files changed, 44 insertions(+), 21 deletions(-)

diff --git a/fs/debugfs/file.c b/fs/debugfs/file.c
index c5ca6ae..ff64bcd 100644
--- a/fs/debugfs/file.c
+++ b/fs/debugfs/file.c
@@ -21,6 +21,7 @@
#include <linux/debugfs.h>
#include <linux/io.h>
#include <linux/slab.h>
+#include <linux/atomic.h>

static ssize_t default_read_file(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
@@ -403,6 +404,47 @@ struct dentry *debugfs_create_size_t(const char *name, umode_t mode,
}
EXPORT_SYMBOL_GPL(debugfs_create_size_t);

+static int debugfs_atomic_t_set(void *data, u64 val)
+{
+ atomic_set((atomic_t *)data, val);
+ return 0;
+}
+static int debugfs_atomic_t_get(void *data, u64 *val)
+{
+ *val = atomic_read((atomic_t *)data);
+ return 0;
+}
+DEFINE_SIMPLE_ATTRIBUTE(fops_atomic_t, debugfs_atomic_t_get,
+ debugfs_atomic_t_set, "%lld\n");
+DEFINE_SIMPLE_ATTRIBUTE(fops_atomic_t_ro, debugfs_atomic_t_get, NULL, "%lld\n");
+DEFINE_SIMPLE_ATTRIBUTE(fops_atomic_t_wo, NULL, debugfs_atomic_t_set, "%lld\n");
+
+/**
+ * debugfs_create_atomic_t - create a debugfs file that is used to read and
+ * write an atomic_t value
+ * @name: a pointer to a string containing the name of the file to create.
+ * @mode: the permission that the file should have
+ * @parent: a pointer to the parent dentry for this file. This should be a
+ * directory dentry if set. If this parameter is %NULL, then the
+ * file will be created in the root of the debugfs filesystem.
+ * @value: a pointer to the variable that the file should read to and write
+ * from.
+ */
+struct dentry *debugfs_create_atomic_t(const char *name, umode_t mode,
+ struct dentry *parent, atomic_t *value)
+{
+ /* if there are no write bits set, make read only */
+ if (!(mode & S_IWUGO))
+ return debugfs_create_file(name, mode, parent, value,
+ &fops_atomic_t_ro);
+ /* if there are no read bits set, make write only */
+ if (!(mode & S_IRUGO))
+ return debugfs_create_file(name, mode, parent, value,
+ &fops_atomic_t_wo);
+
+ return debugfs_create_file(name, mode, parent, value, &fops_atomic_t);
+}
+EXPORT_SYMBOL_GPL(debugfs_create_atomic_t);

static ssize_t read_file_bool(struct file *file, char __user *user_buf,
size_t count, loff_t *ppos)
diff --git a/include/linux/debugfs.h b/include/linux/debugfs.h
index 63f2465..d68b4ea 100644
--- a/include/linux/debugfs.h
+++ b/include/linux/debugfs.h
@@ -79,6 +79,8 @@ struct dentry *debugfs_create_x64(const char *name, umode_t mode,
struct dentry *parent, u64 *value);
struct dentry *debugfs_create_size_t(const char *name, umode_t mode,
struct dentry *parent, size_t *value);
+struct dentry *debugfs_create_atomic_t(const char *name, umode_t mode,
+ struct dentry *parent, atomic_t *value);
struct dentry *debugfs_create_bool(const char *name, umode_t mode,
struct dentry *parent, u32 *value);

diff --git a/lib/fault-inject.c b/lib/fault-inject.c
index c5c7a76..d7d501e 100644
--- a/lib/fault-inject.c
+++ b/lib/fault-inject.c
@@ -182,27 +182,6 @@ static struct dentry *debugfs_create_stacktrace_depth(

#endif /* CONFIG_FAULT_INJECTION_STACKTRACE_FILTER */

-static int debugfs_atomic_t_set(void *data, u64 val)
-{
- atomic_set((atomic_t *)data, val);
- return 0;
-}
-
-static int debugfs_atomic_t_get(void *data, u64 *val)
-{
- *val = atomic_read((atomic_t *)data);
- return 0;
-}
-
-DEFINE_SIMPLE_ATTRIBUTE(fops_atomic_t, debugfs_atomic_t_get,
- debugfs_atomic_t_set, "%lld\n");
-
-static struct dentry *debugfs_create_atomic_t(const char *name, umode_t mode,
- struct dentry *parent, atomic_t *value)
-{
- return debugfs_create_file(name, mode, parent, value, &fops_atomic_t);
-}
-
struct dentry *fault_create_debugfs_attr(const char *name,
struct dentry *parent, struct fault_attr *attr)
{
--
1.7.9.5

2013-05-13 12:41:19

by Seth Jennings

[permalink] [raw]
Subject: [PATCHv11 2/4] zbud: add to mm/

zbud is an special purpose allocator for storing compressed pages. It is
designed to store up to two compressed pages per physical page. While this
design limits storage density, it has simple and deterministic reclaim
properties that make it preferable to a higher density approach when reclaim
will be used.

zbud works by storing compressed pages, or "zpages", together in pairs in a
single memory page called a "zbud page". The first buddy is "left
justifed" at the beginning of the zbud page, and the last buddy is "right
justified" at the end of the zbud page. The benefit is that if either
buddy is freed, the freed buddy space, coalesced with whatever slack space
that existed between the buddies, results in the largest possible free region
within the zbud page.

zbud also provides an attractive lower bound on density. The ratio of zpages
to zbud pages can not be less than 1. This ensures that zbud can never "do
harm" by using more pages to store zpages than the uncompressed zpages would
have used on their own.

This patch adds zbud to mm/ for later use by zswap.

Signed-off-by: Seth Jennings <[email protected]>
---
include/linux/zbud.h | 22 ++
mm/Kconfig | 10 +
mm/Makefile | 1 +
mm/zbud.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 597 insertions(+)
create mode 100644 include/linux/zbud.h
create mode 100644 mm/zbud.c

diff --git a/include/linux/zbud.h b/include/linux/zbud.h
new file mode 100644
index 0000000..954252b
--- /dev/null
+++ b/include/linux/zbud.h
@@ -0,0 +1,22 @@
+#ifndef _ZBUD_H_
+#define _ZBUD_H_
+
+#include <linux/types.h>
+
+struct zbud_pool;
+
+struct zbud_ops {
+ int (*evict)(struct zbud_pool *pool, unsigned long handle);
+};
+
+struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops);
+void zbud_destroy_pool(struct zbud_pool *pool);
+int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
+ unsigned long *handle);
+void zbud_free(struct zbud_pool *pool, unsigned long handle);
+int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries);
+void *zbud_map(struct zbud_pool *pool, unsigned long handle);
+void zbud_unmap(struct zbud_pool *pool, unsigned long handle);
+int zbud_get_pool_size(struct zbud_pool *pool);
+
+#endif /* _ZBUD_H_ */
diff --git a/mm/Kconfig b/mm/Kconfig
index e742d06..908f41b 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -477,3 +477,13 @@ config FRONTSWAP
and swap data is stored as normal on the matching swap device.

If unsure, say Y to enable frontswap.
+
+config ZBUD
+ tristate "Buddy allocator for compressed pages"
+ default n
+ help
+ zbud is an special purpose allocator for storing compressed pages.
+ It is designed to store up to two compressed pages per physical page.
+ While this design limits storage density, it has simple and
+ deterministic reclaim properties that make it preferable to a higher
+ density approach when reclaim will be used.
diff --git a/mm/Makefile b/mm/Makefile
index 72c5acb..95f0197 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -58,3 +58,4 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
obj-$(CONFIG_DEBUG_KMEMLEAK_TEST) += kmemleak-test.o
obj-$(CONFIG_CLEANCACHE) += cleancache.o
obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
+obj-$(CONFIG_ZBUD) += zbud.o
diff --git a/mm/zbud.c b/mm/zbud.c
new file mode 100644
index 0000000..e5bd0e6
--- /dev/null
+++ b/mm/zbud.c
@@ -0,0 +1,564 @@
+/*
+ * zbud.c - Buddy Allocator for Compressed Pages
+ *
+ * Copyright (C) 2013, Seth Jennings, IBM
+ *
+ * Concepts based on zcache internal zbud allocator by Dan Magenheimer.
+ *
+ * zbud is an special purpose allocator for storing compressed pages. It is
+ * designed to store up to two compressed pages per physical page. While this
+ * design limits storage density, it has simple and deterministic reclaim
+ * properties that make it preferable to a higher density approach when reclaim
+ * will be used.
+ *
+ * zbud works by storing compressed pages, or "zpages", together in pairs in a
+ * single memory page called a "zbud page". The first buddy is "left
+ * justifed" at the beginning of the zbud page, and the last buddy is "right
+ * justified" at the end of the zbud page. The benefit is that if either
+ * buddy is freed, the freed buddy space, coalesced with whatever slack space
+ * that existed between the buddies, results in the largest possible free region
+ * within the zbud page.
+ *
+ * zbud also provides an attractive lower bound on density. The ratio of zpages
+ * to zbud pages can not be less than 1. This ensures that zbud can never "do
+ * harm" by using more pages to store zpages than the uncompressed zpages would
+ * have used on their own.
+ *
+ * zbud pages are divided into "chunks". The size of the chunks is fixed at
+ * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
+ * into chunks allows organizing unbuddied zbud pages into a manageable number
+ * of unbuddied lists according to the number of free chunks available in the
+ * zbud page.
+ *
+ * The zbud API differs from that of conventional allocators in that the
+ * allocation function, zbud_alloc(), returns an opaque handle to the user,
+ * not a dereferenceable pointer. The user must map the handle using
+ * zbud_map() in order to get a usable pointer by which to access the
+ * allocation data and unmap the handle with zbud_unmap() when operations
+ * on the allocation data are complete.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/atomic.h>
+#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/preempt.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/zbud.h>
+
+/*****************
+ * Structures
+*****************/
+/**
+ * struct zbud_page - zbud page metadata overlay
+ * @page: typed reference to the underlying struct page
+ * @donotuse: this overlays the page flags and should not be used
+ * @first_chunks: the size of the first buddy in chunks, 0 if free
+ * @last_chunks: the size of the last buddy in chunks, 0 if free
+ * @buddy: links the zbud page into the unbuddied/buddied lists in the pool
+ * @lru: links the zbud page into the lru list in the pool
+ *
+ * This structure overlays the struct page to store metadata needed for a
+ * single storage page in for zbud. There is a BUILD_BUG_ON in zbud_init()
+ * that ensures this structure is not larger that struct page.
+ *
+ * The PG_reclaim flag of the underlying page is used for indicating
+ * that this zbud page is under reclaim (see zbud_reclaim_page())
+ */
+struct zbud_page {
+ union {
+ struct page page;
+ struct {
+ unsigned long donotuse;
+ u16 first_chunks;
+ u16 last_chunks;
+ struct list_head buddy;
+ struct list_head lru;
+ };
+ };
+};
+
+/*
+ * NCHUNKS_ORDER determines the internal allocation granularity, effectively
+ * adjusting internal fragmentation. It also determines the number of
+ * freelists maintained in each pool. NCHUNKS_ORDER of 6 means that the
+ * allocation granularity will be in chunks of size PAGE_SIZE/64, and there
+ * will be 64 freelists per pool.
+ */
+#define NCHUNKS_ORDER 6
+
+#define CHUNK_SHIFT (PAGE_SHIFT - NCHUNKS_ORDER)
+#define CHUNK_SIZE (1 << CHUNK_SHIFT)
+#define NCHUNKS (PAGE_SIZE >> CHUNK_SHIFT)
+
+/**
+ * struct zbud_pool - stores metadata for each zbud pool
+ * @lock: protects all pool lists and first|last_chunk fields of any
+ * zbud page in the pool
+ * @unbuddied: array of lists tracking zbud pages that only contain one buddy;
+ * the lists each zbud page is added to depends on the size of
+ * its free region.
+ * @buddied: list tracking the zbud pages that contain two buddies;
+ * these zbud pages are full
+ * @pages_nr: number of zbud pages in the pool.
+ * @ops: pointer to a structure of user defined operations specified at
+ * pool creation time.
+ *
+ * This structure is allocated at pool creation time and maintains metadata
+ * pertaining to a particular zbud pool.
+ */
+struct zbud_pool {
+ spinlock_t lock;
+ struct list_head unbuddied[NCHUNKS];
+ struct list_head buddied;
+ struct list_head lru;
+ atomic_t pages_nr;
+ struct zbud_ops *ops;
+};
+
+/*****************
+ * Helpers
+*****************/
+/* Just to make the code easier to read */
+enum buddy {
+ FIRST,
+ LAST
+};
+
+/* Converts an allocation size in bytes to size in zbud chunks */
+static inline int size_to_chunks(int size)
+{
+ return (size + CHUNK_SIZE - 1) >> CHUNK_SHIFT;
+}
+
+#define for_each_unbuddied_list(_iter, _begin) \
+ for ((_iter) = (_begin); (_iter) < NCHUNKS; (_iter)++)
+
+/* Initializes a zbud page from a newly allocated page */
+static inline struct zbud_page *init_zbud_page(struct page *page)
+{
+ struct zbud_page *zbpage = (struct zbud_page *)page;
+ zbpage->first_chunks = 0;
+ zbpage->last_chunks = 0;
+ INIT_LIST_HEAD(&zbpage->buddy);
+ INIT_LIST_HEAD(&zbpage->lru);
+ return zbpage;
+}
+
+/* Resets a zbud page so that it can be properly freed */
+static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
+{
+ struct page *page = &zbpage->page;
+ set_page_private(page, 0);
+ page->mapping = NULL;
+ page->index = 0;
+ page_mapcount_reset(page);
+ init_page_count(page);
+ INIT_LIST_HEAD(&page->lru);
+ return page;
+}
+
+/*
+ * Encodes the handle of a particular buddy within a zbud page
+ * Pool lock should be held as this function accesses first|last_chunks
+ */
+static inline unsigned long encode_handle(struct zbud_page *zbpage,
+ enum buddy bud)
+{
+ unsigned long handle;
+
+ /*
+ * For now, the encoded handle is actually just the pointer to the data
+ * but this might not always be the case. A little information hiding.
+ */
+ handle = (unsigned long)page_address(&zbpage->page);
+ if (bud == FIRST)
+ return handle;
+ handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
+ return handle;
+}
+
+/* Returns the zbud page where a given handle is stored */
+static inline struct zbud_page *handle_to_zbud_page(unsigned long handle)
+{
+ return (struct zbud_page *)(virt_to_page(handle));
+}
+
+/* Returns the number of free chunks in a zbud page */
+static inline int num_free_chunks(struct zbud_page *zbpage)
+{
+ /*
+ * Rather than branch for different situations, just use the fact that
+ * free buddies have a length of zero to simplify everything.
+ */
+ return NCHUNKS - zbpage->first_chunks - zbpage->last_chunks;
+}
+
+/*****************
+ * API Functions
+*****************/
+/**
+ * zbud_create_pool() - create a new zbud pool
+ * @gfp: gfp flags when allocating the zbud pool structure
+ * @ops: user-defined operations for the zbud pool
+ *
+ * Return: pointer to the new zbud pool or NULL if the metadata allocation
+ * failed.
+ */
+struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
+{
+ struct zbud_pool *pool;
+ int i;
+
+ pool = kmalloc(sizeof(struct zbud_pool), gfp);
+ if (!pool)
+ return NULL;
+ spin_lock_init(&pool->lock);
+ for_each_unbuddied_list(i, 0)
+ INIT_LIST_HEAD(&pool->unbuddied[i]);
+ INIT_LIST_HEAD(&pool->buddied);
+ INIT_LIST_HEAD(&pool->lru);
+ atomic_set(&pool->pages_nr, 0);
+ pool->ops = ops;
+ return pool;
+}
+EXPORT_SYMBOL_GPL(zbud_create_pool);
+
+/**
+ * zbud_destroy_pool() - destroys an existing zbud pool
+ * @pool: the zbud pool to be destroyed
+ */
+void zbud_destroy_pool(struct zbud_pool *pool)
+{
+ kfree(pool);
+}
+EXPORT_SYMBOL_GPL(zbud_destroy_pool);
+
+/**
+ * zbud_alloc() - allocates a region of a given size
+ * @pool: zbud pool from which to allocate
+ * @size: size in bytes of the desired allocation
+ * @gfp: gfp flags used if the pool needs to grow
+ * @handle: handle of the new allocation
+ *
+ * This function will attempt to find a free region in the pool large
+ * enough to satisfy the allocation request. First, it tries to use
+ * free space in the most recently used zbud page, at the beginning of
+ * the pool LRU list. If that zbud page is full or doesn't have the
+ * required free space, a best fit search of the unbuddied lists is
+ * performed. If no suitable free region is found, then a new page
+ * is allocated and added to the pool to satisfy the request.
+ *
+ * gfp should not set __GFP_HIGHMEM as highmem pages cannot be used
+ * as zbud pool pages.
+ *
+ * Return: 0 if success and handle is set, otherwise -EINVAL is the size or
+ * gfp arguments are invalid or -ENOMEM if the pool was unable to allocate
+ * a new page.
+ */
+int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
+ unsigned long *handle)
+{
+ int chunks, i, freechunks;
+ struct zbud_page *zbpage = NULL;
+ enum buddy bud;
+ struct page *page;
+
+ if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
+ return -EINVAL;
+ chunks = size_to_chunks(size);
+ spin_lock(&pool->lock);
+
+ /*
+ * First, try to use the zbpage we last used (at the head of the
+ * LRU) to increase LRU locality of the buddies. This is first fit.
+ */
+ if (!list_empty(&pool->lru)) {
+ zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
+ if (num_free_chunks(zbpage) >= chunks) {
+ if (zbpage->first_chunks == 0) {
+ list_del(&zbpage->buddy);
+ bud = FIRST;
+ goto found;
+ }
+ if (zbpage->last_chunks == 0) {
+ list_del(&zbpage->buddy);
+ bud = LAST;
+ goto found;
+ }
+ }
+ }
+
+ /* Second, try to find an unbuddied zbpage. This is best fit. */
+ zbpage = NULL;
+ for_each_unbuddied_list(i, chunks) {
+ if (!list_empty(&pool->unbuddied[i])) {
+ zbpage = list_first_entry(&pool->unbuddied[i],
+ struct zbud_page, buddy);
+ list_del(&zbpage->buddy);
+ if (zbpage->first_chunks == 0)
+ bud = FIRST;
+ else
+ bud = LAST;
+ goto found;
+ }
+ }
+
+ /* Lastly, couldn't find unbuddied zbpage, create new one */
+ spin_unlock(&pool->lock);
+ page = alloc_page(gfp);
+ if (!page)
+ return -ENOMEM;
+ spin_lock(&pool->lock);
+ atomic_inc(&pool->pages_nr);
+ zbpage = init_zbud_page(page);
+ bud = FIRST;
+
+found:
+ if (bud == FIRST)
+ zbpage->first_chunks = chunks;
+ else
+ zbpage->last_chunks = chunks;
+
+ if (zbpage->first_chunks == 0 || zbpage->last_chunks == 0) {
+ /* Add to unbuddied list */
+ freechunks = num_free_chunks(zbpage);
+ list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
+ } else {
+ /* Add to buddied list */
+ list_add(&zbpage->buddy, &pool->buddied);
+ }
+
+ /* Add/move zbpage to beginning of LRU */
+ if (!list_empty(&zbpage->lru))
+ list_del(&zbpage->lru);
+ list_add(&zbpage->lru, &pool->lru);
+
+ *handle = encode_handle(zbpage, bud);
+ spin_unlock(&pool->lock);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(zbud_alloc);
+
+/**
+ * zbud_free() - frees the allocation associated with the given handle
+ * @pool: pool in which the allocation resided
+ * @handle: handle associated with the allocation returned by zbud_alloc()
+ *
+ * In the case that the zbud page in which the allocation resides is under
+ * reclaim, as indicated by the PG_reclaim flag being set, this function
+ * only sets the first|last_chunks to 0. The page is actually freed
+ * once both buddies are evicted (see zbud_reclaim_page() below).
+ */
+void zbud_free(struct zbud_pool *pool, unsigned long handle)
+{
+ struct zbud_page *zbpage;
+ int freechunks;
+
+ spin_lock(&pool->lock);
+ zbpage = handle_to_zbud_page(handle);
+
+ /* If first buddy, handle will be page aligned */
+ if (handle & ~PAGE_MASK)
+ zbpage->last_chunks = 0;
+ else
+ zbpage->first_chunks = 0;
+
+ if (PageReclaim(&zbpage->page)) {
+ /* zbpage is under reclaim, reclaim will free */
+ spin_unlock(&pool->lock);
+ return;
+ }
+
+ /* Remove from existing buddy list */
+ list_del(&zbpage->buddy);
+
+ if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
+ /* zbpage is empty, free */
+ list_del(&zbpage->lru);
+ __free_page(reset_zbud_page(zbpage));
+ atomic_dec(&pool->pages_nr);
+ } else {
+ /* Add to unbuddied list */
+ freechunks = num_free_chunks(zbpage);
+ list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
+ }
+
+ spin_unlock(&pool->lock);
+}
+EXPORT_SYMBOL_GPL(zbud_free);
+
+#define list_tail_entry(ptr, type, member) \
+ list_entry((ptr)->prev, type, member)
+
+/**
+ * zbud_reclaim_page() - evicts allocations from a pool page and frees it
+ * @pool: pool from which a page will attempt to be evicted
+ * @retires: number of pages on the LRU list for which eviction will
+ * be attempted before failing
+ *
+ * zbud reclaim is different from normal system reclaim in that the reclaim is
+ * done from the bottom, up. This is because only the bottom layer, zbud, has
+ * information on how the allocations are organized within each zbud page. This
+ * has the potential to create interesting locking situations between zbud and
+ * the user, however.
+ *
+ * To avoid these, this is how zbud_reclaim_page() should be called:
+
+ * The user detects a page should be reclaimed and calls zbud_reclaim_page().
+ * zbud_reclaim_page() will remove a zbud page from the pool LRU list and call
+ * the user-defined eviction handler with the pool and handle as arguments.
+ *
+ * If the handle can not be evicted, the eviction handler should return
+ * non-zero. zbud_reclaim_page() will add the zbud page back to the
+ * appropriate list and try the next zbud page on the LRU up to
+ * a user defined number of retries.
+ *
+ * If the handle is successfully evicted, the eviction handler should
+ * return 0 _and_ should have called zbud_free() on the handle. zbud_free()
+ * contains logic to delay freeing the page if the page is under reclaim,
+ * as indicated by the setting of the PG_reclaim flag on the underlying page.
+ *
+ * If all buddies in the zbud page are successfully evicted, then the
+ * zbud page can be freed.
+ *
+ * Returns: 0 if page is successfully freed, otherwise -EINVAL if there are
+ * no pages to evict or an eviction handler is not registered, -EAGAIN if
+ * the retry limit was hit.
+ */
+int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries)
+{
+ int i, ret, freechunks;
+ struct zbud_page *zbpage;
+ unsigned long first_handle = 0, last_handle = 0;
+
+ spin_lock(&pool->lock);
+ if (!pool->ops || !pool->ops->evict || list_empty(&pool->lru) ||
+ retries == 0) {
+ spin_unlock(&pool->lock);
+ return -EINVAL;
+ }
+ for (i = 0; i < retries; i++) {
+ zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
+ list_del(&zbpage->lru);
+ list_del(&zbpage->buddy);
+ /* Protect zbpage against free */
+ SetPageReclaim(&zbpage->page);
+ /*
+ * We need encode the handles before unlocking, since we can
+ * race with free that will set (first|last)_chunks to 0
+ */
+ first_handle = 0;
+ last_handle = 0;
+ if (zbpage->first_chunks)
+ first_handle = encode_handle(zbpage, FIRST);
+ if (zbpage->last_chunks)
+ last_handle = encode_handle(zbpage, LAST);
+ spin_unlock(&pool->lock);
+
+ /* Issue the eviction callback(s) */
+ if (first_handle) {
+ ret = pool->ops->evict(pool, first_handle);
+ if (ret)
+ goto next;
+ }
+ if (last_handle) {
+ ret = pool->ops->evict(pool, last_handle);
+ if (ret)
+ goto next;
+ }
+next:
+ spin_lock(&pool->lock);
+ ClearPageReclaim(&zbpage->page);
+ if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
+ /*
+ * Both buddies are now free, free the zbpage and
+ * return success.
+ */
+ __free_page(reset_zbud_page(zbpage));
+ atomic_dec(&pool->pages_nr);
+ spin_unlock(&pool->lock);
+ return 0;
+ } else if (zbpage->first_chunks == 0 ||
+ zbpage->last_chunks == 0) {
+ /* add to unbuddied list */
+ freechunks = num_free_chunks(zbpage);
+ list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
+ } else {
+ /* add to buddied list */
+ list_add(&zbpage->buddy, &pool->buddied);
+ }
+
+ /* add to beginning of LRU */
+ list_add(&zbpage->lru, &pool->lru);
+ }
+ spin_unlock(&pool->lock);
+ return -EAGAIN;
+}
+EXPORT_SYMBOL_GPL(zbud_reclaim_page);
+
+/**
+ * zbud_map() - maps the allocation associated with the given handle
+ * @pool: pool in which the allocation resides
+ * @handle: handle associated with the allocation to be mapped
+ *
+ * While trivial for zbud, the mapping functions for others allocators
+ * implementing this allocation API could have more complex information encoded
+ * in the handle and could create temporary mappings to make the data
+ * accessible to the user.
+ *
+ * Returns: a pointer to the mapped allocation
+ */
+void *zbud_map(struct zbud_pool *pool, unsigned long handle)
+{
+ return (void *)(handle);
+}
+EXPORT_SYMBOL_GPL(zbud_map);
+
+/**
+ * zbud_unmap() - maps the allocation associated with the given handle
+ * @pool: pool in which the allocation resides
+ * @handle: handle associated with the allocation to be unmapped
+ */
+void zbud_unmap(struct zbud_pool *pool, unsigned long handle)
+{
+}
+EXPORT_SYMBOL_GPL(zbud_unmap);
+
+/**
+ * zbud_get_pool_size() - gets the zbud pool size in pages
+ * @pool: pool whose size is being queried
+ *
+ * Returns: size in pages of the given pool
+ */
+int zbud_get_pool_size(struct zbud_pool *pool)
+{
+ return atomic_read(&pool->pages_nr);
+}
+EXPORT_SYMBOL_GPL(zbud_get_pool_size);
+
+static int __init init_zbud(void)
+{
+ /* Make sure we aren't overflowing the underlying struct page */
+ BUILD_BUG_ON(sizeof(struct zbud_page) != sizeof(struct page));
+ /* Make sure we can represent any chunk offset with a u16 */
+ BUILD_BUG_ON(sizeof(u16) * BITS_PER_BYTE < PAGE_SHIFT - CHUNK_SHIFT);
+ pr_info("loaded\n");
+ return 0;
+}
+
+static void __exit exit_zbud(void)
+{
+ pr_info("unloaded\n");
+}
+
+module_init(init_zbud);
+module_exit(exit_zbud);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Seth Jennings <[email protected]>");
+MODULE_DESCRIPTION("Buddy Allocator for Compressed Pages");
--
1.7.9.5

2013-05-13 15:46:07

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 2/4] zbud: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Sent: Monday, May 13, 2013 6:40 AM
> Subject: [PATCHv11 2/4] zbud: add to mm/

One comment about a questionable algorithm change (vs my original zbud code)
below... I'll leave the detailed code review to others.

Dan

> zbud is an special purpose allocator for storing compressed pages. It is
> designed to store up to two compressed pages per physical page. While this
> design limits storage density, it has simple and deterministic reclaim
> properties that make it preferable to a higher density approach when reclaim
> will be used.
>
> zbud works by storing compressed pages, or "zpages", together in pairs in a
> single memory page called a "zbud page". The first buddy is "left
> justifed" at the beginning of the zbud page, and the last buddy is "right
> justified" at the end of the zbud page. The benefit is that if either
> buddy is freed, the freed buddy space, coalesced with whatever slack space
> that existed between the buddies, results in the largest possible free region
> within the zbud page.
>
> zbud also provides an attractive lower bound on density. The ratio of zpages
> to zbud pages can not be less than 1. This ensures that zbud can never "do
> harm" by using more pages to store zpages than the uncompressed zpages would
> have used on their own.
>
> This patch adds zbud to mm/ for later use by zswap.
>
> Signed-off-by: Seth Jennings <[email protected]>
> ---
> include/linux/zbud.h | 22 ++
> mm/Kconfig | 10 +
> mm/Makefile | 1 +
> mm/zbud.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 597 insertions(+)
> create mode 100644 include/linux/zbud.h
> create mode 100644 mm/zbud.c
>
> diff --git a/include/linux/zbud.h b/include/linux/zbud.h
> new file mode 100644
> index 0000000..954252b
> --- /dev/null
> +++ b/include/linux/zbud.h
> @@ -0,0 +1,22 @@
> +#ifndef _ZBUD_H_
> +#define _ZBUD_H_
> +
> +#include <linux/types.h>
> +
> +struct zbud_pool;
> +
> +struct zbud_ops {
> + int (*evict)(struct zbud_pool *pool, unsigned long handle);
> +};
> +
> +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops);
> +void zbud_destroy_pool(struct zbud_pool *pool);
> +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> + unsigned long *handle);
> +void zbud_free(struct zbud_pool *pool, unsigned long handle);
> +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries);
> +void *zbud_map(struct zbud_pool *pool, unsigned long handle);
> +void zbud_unmap(struct zbud_pool *pool, unsigned long handle);
> +int zbud_get_pool_size(struct zbud_pool *pool);
> +
> +#endif /* _ZBUD_H_ */
> diff --git a/mm/Kconfig b/mm/Kconfig
> index e742d06..908f41b 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -477,3 +477,13 @@ config FRONTSWAP
> and swap data is stored as normal on the matching swap device.
>
> If unsure, say Y to enable frontswap.
> +
> +config ZBUD
> + tristate "Buddy allocator for compressed pages"
> + default n
> + help
> + zbud is an special purpose allocator for storing compressed pages.
> + It is designed to store up to two compressed pages per physical page.
> + While this design limits storage density, it has simple and
> + deterministic reclaim properties that make it preferable to a higher
> + density approach when reclaim will be used.
> diff --git a/mm/Makefile b/mm/Makefile
> index 72c5acb..95f0197 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -58,3 +58,4 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
> obj-$(CONFIG_DEBUG_KMEMLEAK_TEST) += kmemleak-test.o
> obj-$(CONFIG_CLEANCACHE) += cleancache.o
> obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
> +obj-$(CONFIG_ZBUD) += zbud.o
> diff --git a/mm/zbud.c b/mm/zbud.c
> new file mode 100644
> index 0000000..e5bd0e6
> --- /dev/null
> +++ b/mm/zbud.c
> @@ -0,0 +1,564 @@
> +/*
> + * zbud.c - Buddy Allocator for Compressed Pages
> + *
> + * Copyright (C) 2013, Seth Jennings, IBM
> + *
> + * Concepts based on zcache internal zbud allocator by Dan Magenheimer.
> + *
> + * zbud is an special purpose allocator for storing compressed pages. It is
> + * designed to store up to two compressed pages per physical page. While this
> + * design limits storage density, it has simple and deterministic reclaim
> + * properties that make it preferable to a higher density approach when reclaim
> + * will be used.
> + *
> + * zbud works by storing compressed pages, or "zpages", together in pairs in a
> + * single memory page called a "zbud page". The first buddy is "left
> + * justifed" at the beginning of the zbud page, and the last buddy is "right
> + * justified" at the end of the zbud page. The benefit is that if either
> + * buddy is freed, the freed buddy space, coalesced with whatever slack space
> + * that existed between the buddies, results in the largest possible free region
> + * within the zbud page.
> + *
> + * zbud also provides an attractive lower bound on density. The ratio of zpages
> + * to zbud pages can not be less than 1. This ensures that zbud can never "do
> + * harm" by using more pages to store zpages than the uncompressed zpages would
> + * have used on their own.
> + *
> + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> + * into chunks allows organizing unbuddied zbud pages into a manageable number
> + * of unbuddied lists according to the number of free chunks available in the
> + * zbud page.
> + *
> + * The zbud API differs from that of conventional allocators in that the
> + * allocation function, zbud_alloc(), returns an opaque handle to the user,
> + * not a dereferenceable pointer. The user must map the handle using
> + * zbud_map() in order to get a usable pointer by which to access the
> + * allocation data and unmap the handle with zbud_unmap() when operations
> + * on the allocation data are complete.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/atomic.h>
> +#include <linux/list.h>
> +#include <linux/mm.h>
> +#include <linux/module.h>
> +#include <linux/preempt.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/zbud.h>
> +
> +/*****************
> + * Structures
> +*****************/
> +/**
> + * struct zbud_page - zbud page metadata overlay
> + * @page: typed reference to the underlying struct page
> + * @donotuse: this overlays the page flags and should not be used
> + * @first_chunks: the size of the first buddy in chunks, 0 if free
> + * @last_chunks: the size of the last buddy in chunks, 0 if free
> + * @buddy: links the zbud page into the unbuddied/buddied lists in the pool
> + * @lru: links the zbud page into the lru list in the pool
> + *
> + * This structure overlays the struct page to store metadata needed for a
> + * single storage page in for zbud. There is a BUILD_BUG_ON in zbud_init()
> + * that ensures this structure is not larger that struct page.
> + *
> + * The PG_reclaim flag of the underlying page is used for indicating
> + * that this zbud page is under reclaim (see zbud_reclaim_page())
> + */
> +struct zbud_page {
> + union {
> + struct page page;
> + struct {
> + unsigned long donotuse;
> + u16 first_chunks;
> + u16 last_chunks;
> + struct list_head buddy;
> + struct list_head lru;
> + };
> + };
> +};
> +
> +/*
> + * NCHUNKS_ORDER determines the internal allocation granularity, effectively
> + * adjusting internal fragmentation. It also determines the number of
> + * freelists maintained in each pool. NCHUNKS_ORDER of 6 means that the
> + * allocation granularity will be in chunks of size PAGE_SIZE/64, and there
> + * will be 64 freelists per pool.
> + */
> +#define NCHUNKS_ORDER 6
> +
> +#define CHUNK_SHIFT (PAGE_SHIFT - NCHUNKS_ORDER)
> +#define CHUNK_SIZE (1 << CHUNK_SHIFT)
> +#define NCHUNKS (PAGE_SIZE >> CHUNK_SHIFT)
> +
> +/**
> + * struct zbud_pool - stores metadata for each zbud pool
> + * @lock: protects all pool lists and first|last_chunk fields of any
> + * zbud page in the pool
> + * @unbuddied: array of lists tracking zbud pages that only contain one buddy;
> + * the lists each zbud page is added to depends on the size of
> + * its free region.
> + * @buddied: list tracking the zbud pages that contain two buddies;
> + * these zbud pages are full
> + * @pages_nr: number of zbud pages in the pool.
> + * @ops: pointer to a structure of user defined operations specified at
> + * pool creation time.
> + *
> + * This structure is allocated at pool creation time and maintains metadata
> + * pertaining to a particular zbud pool.
> + */
> +struct zbud_pool {
> + spinlock_t lock;
> + struct list_head unbuddied[NCHUNKS];
> + struct list_head buddied;
> + struct list_head lru;
> + atomic_t pages_nr;
> + struct zbud_ops *ops;
> +};
> +
> +/*****************
> + * Helpers
> +*****************/
> +/* Just to make the code easier to read */
> +enum buddy {
> + FIRST,
> + LAST
> +};
> +
> +/* Converts an allocation size in bytes to size in zbud chunks */
> +static inline int size_to_chunks(int size)
> +{
> + return (size + CHUNK_SIZE - 1) >> CHUNK_SHIFT;
> +}
> +
> +#define for_each_unbuddied_list(_iter, _begin) \
> + for ((_iter) = (_begin); (_iter) < NCHUNKS; (_iter)++)
> +
> +/* Initializes a zbud page from a newly allocated page */
> +static inline struct zbud_page *init_zbud_page(struct page *page)
> +{
> + struct zbud_page *zbpage = (struct zbud_page *)page;
> + zbpage->first_chunks = 0;
> + zbpage->last_chunks = 0;
> + INIT_LIST_HEAD(&zbpage->buddy);
> + INIT_LIST_HEAD(&zbpage->lru);
> + return zbpage;
> +}
> +
> +/* Resets a zbud page so that it can be properly freed */
> +static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
> +{
> + struct page *page = &zbpage->page;
> + set_page_private(page, 0);
> + page->mapping = NULL;
> + page->index = 0;
> + page_mapcount_reset(page);
> + init_page_count(page);
> + INIT_LIST_HEAD(&page->lru);
> + return page;
> +}
> +
> +/*
> + * Encodes the handle of a particular buddy within a zbud page
> + * Pool lock should be held as this function accesses first|last_chunks
> + */
> +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> + enum buddy bud)
> +{
> + unsigned long handle;
> +
> + /*
> + * For now, the encoded handle is actually just the pointer to the data
> + * but this might not always be the case. A little information hiding.
> + */
> + handle = (unsigned long)page_address(&zbpage->page);
> + if (bud == FIRST)
> + return handle;
> + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> + return handle;
> +}
> +
> +/* Returns the zbud page where a given handle is stored */
> +static inline struct zbud_page *handle_to_zbud_page(unsigned long handle)
> +{
> + return (struct zbud_page *)(virt_to_page(handle));
> +}
> +
> +/* Returns the number of free chunks in a zbud page */
> +static inline int num_free_chunks(struct zbud_page *zbpage)
> +{
> + /*
> + * Rather than branch for different situations, just use the fact that
> + * free buddies have a length of zero to simplify everything.
> + */
> + return NCHUNKS - zbpage->first_chunks - zbpage->last_chunks;
> +}
> +
> +/*****************
> + * API Functions
> +*****************/
> +/**
> + * zbud_create_pool() - create a new zbud pool
> + * @gfp: gfp flags when allocating the zbud pool structure
> + * @ops: user-defined operations for the zbud pool
> + *
> + * Return: pointer to the new zbud pool or NULL if the metadata allocation
> + * failed.
> + */
> +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
> +{
> + struct zbud_pool *pool;
> + int i;
> +
> + pool = kmalloc(sizeof(struct zbud_pool), gfp);
> + if (!pool)
> + return NULL;
> + spin_lock_init(&pool->lock);
> + for_each_unbuddied_list(i, 0)
> + INIT_LIST_HEAD(&pool->unbuddied[i]);
> + INIT_LIST_HEAD(&pool->buddied);
> + INIT_LIST_HEAD(&pool->lru);
> + atomic_set(&pool->pages_nr, 0);
> + pool->ops = ops;
> + return pool;
> +}
> +EXPORT_SYMBOL_GPL(zbud_create_pool);
> +
> +/**
> + * zbud_destroy_pool() - destroys an existing zbud pool
> + * @pool: the zbud pool to be destroyed
> + */
> +void zbud_destroy_pool(struct zbud_pool *pool)
> +{
> + kfree(pool);
> +}
> +EXPORT_SYMBOL_GPL(zbud_destroy_pool);
> +
> +/**
> + * zbud_alloc() - allocates a region of a given size
> + * @pool: zbud pool from which to allocate
> + * @size: size in bytes of the desired allocation
> + * @gfp: gfp flags used if the pool needs to grow
> + * @handle: handle of the new allocation
> + *
> + * This function will attempt to find a free region in the pool large
> + * enough to satisfy the allocation request. First, it tries to use
> + * free space in the most recently used zbud page, at the beginning of
> + * the pool LRU list. If that zbud page is full or doesn't have the
> + * required free space, a best fit search of the unbuddied lists is
> + * performed. If no suitable free region is found, then a new page
> + * is allocated and added to the pool to satisfy the request.
> + *
> + * gfp should not set __GFP_HIGHMEM as highmem pages cannot be used
> + * as zbud pool pages.
> + *
> + * Return: 0 if success and handle is set, otherwise -EINVAL is the size or
> + * gfp arguments are invalid or -ENOMEM if the pool was unable to allocate
> + * a new page.
> + */
> +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> + unsigned long *handle)
> +{
> + int chunks, i, freechunks;
> + struct zbud_page *zbpage = NULL;
> + enum buddy bud;
> + struct page *page;
> +
> + if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
> + return -EINVAL;
> + chunks = size_to_chunks(size);
> + spin_lock(&pool->lock);
> +
> + /*
> + * First, try to use the zbpage we last used (at the head of the
> + * LRU) to increase LRU locality of the buddies. This is first fit.
> + */
> + if (!list_empty(&pool->lru)) {
> + zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
> + if (num_free_chunks(zbpage) >= chunks) {
> + if (zbpage->first_chunks == 0) {
> + list_del(&zbpage->buddy);
> + bud = FIRST;
> + goto found;
> + }
> + if (zbpage->last_chunks == 0) {
> + list_del(&zbpage->buddy);
> + bud = LAST;
> + goto found;
> + }
> + }
> + }

The above appears to be a new addition to my original zbud design.
While it may appear to be a good idea for improving LRU-ness, I
suspect it may have unexpected side effects in that I think far
fewer "fat" zpages will be buddied, which will result in many more
unbuddied pages containing a single fat zpage, which means much worse
overall density on many workloads.

This may not be apparent in kernbench or specjbb or any workload
where the vast majority of zpages compress to less than PAGE_SIZE/2,
but for a zsize distribution that is symmetric or "skews fat",
it may become very apparent.

> + /* Second, try to find an unbuddied zbpage. This is best fit. */
> + zbpage = NULL;
> + for_each_unbuddied_list(i, chunks) {
> + if (!list_empty(&pool->unbuddied[i])) {
> + zbpage = list_first_entry(&pool->unbuddied[i],
> + struct zbud_page, buddy);
> + list_del(&zbpage->buddy);
> + if (zbpage->first_chunks == 0)
> + bud = FIRST;
> + else
> + bud = LAST;
> + goto found;
> + }
> + }
> +
> + /* Lastly, couldn't find unbuddied zbpage, create new one */
> + spin_unlock(&pool->lock);
> + page = alloc_page(gfp);
> + if (!page)
> + return -ENOMEM;
> + spin_lock(&pool->lock);
> + atomic_inc(&pool->pages_nr);
> + zbpage = init_zbud_page(page);
> + bud = FIRST;
> +
> +found:
> + if (bud == FIRST)
> + zbpage->first_chunks = chunks;
> + else
> + zbpage->last_chunks = chunks;
> +
> + if (zbpage->first_chunks == 0 || zbpage->last_chunks == 0) {
> + /* Add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + } else {
> + /* Add to buddied list */
> + list_add(&zbpage->buddy, &pool->buddied);
> + }
> +
> + /* Add/move zbpage to beginning of LRU */
> + if (!list_empty(&zbpage->lru))
> + list_del(&zbpage->lru);
> + list_add(&zbpage->lru, &pool->lru);
> +
> + *handle = encode_handle(zbpage, bud);
> + spin_unlock(&pool->lock);
> +
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(zbud_alloc);
> +
> +/**
> + * zbud_free() - frees the allocation associated with the given handle
> + * @pool: pool in which the allocation resided
> + * @handle: handle associated with the allocation returned by zbud_alloc()
> + *
> + * In the case that the zbud page in which the allocation resides is under
> + * reclaim, as indicated by the PG_reclaim flag being set, this function
> + * only sets the first|last_chunks to 0. The page is actually freed
> + * once both buddies are evicted (see zbud_reclaim_page() below).
> + */
> +void zbud_free(struct zbud_pool *pool, unsigned long handle)
> +{
> + struct zbud_page *zbpage;
> + int freechunks;
> +
> + spin_lock(&pool->lock);
> + zbpage = handle_to_zbud_page(handle);
> +
> + /* If first buddy, handle will be page aligned */
> + if (handle & ~PAGE_MASK)
> + zbpage->last_chunks = 0;
> + else
> + zbpage->first_chunks = 0;
> +
> + if (PageReclaim(&zbpage->page)) {
> + /* zbpage is under reclaim, reclaim will free */
> + spin_unlock(&pool->lock);
> + return;
> + }
> +
> + /* Remove from existing buddy list */
> + list_del(&zbpage->buddy);
> +
> + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> + /* zbpage is empty, free */
> + list_del(&zbpage->lru);
> + __free_page(reset_zbud_page(zbpage));
> + atomic_dec(&pool->pages_nr);
> + } else {
> + /* Add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + }
> +
> + spin_unlock(&pool->lock);
> +}
> +EXPORT_SYMBOL_GPL(zbud_free);
> +
> +#define list_tail_entry(ptr, type, member) \
> + list_entry((ptr)->prev, type, member)
> +
> +/**
> + * zbud_reclaim_page() - evicts allocations from a pool page and frees it
> + * @pool: pool from which a page will attempt to be evicted
> + * @retires: number of pages on the LRU list for which eviction will
> + * be attempted before failing
> + *
> + * zbud reclaim is different from normal system reclaim in that the reclaim is
> + * done from the bottom, up. This is because only the bottom layer, zbud, has
> + * information on how the allocations are organized within each zbud page. This
> + * has the potential to create interesting locking situations between zbud and
> + * the user, however.
> + *
> + * To avoid these, this is how zbud_reclaim_page() should be called:
> +
> + * The user detects a page should be reclaimed and calls zbud_reclaim_page().
> + * zbud_reclaim_page() will remove a zbud page from the pool LRU list and call
> + * the user-defined eviction handler with the pool and handle as arguments.
> + *
> + * If the handle can not be evicted, the eviction handler should return
> + * non-zero. zbud_reclaim_page() will add the zbud page back to the
> + * appropriate list and try the next zbud page on the LRU up to
> + * a user defined number of retries.
> + *
> + * If the handle is successfully evicted, the eviction handler should
> + * return 0 _and_ should have called zbud_free() on the handle. zbud_free()
> + * contains logic to delay freeing the page if the page is under reclaim,
> + * as indicated by the setting of the PG_reclaim flag on the underlying page.
> + *
> + * If all buddies in the zbud page are successfully evicted, then the
> + * zbud page can be freed.
> + *
> + * Returns: 0 if page is successfully freed, otherwise -EINVAL if there are
> + * no pages to evict or an eviction handler is not registered, -EAGAIN if
> + * the retry limit was hit.
> + */
> +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries)
> +{
> + int i, ret, freechunks;
> + struct zbud_page *zbpage;
> + unsigned long first_handle = 0, last_handle = 0;
> +
> + spin_lock(&pool->lock);
> + if (!pool->ops || !pool->ops->evict || list_empty(&pool->lru) ||
> + retries == 0) {
> + spin_unlock(&pool->lock);
> + return -EINVAL;
> + }
> + for (i = 0; i < retries; i++) {
> + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> + list_del(&zbpage->lru);
> + list_del(&zbpage->buddy);
> + /* Protect zbpage against free */
> + SetPageReclaim(&zbpage->page);
> + /*
> + * We need encode the handles before unlocking, since we can
> + * race with free that will set (first|last)_chunks to 0
> + */
> + first_handle = 0;
> + last_handle = 0;
> + if (zbpage->first_chunks)
> + first_handle = encode_handle(zbpage, FIRST);
> + if (zbpage->last_chunks)
> + last_handle = encode_handle(zbpage, LAST);
> + spin_unlock(&pool->lock);
> +
> + /* Issue the eviction callback(s) */
> + if (first_handle) {
> + ret = pool->ops->evict(pool, first_handle);
> + if (ret)
> + goto next;
> + }
> + if (last_handle) {
> + ret = pool->ops->evict(pool, last_handle);
> + if (ret)
> + goto next;
> + }
> +next:
> + spin_lock(&pool->lock);
> + ClearPageReclaim(&zbpage->page);
> + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> + /*
> + * Both buddies are now free, free the zbpage and
> + * return success.
> + */
> + __free_page(reset_zbud_page(zbpage));
> + atomic_dec(&pool->pages_nr);
> + spin_unlock(&pool->lock);
> + return 0;
> + } else if (zbpage->first_chunks == 0 ||
> + zbpage->last_chunks == 0) {
> + /* add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + } else {
> + /* add to buddied list */
> + list_add(&zbpage->buddy, &pool->buddied);
> + }
> +
> + /* add to beginning of LRU */
> + list_add(&zbpage->lru, &pool->lru);
> + }
> + spin_unlock(&pool->lock);
> + return -EAGAIN;
> +}
> +EXPORT_SYMBOL_GPL(zbud_reclaim_page);
> +
> +/**
> + * zbud_map() - maps the allocation associated with the given handle
> + * @pool: pool in which the allocation resides
> + * @handle: handle associated with the allocation to be mapped
> + *
> + * While trivial for zbud, the mapping functions for others allocators
> + * implementing this allocation API could have more complex information encoded
> + * in the handle and could create temporary mappings to make the data
> + * accessible to the user.
> + *
> + * Returns: a pointer to the mapped allocation
> + */
> +void *zbud_map(struct zbud_pool *pool, unsigned long handle)
> +{
> + return (void *)(handle);
> +}
> +EXPORT_SYMBOL_GPL(zbud_map);
> +
> +/**
> + * zbud_unmap() - maps the allocation associated with the given handle
> + * @pool: pool in which the allocation resides
> + * @handle: handle associated with the allocation to be unmapped
> + */
> +void zbud_unmap(struct zbud_pool *pool, unsigned long handle)
> +{
> +}
> +EXPORT_SYMBOL_GPL(zbud_unmap);
> +
> +/**
> + * zbud_get_pool_size() - gets the zbud pool size in pages
> + * @pool: pool whose size is being queried
> + *
> + * Returns: size in pages of the given pool
> + */
> +int zbud_get_pool_size(struct zbud_pool *pool)
> +{
> + return atomic_read(&pool->pages_nr);
> +}
> +EXPORT_SYMBOL_GPL(zbud_get_pool_size);
> +
> +static int __init init_zbud(void)
> +{
> + /* Make sure we aren't overflowing the underlying struct page */
> + BUILD_BUG_ON(sizeof(struct zbud_page) != sizeof(struct page));
> + /* Make sure we can represent any chunk offset with a u16 */
> + BUILD_BUG_ON(sizeof(u16) * BITS_PER_BYTE < PAGE_SHIFT - CHUNK_SHIFT);
> + pr_info("loaded\n");
> + return 0;
> +}
> +
> +static void __exit exit_zbud(void)
> +{
> + pr_info("unloaded\n");
> +}
> +
> +module_init(init_zbud);
> +module_exit(exit_zbud);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> +MODULE_DESCRIPTION("Buddy Allocator for Compressed Pages");
> --
> 1.7.9.5

2013-05-13 20:59:37

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Mon, May 13, 2013 at 08:43:36AM -0700, Dan Magenheimer wrote:
> > From: Seth Jennings [mailto:[email protected]]
> > Sent: Monday, May 13, 2013 6:40 AM
> > Subject: [PATCHv11 2/4] zbud: add to mm/
>
> One comment about a questionable algorithm change (vs my original zbud code)
> below... I'll leave the detailed code review to others.
>
> Dan
>
> > zbud is an special purpose allocator for storing compressed pages. It is
> > designed to store up to two compressed pages per physical page. While this
> > design limits storage density, it has simple and deterministic reclaim
> > properties that make it preferable to a higher density approach when reclaim
> > will be used.
> >
> > zbud works by storing compressed pages, or "zpages", together in pairs in a
> > single memory page called a "zbud page". The first buddy is "left
> > justifed" at the beginning of the zbud page, and the last buddy is "right
> > justified" at the end of the zbud page. The benefit is that if either
> > buddy is freed, the freed buddy space, coalesced with whatever slack space
> > that existed between the buddies, results in the largest possible free region
> > within the zbud page.
> >
> > zbud also provides an attractive lower bound on density. The ratio of zpages
> > to zbud pages can not be less than 1. This ensures that zbud can never "do
> > harm" by using more pages to store zpages than the uncompressed zpages would
> > have used on their own.
> >
> > This patch adds zbud to mm/ for later use by zswap.
> >
> > Signed-off-by: Seth Jennings <[email protected]>
> > ---
> > include/linux/zbud.h | 22 ++
> > mm/Kconfig | 10 +
> > mm/Makefile | 1 +
> > mm/zbud.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++
> > 4 files changed, 597 insertions(+)
> > create mode 100644 include/linux/zbud.h
> > create mode 100644 mm/zbud.c
> >
> > diff --git a/include/linux/zbud.h b/include/linux/zbud.h
> > new file mode 100644
> > index 0000000..954252b
> > --- /dev/null
> > +++ b/include/linux/zbud.h
> > @@ -0,0 +1,22 @@
> > +#ifndef _ZBUD_H_
> > +#define _ZBUD_H_
> > +
> > +#include <linux/types.h>
> > +
> > +struct zbud_pool;
> > +
> > +struct zbud_ops {
> > + int (*evict)(struct zbud_pool *pool, unsigned long handle);
> > +};
> > +
> > +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops);
> > +void zbud_destroy_pool(struct zbud_pool *pool);
> > +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> > + unsigned long *handle);
> > +void zbud_free(struct zbud_pool *pool, unsigned long handle);
> > +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries);
> > +void *zbud_map(struct zbud_pool *pool, unsigned long handle);
> > +void zbud_unmap(struct zbud_pool *pool, unsigned long handle);
> > +int zbud_get_pool_size(struct zbud_pool *pool);
> > +
> > +#endif /* _ZBUD_H_ */
> > diff --git a/mm/Kconfig b/mm/Kconfig
> > index e742d06..908f41b 100644
> > --- a/mm/Kconfig
> > +++ b/mm/Kconfig
> > @@ -477,3 +477,13 @@ config FRONTSWAP
> > and swap data is stored as normal on the matching swap device.
> >
> > If unsure, say Y to enable frontswap.
> > +
> > +config ZBUD
> > + tristate "Buddy allocator for compressed pages"
> > + default n
> > + help
> > + zbud is an special purpose allocator for storing compressed pages.
> > + It is designed to store up to two compressed pages per physical page.
> > + While this design limits storage density, it has simple and
> > + deterministic reclaim properties that make it preferable to a higher
> > + density approach when reclaim will be used.
> > diff --git a/mm/Makefile b/mm/Makefile
> > index 72c5acb..95f0197 100644
> > --- a/mm/Makefile
> > +++ b/mm/Makefile
> > @@ -58,3 +58,4 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
> > obj-$(CONFIG_DEBUG_KMEMLEAK_TEST) += kmemleak-test.o
> > obj-$(CONFIG_CLEANCACHE) += cleancache.o
> > obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
> > +obj-$(CONFIG_ZBUD) += zbud.o
> > diff --git a/mm/zbud.c b/mm/zbud.c
> > new file mode 100644
> > index 0000000..e5bd0e6
> > --- /dev/null
> > +++ b/mm/zbud.c
> > @@ -0,0 +1,564 @@
> > +/*
> > + * zbud.c - Buddy Allocator for Compressed Pages
> > + *
> > + * Copyright (C) 2013, Seth Jennings, IBM
> > + *
> > + * Concepts based on zcache internal zbud allocator by Dan Magenheimer.
> > + *
> > + * zbud is an special purpose allocator for storing compressed pages. It is
> > + * designed to store up to two compressed pages per physical page. While this
> > + * design limits storage density, it has simple and deterministic reclaim
> > + * properties that make it preferable to a higher density approach when reclaim
> > + * will be used.
> > + *
> > + * zbud works by storing compressed pages, or "zpages", together in pairs in a
> > + * single memory page called a "zbud page". The first buddy is "left
> > + * justifed" at the beginning of the zbud page, and the last buddy is "right
> > + * justified" at the end of the zbud page. The benefit is that if either
> > + * buddy is freed, the freed buddy space, coalesced with whatever slack space
> > + * that existed between the buddies, results in the largest possible free region
> > + * within the zbud page.
> > + *
> > + * zbud also provides an attractive lower bound on density. The ratio of zpages
> > + * to zbud pages can not be less than 1. This ensures that zbud can never "do
> > + * harm" by using more pages to store zpages than the uncompressed zpages would
> > + * have used on their own.
> > + *
> > + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> > + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> > + * into chunks allows organizing unbuddied zbud pages into a manageable number
> > + * of unbuddied lists according to the number of free chunks available in the
> > + * zbud page.
> > + *
> > + * The zbud API differs from that of conventional allocators in that the
> > + * allocation function, zbud_alloc(), returns an opaque handle to the user,
> > + * not a dereferenceable pointer. The user must map the handle using
> > + * zbud_map() in order to get a usable pointer by which to access the
> > + * allocation data and unmap the handle with zbud_unmap() when operations
> > + * on the allocation data are complete.
> > + */
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/atomic.h>
> > +#include <linux/list.h>
> > +#include <linux/mm.h>
> > +#include <linux/module.h>
> > +#include <linux/preempt.h>
> > +#include <linux/slab.h>
> > +#include <linux/spinlock.h>
> > +#include <linux/zbud.h>
> > +
> > +/*****************
> > + * Structures
> > +*****************/
> > +/**
> > + * struct zbud_page - zbud page metadata overlay
> > + * @page: typed reference to the underlying struct page
> > + * @donotuse: this overlays the page flags and should not be used
> > + * @first_chunks: the size of the first buddy in chunks, 0 if free
> > + * @last_chunks: the size of the last buddy in chunks, 0 if free
> > + * @buddy: links the zbud page into the unbuddied/buddied lists in the pool
> > + * @lru: links the zbud page into the lru list in the pool
> > + *
> > + * This structure overlays the struct page to store metadata needed for a
> > + * single storage page in for zbud. There is a BUILD_BUG_ON in zbud_init()
> > + * that ensures this structure is not larger that struct page.
> > + *
> > + * The PG_reclaim flag of the underlying page is used for indicating
> > + * that this zbud page is under reclaim (see zbud_reclaim_page())
> > + */
> > +struct zbud_page {
> > + union {
> > + struct page page;
> > + struct {
> > + unsigned long donotuse;
> > + u16 first_chunks;
> > + u16 last_chunks;
> > + struct list_head buddy;
> > + struct list_head lru;
> > + };
> > + };
> > +};
> > +
> > +/*
> > + * NCHUNKS_ORDER determines the internal allocation granularity, effectively
> > + * adjusting internal fragmentation. It also determines the number of
> > + * freelists maintained in each pool. NCHUNKS_ORDER of 6 means that the
> > + * allocation granularity will be in chunks of size PAGE_SIZE/64, and there
> > + * will be 64 freelists per pool.
> > + */
> > +#define NCHUNKS_ORDER 6
> > +
> > +#define CHUNK_SHIFT (PAGE_SHIFT - NCHUNKS_ORDER)
> > +#define CHUNK_SIZE (1 << CHUNK_SHIFT)
> > +#define NCHUNKS (PAGE_SIZE >> CHUNK_SHIFT)
> > +
> > +/**
> > + * struct zbud_pool - stores metadata for each zbud pool
> > + * @lock: protects all pool lists and first|last_chunk fields of any
> > + * zbud page in the pool
> > + * @unbuddied: array of lists tracking zbud pages that only contain one buddy;
> > + * the lists each zbud page is added to depends on the size of
> > + * its free region.
> > + * @buddied: list tracking the zbud pages that contain two buddies;
> > + * these zbud pages are full
> > + * @pages_nr: number of zbud pages in the pool.
> > + * @ops: pointer to a structure of user defined operations specified at
> > + * pool creation time.
> > + *
> > + * This structure is allocated at pool creation time and maintains metadata
> > + * pertaining to a particular zbud pool.
> > + */
> > +struct zbud_pool {
> > + spinlock_t lock;
> > + struct list_head unbuddied[NCHUNKS];
> > + struct list_head buddied;
> > + struct list_head lru;
> > + atomic_t pages_nr;
> > + struct zbud_ops *ops;
> > +};
> > +
> > +/*****************
> > + * Helpers
> > +*****************/
> > +/* Just to make the code easier to read */
> > +enum buddy {
> > + FIRST,
> > + LAST
> > +};
> > +
> > +/* Converts an allocation size in bytes to size in zbud chunks */
> > +static inline int size_to_chunks(int size)
> > +{
> > + return (size + CHUNK_SIZE - 1) >> CHUNK_SHIFT;
> > +}
> > +
> > +#define for_each_unbuddied_list(_iter, _begin) \
> > + for ((_iter) = (_begin); (_iter) < NCHUNKS; (_iter)++)
> > +
> > +/* Initializes a zbud page from a newly allocated page */
> > +static inline struct zbud_page *init_zbud_page(struct page *page)
> > +{
> > + struct zbud_page *zbpage = (struct zbud_page *)page;
> > + zbpage->first_chunks = 0;
> > + zbpage->last_chunks = 0;
> > + INIT_LIST_HEAD(&zbpage->buddy);
> > + INIT_LIST_HEAD(&zbpage->lru);
> > + return zbpage;
> > +}
> > +
> > +/* Resets a zbud page so that it can be properly freed */
> > +static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
> > +{
> > + struct page *page = &zbpage->page;
> > + set_page_private(page, 0);
> > + page->mapping = NULL;
> > + page->index = 0;
> > + page_mapcount_reset(page);
> > + init_page_count(page);
> > + INIT_LIST_HEAD(&page->lru);
> > + return page;
> > +}
> > +
> > +/*
> > + * Encodes the handle of a particular buddy within a zbud page
> > + * Pool lock should be held as this function accesses first|last_chunks
> > + */
> > +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> > + enum buddy bud)
> > +{
> > + unsigned long handle;
> > +
> > + /*
> > + * For now, the encoded handle is actually just the pointer to the data
> > + * but this might not always be the case. A little information hiding.
> > + */
> > + handle = (unsigned long)page_address(&zbpage->page);
> > + if (bud == FIRST)
> > + return handle;
> > + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> > + return handle;
> > +}
> > +
> > +/* Returns the zbud page where a given handle is stored */
> > +static inline struct zbud_page *handle_to_zbud_page(unsigned long handle)
> > +{
> > + return (struct zbud_page *)(virt_to_page(handle));
> > +}
> > +
> > +/* Returns the number of free chunks in a zbud page */
> > +static inline int num_free_chunks(struct zbud_page *zbpage)
> > +{
> > + /*
> > + * Rather than branch for different situations, just use the fact that
> > + * free buddies have a length of zero to simplify everything.
> > + */
> > + return NCHUNKS - zbpage->first_chunks - zbpage->last_chunks;
> > +}
> > +
> > +/*****************
> > + * API Functions
> > +*****************/
> > +/**
> > + * zbud_create_pool() - create a new zbud pool
> > + * @gfp: gfp flags when allocating the zbud pool structure
> > + * @ops: user-defined operations for the zbud pool
> > + *
> > + * Return: pointer to the new zbud pool or NULL if the metadata allocation
> > + * failed.
> > + */
> > +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
> > +{
> > + struct zbud_pool *pool;
> > + int i;
> > +
> > + pool = kmalloc(sizeof(struct zbud_pool), gfp);
> > + if (!pool)
> > + return NULL;
> > + spin_lock_init(&pool->lock);
> > + for_each_unbuddied_list(i, 0)
> > + INIT_LIST_HEAD(&pool->unbuddied[i]);
> > + INIT_LIST_HEAD(&pool->buddied);
> > + INIT_LIST_HEAD(&pool->lru);
> > + atomic_set(&pool->pages_nr, 0);
> > + pool->ops = ops;
> > + return pool;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_create_pool);
> > +
> > +/**
> > + * zbud_destroy_pool() - destroys an existing zbud pool
> > + * @pool: the zbud pool to be destroyed
> > + */
> > +void zbud_destroy_pool(struct zbud_pool *pool)
> > +{
> > + kfree(pool);
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_destroy_pool);
> > +
> > +/**
> > + * zbud_alloc() - allocates a region of a given size
> > + * @pool: zbud pool from which to allocate
> > + * @size: size in bytes of the desired allocation
> > + * @gfp: gfp flags used if the pool needs to grow
> > + * @handle: handle of the new allocation
> > + *
> > + * This function will attempt to find a free region in the pool large
> > + * enough to satisfy the allocation request. First, it tries to use
> > + * free space in the most recently used zbud page, at the beginning of
> > + * the pool LRU list. If that zbud page is full or doesn't have the
> > + * required free space, a best fit search of the unbuddied lists is
> > + * performed. If no suitable free region is found, then a new page
> > + * is allocated and added to the pool to satisfy the request.
> > + *
> > + * gfp should not set __GFP_HIGHMEM as highmem pages cannot be used
> > + * as zbud pool pages.
> > + *
> > + * Return: 0 if success and handle is set, otherwise -EINVAL is the size or
> > + * gfp arguments are invalid or -ENOMEM if the pool was unable to allocate
> > + * a new page.
> > + */
> > +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> > + unsigned long *handle)
> > +{
> > + int chunks, i, freechunks;
> > + struct zbud_page *zbpage = NULL;
> > + enum buddy bud;
> > + struct page *page;
> > +
> > + if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
> > + return -EINVAL;
> > + chunks = size_to_chunks(size);
> > + spin_lock(&pool->lock);
> > +
> > + /*
> > + * First, try to use the zbpage we last used (at the head of the
> > + * LRU) to increase LRU locality of the buddies. This is first fit.
> > + */
> > + if (!list_empty(&pool->lru)) {
> > + zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
> > + if (num_free_chunks(zbpage) >= chunks) {
> > + if (zbpage->first_chunks == 0) {
> > + list_del(&zbpage->buddy);
> > + bud = FIRST;
> > + goto found;
> > + }
> > + if (zbpage->last_chunks == 0) {
> > + list_del(&zbpage->buddy);
> > + bud = LAST;
> > + goto found;
> > + }
> > + }
> > + }
>
> The above appears to be a new addition to my original zbud design.
> While it may appear to be a good idea for improving LRU-ness, I
> suspect it may have unexpected side effects in that I think far
> fewer "fat" zpages will be buddied, which will result in many more
> unbuddied pages containing a single fat zpage, which means much worse
> overall density on many workloads.

Yes, I see what you are saying. While I can't think of a workload that would
cause this kind of allocation pattern in practice, I also don't have a way to
measure the impact this first-fit fast path code has on density.

>
> This may not be apparent in kernbench or specjbb or any workload
> where the vast majority of zpages compress to less than PAGE_SIZE/2,
> but for a zsize distribution that is symmetric or "skews fat",
> it may become very apparent.

I'd personally think it should be kept because 1) it makes a fast allocation
path and 2) improves LRU locality. But, without numbers to demonstrate a
performance improvements or impacts on density, I wouldn't be opposed to taking
it out if it is a point of contention.

Anyone else care to weigh in?

Seth

>
> > + /* Second, try to find an unbuddied zbpage. This is best fit. */
> > + zbpage = NULL;
> > + for_each_unbuddied_list(i, chunks) {
> > + if (!list_empty(&pool->unbuddied[i])) {
> > + zbpage = list_first_entry(&pool->unbuddied[i],
> > + struct zbud_page, buddy);
> > + list_del(&zbpage->buddy);
> > + if (zbpage->first_chunks == 0)
> > + bud = FIRST;
> > + else
> > + bud = LAST;
> > + goto found;
> > + }
> > + }
> > +
> > + /* Lastly, couldn't find unbuddied zbpage, create new one */
> > + spin_unlock(&pool->lock);
> > + page = alloc_page(gfp);
> > + if (!page)
> > + return -ENOMEM;
> > + spin_lock(&pool->lock);
> > + atomic_inc(&pool->pages_nr);
> > + zbpage = init_zbud_page(page);
> > + bud = FIRST;
> > +
> > +found:
> > + if (bud == FIRST)
> > + zbpage->first_chunks = chunks;
> > + else
> > + zbpage->last_chunks = chunks;
> > +
> > + if (zbpage->first_chunks == 0 || zbpage->last_chunks == 0) {
> > + /* Add to unbuddied list */
> > + freechunks = num_free_chunks(zbpage);
> > + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> > + } else {
> > + /* Add to buddied list */
> > + list_add(&zbpage->buddy, &pool->buddied);
> > + }
> > +
> > + /* Add/move zbpage to beginning of LRU */
> > + if (!list_empty(&zbpage->lru))
> > + list_del(&zbpage->lru);
> > + list_add(&zbpage->lru, &pool->lru);
> > +
> > + *handle = encode_handle(zbpage, bud);
> > + spin_unlock(&pool->lock);
> > +
> > + return 0;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_alloc);
> > +
> > +/**
> > + * zbud_free() - frees the allocation associated with the given handle
> > + * @pool: pool in which the allocation resided
> > + * @handle: handle associated with the allocation returned by zbud_alloc()
> > + *
> > + * In the case that the zbud page in which the allocation resides is under
> > + * reclaim, as indicated by the PG_reclaim flag being set, this function
> > + * only sets the first|last_chunks to 0. The page is actually freed
> > + * once both buddies are evicted (see zbud_reclaim_page() below).
> > + */
> > +void zbud_free(struct zbud_pool *pool, unsigned long handle)
> > +{
> > + struct zbud_page *zbpage;
> > + int freechunks;
> > +
> > + spin_lock(&pool->lock);
> > + zbpage = handle_to_zbud_page(handle);
> > +
> > + /* If first buddy, handle will be page aligned */
> > + if (handle & ~PAGE_MASK)
> > + zbpage->last_chunks = 0;
> > + else
> > + zbpage->first_chunks = 0;
> > +
> > + if (PageReclaim(&zbpage->page)) {
> > + /* zbpage is under reclaim, reclaim will free */
> > + spin_unlock(&pool->lock);
> > + return;
> > + }
> > +
> > + /* Remove from existing buddy list */
> > + list_del(&zbpage->buddy);
> > +
> > + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> > + /* zbpage is empty, free */
> > + list_del(&zbpage->lru);
> > + __free_page(reset_zbud_page(zbpage));
> > + atomic_dec(&pool->pages_nr);
> > + } else {
> > + /* Add to unbuddied list */
> > + freechunks = num_free_chunks(zbpage);
> > + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> > + }
> > +
> > + spin_unlock(&pool->lock);
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_free);
> > +
> > +#define list_tail_entry(ptr, type, member) \
> > + list_entry((ptr)->prev, type, member)
> > +
> > +/**
> > + * zbud_reclaim_page() - evicts allocations from a pool page and frees it
> > + * @pool: pool from which a page will attempt to be evicted
> > + * @retires: number of pages on the LRU list for which eviction will
> > + * be attempted before failing
> > + *
> > + * zbud reclaim is different from normal system reclaim in that the reclaim is
> > + * done from the bottom, up. This is because only the bottom layer, zbud, has
> > + * information on how the allocations are organized within each zbud page. This
> > + * has the potential to create interesting locking situations between zbud and
> > + * the user, however.
> > + *
> > + * To avoid these, this is how zbud_reclaim_page() should be called:
> > +
> > + * The user detects a page should be reclaimed and calls zbud_reclaim_page().
> > + * zbud_reclaim_page() will remove a zbud page from the pool LRU list and call
> > + * the user-defined eviction handler with the pool and handle as arguments.
> > + *
> > + * If the handle can not be evicted, the eviction handler should return
> > + * non-zero. zbud_reclaim_page() will add the zbud page back to the
> > + * appropriate list and try the next zbud page on the LRU up to
> > + * a user defined number of retries.
> > + *
> > + * If the handle is successfully evicted, the eviction handler should
> > + * return 0 _and_ should have called zbud_free() on the handle. zbud_free()
> > + * contains logic to delay freeing the page if the page is under reclaim,
> > + * as indicated by the setting of the PG_reclaim flag on the underlying page.
> > + *
> > + * If all buddies in the zbud page are successfully evicted, then the
> > + * zbud page can be freed.
> > + *
> > + * Returns: 0 if page is successfully freed, otherwise -EINVAL if there are
> > + * no pages to evict or an eviction handler is not registered, -EAGAIN if
> > + * the retry limit was hit.
> > + */
> > +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries)
> > +{
> > + int i, ret, freechunks;
> > + struct zbud_page *zbpage;
> > + unsigned long first_handle = 0, last_handle = 0;
> > +
> > + spin_lock(&pool->lock);
> > + if (!pool->ops || !pool->ops->evict || list_empty(&pool->lru) ||
> > + retries == 0) {
> > + spin_unlock(&pool->lock);
> > + return -EINVAL;
> > + }
> > + for (i = 0; i < retries; i++) {
> > + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> > + list_del(&zbpage->lru);
> > + list_del(&zbpage->buddy);
> > + /* Protect zbpage against free */
> > + SetPageReclaim(&zbpage->page);
> > + /*
> > + * We need encode the handles before unlocking, since we can
> > + * race with free that will set (first|last)_chunks to 0
> > + */
> > + first_handle = 0;
> > + last_handle = 0;
> > + if (zbpage->first_chunks)
> > + first_handle = encode_handle(zbpage, FIRST);
> > + if (zbpage->last_chunks)
> > + last_handle = encode_handle(zbpage, LAST);
> > + spin_unlock(&pool->lock);
> > +
> > + /* Issue the eviction callback(s) */
> > + if (first_handle) {
> > + ret = pool->ops->evict(pool, first_handle);
> > + if (ret)
> > + goto next;
> > + }
> > + if (last_handle) {
> > + ret = pool->ops->evict(pool, last_handle);
> > + if (ret)
> > + goto next;
> > + }
> > +next:
> > + spin_lock(&pool->lock);
> > + ClearPageReclaim(&zbpage->page);
> > + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> > + /*
> > + * Both buddies are now free, free the zbpage and
> > + * return success.
> > + */
> > + __free_page(reset_zbud_page(zbpage));
> > + atomic_dec(&pool->pages_nr);
> > + spin_unlock(&pool->lock);
> > + return 0;
> > + } else if (zbpage->first_chunks == 0 ||
> > + zbpage->last_chunks == 0) {
> > + /* add to unbuddied list */
> > + freechunks = num_free_chunks(zbpage);
> > + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> > + } else {
> > + /* add to buddied list */
> > + list_add(&zbpage->buddy, &pool->buddied);
> > + }
> > +
> > + /* add to beginning of LRU */
> > + list_add(&zbpage->lru, &pool->lru);
> > + }
> > + spin_unlock(&pool->lock);
> > + return -EAGAIN;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_reclaim_page);
> > +
> > +/**
> > + * zbud_map() - maps the allocation associated with the given handle
> > + * @pool: pool in which the allocation resides
> > + * @handle: handle associated with the allocation to be mapped
> > + *
> > + * While trivial for zbud, the mapping functions for others allocators
> > + * implementing this allocation API could have more complex information encoded
> > + * in the handle and could create temporary mappings to make the data
> > + * accessible to the user.
> > + *
> > + * Returns: a pointer to the mapped allocation
> > + */
> > +void *zbud_map(struct zbud_pool *pool, unsigned long handle)
> > +{
> > + return (void *)(handle);
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_map);
> > +
> > +/**
> > + * zbud_unmap() - maps the allocation associated with the given handle
> > + * @pool: pool in which the allocation resides
> > + * @handle: handle associated with the allocation to be unmapped
> > + */
> > +void zbud_unmap(struct zbud_pool *pool, unsigned long handle)
> > +{
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_unmap);
> > +
> > +/**
> > + * zbud_get_pool_size() - gets the zbud pool size in pages
> > + * @pool: pool whose size is being queried
> > + *
> > + * Returns: size in pages of the given pool
> > + */
> > +int zbud_get_pool_size(struct zbud_pool *pool)
> > +{
> > + return atomic_read(&pool->pages_nr);
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_get_pool_size);
> > +
> > +static int __init init_zbud(void)
> > +{
> > + /* Make sure we aren't overflowing the underlying struct page */
> > + BUILD_BUG_ON(sizeof(struct zbud_page) != sizeof(struct page));
> > + /* Make sure we can represent any chunk offset with a u16 */
> > + BUILD_BUG_ON(sizeof(u16) * BITS_PER_BYTE < PAGE_SHIFT - CHUNK_SHIFT);
> > + pr_info("loaded\n");
> > + return 0;
> > +}
> > +
> > +static void __exit exit_zbud(void)
> > +{
> > + pr_info("unloaded\n");
> > +}
> > +
> > +module_init(init_zbud);
> > +module_exit(exit_zbud);
> > +
> > +MODULE_LICENSE("GPL");
> > +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> > +MODULE_DESCRIPTION("Buddy Allocator for Compressed Pages");
> > --
> > 1.7.9.5
>

2013-05-13 22:32:28

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Subject: [PATCHv11 3/4] zswap: add to mm/
>
> zswap is a thin compression backend for frontswap. It receives pages from
> frontswap and attempts to store them in a compressed memory pool, resulting in
> an effective partial memory reclaim and dramatically reduced swap device I/O.
>
> Additionally, in most cases, pages can be retrieved from this compressed store
> much more quickly than reading from tradition swap devices resulting in faster
> performance for many workloads.
>
> It also has support for evicting swap pages that are currently compressed in
> zswap to the swap device on an LRU(ish) basis. This functionality is very
> important and make zswap a true cache in that, once the cache is full or can't
> grow due to memory pressure, the oldest pages can be moved out of zswap to the
> swap device so newer pages can be compressed and stored in zswap.
>
> This patch adds the zswap driver to mm/
>
> Signed-off-by: Seth Jennings <[email protected]>

A couple of comments below...

> ---
> mm/Kconfig | 15 +
> mm/Makefile | 1 +
> mm/zswap.c | 952 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 968 insertions(+)
> create mode 100644 mm/zswap.c
>
> diff --git a/mm/Kconfig b/mm/Kconfig
> index 908f41b..4042e07 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -487,3 +487,18 @@ config ZBUD
> While this design limits storage density, it has simple and
> deterministic reclaim properties that make it preferable to a higher
> density approach when reclaim will be used.
> +
> +config ZSWAP
> + bool "In-kernel swap page compression"
> + depends on FRONTSWAP && CRYPTO
> + select CRYPTO_LZO
> + select ZBUD
> + default n
> + help
> + Zswap is a backend for the frontswap mechanism in the VMM.
> + It receives pages from frontswap and attempts to store them
> + in a compressed memory pool, resulting in an effective
> + partial memory reclaim. In addition, pages and be retrieved
> + from this compressed store much faster than most tradition
> + swap devices resulting in reduced I/O and faster performance
> + for many workloads.
> diff --git a/mm/Makefile b/mm/Makefile
> index 95f0197..f008033 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -32,6 +32,7 @@ obj-$(CONFIG_HAVE_MEMBLOCK) += memblock.o
> obj-$(CONFIG_BOUNCE) += bounce.o
> obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
> obj-$(CONFIG_FRONTSWAP) += frontswap.o
> +obj-$(CONFIG_ZSWAP) += zswap.o
> obj-$(CONFIG_HAS_DMA) += dmapool.o
> obj-$(CONFIG_HUGETLBFS) += hugetlb.o
> obj-$(CONFIG_NUMA) += mempolicy.o
> diff --git a/mm/zswap.c b/mm/zswap.c
> new file mode 100644
> index 0000000..b1070ca
> --- /dev/null
> +++ b/mm/zswap.c
> @@ -0,0 +1,952 @@
> +/*
> + * zswap.c - zswap driver file
> + *
> + * zswap is a backend for frontswap that takes pages that are in the
> + * process of being swapped out and attempts to compress them and store
> + * them in a RAM-based memory pool. This results in a significant I/O
> + * reduction on the real swap device and, in the case of a slow swap
> + * device, can also improve workload performance.
> + *
> + * Copyright (C) 2012 Seth Jennings <[email protected]>
> + *
> + * This program is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU General Public License
> + * as published by the Free Software Foundation; either version 2
> + * of the License, or (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> +*/
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/module.h>
> +#include <linux/cpu.h>
> +#include <linux/highmem.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/types.h>
> +#include <linux/atomic.h>
> +#include <linux/frontswap.h>
> +#include <linux/rbtree.h>
> +#include <linux/swap.h>
> +#include <linux/crypto.h>
> +#include <linux/mempool.h>
> +#include <linux/zbud.h>
> +
> +#include <linux/mm_types.h>
> +#include <linux/page-flags.h>
> +#include <linux/swapops.h>
> +#include <linux/writeback.h>
> +#include <linux/pagemap.h>
> +
> +/*********************************
> +* statistics
> +**********************************/
> +/* Number of memory pages used by the compressed pool */
> +static atomic_t zswap_pool_pages = ATOMIC_INIT(0);
> +/* The number of compressed pages currently stored in zswap */
> +static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
> +
> +/*
> + * The statistics below are not protected from concurrent access for
> + * performance reasons so they may not be a 100% accurate. However,
> + * they do provide useful information on roughly how many times a
> + * certain event is occurring.
> +*/
> +static u64 zswap_pool_limit_hit;
> +static u64 zswap_written_back_pages;
> +static u64 zswap_reject_reclaim_fail;
> +static u64 zswap_reject_compress_poor;
> +static u64 zswap_reject_alloc_fail;
> +static u64 zswap_reject_kmemcache_fail;
> +static u64 zswap_duplicate_entry;
> +
> +/*********************************
> +* tunables
> +**********************************/
> +/* Enable/disable zswap (disabled by default, fixed at boot for now) */
> +static bool zswap_enabled;
> +module_param_named(enabled, zswap_enabled, bool, 0);
> +
> +/* Compressor to be used by zswap (fixed at boot for now) */
> +#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
> +static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> +module_param_named(compressor, zswap_compressor, charp, 0);
> +
> +/* The maximum percentage of memory that the compressed pool can occupy */
> +static unsigned int zswap_max_pool_percent = 20;
> +module_param_named(max_pool_percent,
> + zswap_max_pool_percent, uint, 0644);

This limit, along with the code that enforces it (by calling reclaim
when the limit is reached), is IMHO questionable. Is there any
other kernel memory allocation that is constrained by a percentage
of total memory rather than dynamically according to current
system conditions? As Mel pointed out (approx.), if this limit
is reached by a zswap-storm and filled with pages of long-running,
rarely-used processes, 20% of RAM (by default here) becomes forever
clogged.

Zswap reclaim/writeback needs to be cognizant of (and perhaps driven
by) system memory pressure, not some user-settable percentage.
There's some tough policy questions that need to be answered here,
perhaps not before zswap gets merged, but certainly before it
gets enabled by default by distros.

> +/*
> + * Maximum compression ratio, as as percentage, for an acceptable
> + * compressed page. Any pages that do not compress by at least
> + * this ratio will be rejected.
> +*/
> +static unsigned int zswap_max_compression_ratio = 80;
> +module_param_named(max_compression_ratio,
> + zswap_max_compression_ratio, uint, 0644);

Per earlier discussion, this number is actually derived
from a zsmalloc constraint and doesn't necessarily apply
to zbud. And I don't think any mortal user or system
administrator would have any idea what value to change
this to or the potential impact of changing it. IMHO
it should be removed, or at least moved to and enforced
by the specific allocator code.

> +/*********************************
> +* compression functions
> +**********************************/
> +/* per-cpu compression transforms */
> +static struct crypto_comp * __percpu *zswap_comp_pcpu_tfms;
> +
> +enum comp_op {
> + ZSWAP_COMPOP_COMPRESS,
> + ZSWAP_COMPOP_DECOMPRESS
> +};
> +
> +static int zswap_comp_op(enum comp_op op, const u8 *src, unsigned int slen,
> + u8 *dst, unsigned int *dlen)
> +{
> + struct crypto_comp *tfm;
> + int ret;
> +
> + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, get_cpu());
> + switch (op) {
> + case ZSWAP_COMPOP_COMPRESS:
> + ret = crypto_comp_compress(tfm, src, slen, dst, dlen);
> + break;
> + case ZSWAP_COMPOP_DECOMPRESS:
> + ret = crypto_comp_decompress(tfm, src, slen, dst, dlen);
> + break;
> + default:
> + ret = -EINVAL;
> + }
> +
> + put_cpu();
> + return ret;
> +}
> +
> +static int __init zswap_comp_init(void)
> +{
> + if (!crypto_has_comp(zswap_compressor, 0, 0)) {
> + pr_info("%s compressor not available\n", zswap_compressor);
> + /* fall back to default compressor */
> + zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> + if (!crypto_has_comp(zswap_compressor, 0, 0))
> + /* can't even load the default compressor */
> + return -ENODEV;
> + }
> + pr_info("using %s compressor\n", zswap_compressor);
> +
> + /* alloc percpu transforms */
> + zswap_comp_pcpu_tfms = alloc_percpu(struct crypto_comp *);
> + if (!zswap_comp_pcpu_tfms)
> + return -ENOMEM;
> + return 0;
> +}
> +
> +static void zswap_comp_exit(void)
> +{
> + /* free percpu transforms */
> + if (zswap_comp_pcpu_tfms)
> + free_percpu(zswap_comp_pcpu_tfms);
> +}
> +
> +/*********************************
> +* data structures
> +**********************************/
> +/*
> + * struct zswap_entry
> + *
> + * This structure contains the metadata for tracking a single compressed
> + * page within zswap.
> + *
> + * rbnode - links the entry into red-black tree for the appropriate swap type
> + * refcount - the number of outstanding reference to the entry. This is needed
> + * to protect against premature freeing of the entry by code
> + * concurent calls to load, invalidate, and writeback. The lock
> + * for the zswap_tree structure that contains the entry must
> + * be held while changing the refcount. Since the lock must
> + * be held, there is no reason to also make refcount atomic.
> + * type - the swap type for the entry. Used to map back to the zswap_tree
> + * structure that contains the entry.
> + * offset - the swap offset for the entry. Index into the red-black tree.
> + * handle - zsmalloc allocation handle that stores the compressed page data
> + * length - the length in bytes of the compressed page data. Needed during
> + * decompression
> + */
> +struct zswap_entry {
> + struct rb_node rbnode;
> + pgoff_t offset;
> + int refcount;
> + unsigned int length;
> + unsigned long handle;
> +};
> +
> +struct zswap_header {
> + swp_entry_t swpentry;
> +};
> +
> +/*
> + * The tree lock in the zswap_tree struct protects a few things:
> + * - the rbtree
> + * - the refcount field of each entry in the tree
> + */
> +struct zswap_tree {
> + struct rb_root rbroot;
> + spinlock_t lock;
> + struct zbud_pool *pool;
> + unsigned type;
> +};
> +
> +static struct zswap_tree *zswap_trees[MAX_SWAPFILES];
> +
> +/*********************************
> +* zswap entry functions
> +**********************************/
> +#define ZSWAP_KMEM_CACHE_NAME "zswap_entry_cache"
> +static struct kmem_cache *zswap_entry_cache;
> +
> +static inline int zswap_entry_cache_create(void)
> +{
> + zswap_entry_cache =
> + kmem_cache_create(ZSWAP_KMEM_CACHE_NAME,
> + sizeof(struct zswap_entry), 0, 0, NULL);
> + return (zswap_entry_cache == NULL);
> +}
> +
> +static inline void zswap_entry_cache_destory(void)
> +{
> + kmem_cache_destroy(zswap_entry_cache);
> +}
> +
> +static inline struct zswap_entry *zswap_entry_cache_alloc(gfp_t gfp)
> +{
> + struct zswap_entry *entry;
> + entry = kmem_cache_alloc(zswap_entry_cache, gfp);
> + if (!entry)
> + return NULL;
> + entry->refcount = 1;
> + return entry;
> +}
> +
> +static inline void zswap_entry_cache_free(struct zswap_entry *entry)
> +{
> + kmem_cache_free(zswap_entry_cache, entry);
> +}
> +
> +static inline void zswap_entry_get(struct zswap_entry *entry)
> +{
> + entry->refcount++;
> +}
> +
> +static inline int zswap_entry_put(struct zswap_entry *entry)
> +{
> + entry->refcount--;
> + return entry->refcount;
> +}
> +
> +/*********************************
> +* rbtree functions
> +**********************************/
> +static struct zswap_entry *zswap_rb_search(struct rb_root *root, pgoff_t offset)
> +{
> + struct rb_node *node = root->rb_node;
> + struct zswap_entry *entry;
> +
> + while (node) {
> + entry = rb_entry(node, struct zswap_entry, rbnode);
> + if (entry->offset > offset)
> + node = node->rb_left;
> + else if (entry->offset < offset)
> + node = node->rb_right;
> + else
> + return entry;
> + }
> + return NULL;
> +}
> +
> +/*
> + * In the case that a entry with the same offset is found, it a pointer to
> + * the existing entry is stored in dupentry and the function returns -EEXIST
> +*/
> +static int zswap_rb_insert(struct rb_root *root, struct zswap_entry *entry,
> + struct zswap_entry **dupentry)
> +{
> + struct rb_node **link = &root->rb_node, *parent = NULL;
> + struct zswap_entry *myentry;
> +
> + while (*link) {
> + parent = *link;
> + myentry = rb_entry(parent, struct zswap_entry, rbnode);
> + if (myentry->offset > entry->offset)
> + link = &(*link)->rb_left;
> + else if (myentry->offset < entry->offset)
> + link = &(*link)->rb_right;
> + else {
> + *dupentry = myentry;
> + return -EEXIST;
> + }
> + }
> + rb_link_node(&entry->rbnode, parent, link);
> + rb_insert_color(&entry->rbnode, root);
> + return 0;
> +}
> +
> +/*********************************
> +* per-cpu code
> +**********************************/
> +static DEFINE_PER_CPU(u8 *, zswap_dstmem);
> +
> +static int __zswap_cpu_notifier(unsigned long action, unsigned long cpu)
> +{
> + struct crypto_comp *tfm;
> + u8 *dst;
> +
> + switch (action) {
> + case CPU_UP_PREPARE:
> + tfm = crypto_alloc_comp(zswap_compressor, 0, 0);
> + if (IS_ERR(tfm)) {
> + pr_err("can't allocate compressor transform\n");
> + return NOTIFY_BAD;
> + }
> + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = tfm;
> + dst = kmalloc(PAGE_SIZE * 2, GFP_KERNEL);
> + if (!dst) {
> + pr_err("can't allocate compressor buffer\n");
> + crypto_free_comp(tfm);
> + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> + return NOTIFY_BAD;
> + }
> + per_cpu(zswap_dstmem, cpu) = dst;
> + break;
> + case CPU_DEAD:
> + case CPU_UP_CANCELED:
> + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu);
> + if (tfm) {
> + crypto_free_comp(tfm);
> + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> + }
> + dst = per_cpu(zswap_dstmem, cpu);
> + kfree(dst);
> + per_cpu(zswap_dstmem, cpu) = NULL;
> + break;
> + default:
> + break;
> + }
> + return NOTIFY_OK;
> +}
> +
> +static int zswap_cpu_notifier(struct notifier_block *nb,
> + unsigned long action, void *pcpu)
> +{
> + unsigned long cpu = (unsigned long)pcpu;
> + return __zswap_cpu_notifier(action, cpu);
> +}
> +
> +static struct notifier_block zswap_cpu_notifier_block = {
> + .notifier_call = zswap_cpu_notifier
> +};
> +
> +static int zswap_cpu_init(void)
> +{
> + unsigned long cpu;
> +
> + get_online_cpus();
> + for_each_online_cpu(cpu)
> + if (__zswap_cpu_notifier(CPU_UP_PREPARE, cpu) != NOTIFY_OK)
> + goto cleanup;
> + register_cpu_notifier(&zswap_cpu_notifier_block);
> + put_online_cpus();
> + return 0;
> +
> +cleanup:
> + for_each_online_cpu(cpu)
> + __zswap_cpu_notifier(CPU_UP_CANCELED, cpu);
> + put_online_cpus();
> + return -ENOMEM;
> +}
> +
> +/*********************************
> +* helpers
> +**********************************/
> +static inline bool zswap_is_full(void)
> +{
> + int pool_pages = atomic_read(&zswap_pool_pages);
> + return (totalram_pages * zswap_max_pool_percent / 100 < pool_pages);
> +}
> +
> +/*
> + * Carries out the common pattern of freeing and entry's zsmalloc allocation,
> + * freeing the entry itself, and decrementing the number of stored pages.
> + */
> +static void zswap_free_entry(struct zswap_tree *tree, struct zswap_entry *entry)
> +{
> + zbud_free(tree->pool, entry->handle);
> + zswap_entry_cache_free(entry);
> + atomic_dec(&zswap_stored_pages);
> + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> +}
> +
> +/*********************************
> +* writeback code
> +**********************************/
> +/* return enum for zswap_get_swap_cache_page */
> +enum zswap_get_swap_ret {
> + ZSWAP_SWAPCACHE_NEW,
> + ZSWAP_SWAPCACHE_EXIST,
> + ZSWAP_SWAPCACHE_NOMEM
> +};
> +
> +/*
> + * zswap_get_swap_cache_page
> + *
> + * This is an adaption of read_swap_cache_async()
> + *
> + * This function tries to find a page with the given swap entry
> + * in the swapper_space address space (the swap cache). If the page
> + * is found, it is returned in retpage. Otherwise, a page is allocated,
> + * added to the swap cache, and returned in retpage.
> + *
> + * If success, the swap cache page is returned in retpage
> + * Returns 0 if page was already in the swap cache, page is not locked
> + * Returns 1 if the new page needs to be populated, page is locked
> + * Returns <0 on error
> + */
> +static int zswap_get_swap_cache_page(swp_entry_t entry,
> + struct page **retpage)
> +{
> + struct page *found_page, *new_page = NULL;
> + struct address_space *swapper_space = &swapper_spaces[swp_type(entry)];
> + int err;
> +
> + *retpage = NULL;
> + do {
> + /*
> + * First check the swap cache. Since this is normally
> + * called after lookup_swap_cache() failed, re-calling
> + * that would confuse statistics.
> + */
> + found_page = find_get_page(swapper_space, entry.val);
> + if (found_page)
> + break;
> +
> + /*
> + * Get a new page to read into from swap.
> + */
> + if (!new_page) {
> + new_page = alloc_page(GFP_KERNEL);
> + if (!new_page)
> + break; /* Out of memory */
> + }
> +
> + /*
> + * call radix_tree_preload() while we can wait.
> + */
> + err = radix_tree_preload(GFP_KERNEL);
> + if (err)
> + break;
> +
> + /*
> + * Swap entry may have been freed since our caller observed it.
> + */
> + err = swapcache_prepare(entry);
> + if (err == -EEXIST) { /* seems racy */
> + radix_tree_preload_end();
> + continue;
> + }
> + if (err) { /* swp entry is obsolete ? */
> + radix_tree_preload_end();
> + break;
> + }
> +
> + /* May fail (-ENOMEM) if radix-tree node allocation failed. */
> + __set_page_locked(new_page);
> + SetPageSwapBacked(new_page);
> + err = __add_to_swap_cache(new_page, entry);
> + if (likely(!err)) {
> + radix_tree_preload_end();
> + lru_cache_add_anon(new_page);
> + *retpage = new_page;
> + return ZSWAP_SWAPCACHE_NEW;
> + }
> + radix_tree_preload_end();
> + ClearPageSwapBacked(new_page);
> + __clear_page_locked(new_page);
> + /*
> + * add_to_swap_cache() doesn't return -EEXIST, so we can safely
> + * clear SWAP_HAS_CACHE flag.
> + */
> + swapcache_free(entry, NULL);
> + } while (err != -ENOMEM);
> +
> + if (new_page)
> + page_cache_release(new_page);
> + if (!found_page)
> + return ZSWAP_SWAPCACHE_NOMEM;
> + *retpage = found_page;
> + return ZSWAP_SWAPCACHE_EXIST;
> +}
> +
> +/*
> + * Attempts to free and entry by adding a page to the swap cache,
> + * decompressing the entry data into the page, and issuing a
> + * bio write to write the page back to the swap device.
> + *
> + * This can be thought of as a "resumed writeback" of the page
> + * to the swap device. We are basically resuming the same swap
> + * writeback path that was intercepted with the frontswap_store()
> + * in the first place. After the page has been decompressed into
> + * the swap cache, the compressed version stored by zswap can be
> + * freed.
> + */
> +static int zswap_writeback_entry(struct zbud_pool *pool, unsigned long handle)
> +{
> + struct zswap_header *zhdr;
> + swp_entry_t swpentry;
> + struct zswap_tree *tree;
> + pgoff_t offset;
> + struct zswap_entry *entry;
> + struct page *page;
> + u8 *src, *dst;
> + unsigned int dlen;
> + int ret, refcount;
> + struct writeback_control wbc = {
> + .sync_mode = WB_SYNC_NONE,
> + };
> +
> + /* extract swpentry from data */
> + zhdr = zbud_map(pool, handle);
> + swpentry = zhdr->swpentry; /* here */
> + zbud_unmap(pool, handle);
> + tree = zswap_trees[swp_type(swpentry)];
> + offset = swp_offset(swpentry);
> + BUG_ON(pool != tree->pool);
> +
> + /* find and ref zswap entry */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was invalidated */
> + spin_unlock(&tree->lock);
> + return 0;
> + }
> + zswap_entry_get(entry);
> + spin_unlock(&tree->lock);
> + BUG_ON(offset != entry->offset);
> +
> + /* try to allocate swap cache page */
> + switch (zswap_get_swap_cache_page(swpentry, &page)) {
> + case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
> + ret = -ENOMEM;
> + goto fail;
> +
> + case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
> + /* page is already in the swap cache, ignore for now */
> + page_cache_release(page);
> + ret = -EEXIST;
> + goto fail;
> +
> + case ZSWAP_SWAPCACHE_NEW: /* page is locked */
> + /* decompress */
> + dlen = PAGE_SIZE;
> + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> + sizeof(struct zswap_header);
> + dst = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
> + entry->length, dst, &dlen);
> + kunmap_atomic(dst);
> + zbud_unmap(tree->pool, entry->handle);
> + BUG_ON(ret);
> + BUG_ON(dlen != PAGE_SIZE);
> +
> + /* page is up to date */
> + SetPageUptodate(page);
> + }
> +
> + /* start writeback */
> + SetPageReclaim(page);
> + __swap_writepage(page, &wbc, end_swap_bio_write);
> + page_cache_release(page);
> + zswap_written_back_pages++;
> +
> + spin_lock(&tree->lock);
> +
> + /* drop local reference */
> + zswap_entry_put(entry);
> + /* drop the initial reference from entry creation */
> + refcount = zswap_entry_put(entry);
> +
> + /*
> + * There are three possible values for refcount here:
> + * (1) refcount is 1, load is in progress, unlink from rbtree,
> + * load will free
> + * (2) refcount is 0, (normal case) entry is valid,
> + * remove from rbtree and free entry
> + * (3) refcount is -1, invalidate happened during writeback,
> + * free entry
> + */
> + if (refcount >= 0) {
> + /* no invalidate yet, remove from rbtree */
> + rb_erase(&entry->rbnode, &tree->rbroot);
> + }
> + spin_unlock(&tree->lock);
> + if (refcount <= 0) {
> + /* free the entry */
> + zswap_free_entry(tree, entry);
> + return 0;
> + }
> + return -EAGAIN;
> +
> +fail:
> + spin_lock(&tree->lock);
> + zswap_entry_put(entry);
> + spin_unlock(&tree->lock);
> + return ret;
> +}
> +
> +/*********************************
> +* frontswap hooks
> +**********************************/
> +/* attempts to compress and store an single page */
> +static int zswap_frontswap_store(unsigned type, pgoff_t offset,
> + struct page *page)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry, *dupentry;
> + int ret;
> + unsigned int dlen = PAGE_SIZE, len;
> + unsigned long handle;
> + char *buf;
> + u8 *src, *dst;
> + struct zswap_header *zhdr;
> +
> + if (!tree) {
> + ret = -ENODEV;
> + goto reject;
> + }
> +
> + /* reclaim space if needed */
> + if (zswap_is_full()) {
> + zswap_pool_limit_hit++;
> + if (zbud_reclaim_page(tree->pool, 8)) {
> + zswap_reject_reclaim_fail++;
> + ret = -ENOMEM;
> + goto reject;
> + }
> + }

See comment above about enforcing "full".

(No further comments below... Thanks, Dan)

> + /* allocate entry */
> + entry = zswap_entry_cache_alloc(GFP_KERNEL);
> + if (!entry) {
> + zswap_reject_kmemcache_fail++;
> + ret = -ENOMEM;
> + goto reject;
> + }
> +
> + /* compress */
> + dst = get_cpu_var(zswap_dstmem);
> + src = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_COMPRESS, src, PAGE_SIZE, dst, &dlen);
> + kunmap_atomic(src);
> + if (ret) {
> + ret = -EINVAL;
> + goto freepage;
> + }
> + len = dlen + sizeof(struct zswap_header);
> + if ((len * 100 / PAGE_SIZE) > zswap_max_compression_ratio) {
> + zswap_reject_compress_poor++;
> + ret = -E2BIG;
> + goto freepage;
> + }
> +
> + /* store */
> + ret = zbud_alloc(tree->pool, len, __GFP_NORETRY | __GFP_NOWARN,
> + &handle);
> + if (ret) {
> + zswap_reject_alloc_fail++;
> + goto freepage;
> + }
> + zhdr = zbud_map(tree->pool, handle);
> + zhdr->swpentry = swp_entry(type, offset);
> + buf = (u8 *)(zhdr + 1);
> + memcpy(buf, dst, dlen);
> + zbud_unmap(tree->pool, handle);
> + put_cpu_var(zswap_dstmem);
> +
> + /* populate entry */
> + entry->offset = offset;
> + entry->handle = handle;
> + entry->length = dlen;
> +
> + /* map */
> + spin_lock(&tree->lock);
> + do {
> + ret = zswap_rb_insert(&tree->rbroot, entry, &dupentry);
> + if (ret == -EEXIST) {
> + zswap_duplicate_entry++;
> + /* remove from rbtree */
> + rb_erase(&dupentry->rbnode, &tree->rbroot);
> + if (!zswap_entry_put(dupentry)) {
> + /* free */
> + zswap_free_entry(tree, dupentry);
> + }
> + }
> + } while (ret == -EEXIST);
> + spin_unlock(&tree->lock);
> +
> + /* update stats */
> + atomic_inc(&zswap_stored_pages);
> + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> +
> + return 0;
> +
> +freepage:
> + put_cpu_var(zswap_dstmem);
> + zswap_entry_cache_free(entry);
> +reject:
> + return ret;
> +}
> +
> +/*
> + * returns 0 if the page was successfully decompressed
> + * return -1 on entry not found or error
> +*/
> +static int zswap_frontswap_load(unsigned type, pgoff_t offset,
> + struct page *page)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry;
> + u8 *src, *dst;
> + unsigned int dlen;
> + int refcount, ret;
> +
> + /* find */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was written back */
> + spin_unlock(&tree->lock);
> + return -1;
> + }
> + zswap_entry_get(entry);
> + spin_unlock(&tree->lock);
> +
> + /* decompress */
> + dlen = PAGE_SIZE;
> + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> + sizeof(struct zswap_header);
> + dst = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src, entry->length,
> + dst, &dlen);
> + kunmap_atomic(dst);
> + zbud_unmap(tree->pool, entry->handle);
> + BUG_ON(ret);
> +
> + spin_lock(&tree->lock);
> + refcount = zswap_entry_put(entry);
> + if (likely(refcount)) {
> + spin_unlock(&tree->lock);
> + return 0;
> + }
> + spin_unlock(&tree->lock);
> +
> + /*
> + * We don't have to unlink from the rbtree because
> + * zswap_writeback_entry() or zswap_frontswap_invalidate page()
> + * has already done this for us if we are the last reference.
> + */
> + /* free */
> +
> + zswap_free_entry(tree, entry);
> +
> + return 0;
> +}
> +
> +/* invalidates a single page */
> +static void zswap_frontswap_invalidate_page(unsigned type, pgoff_t offset)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry;
> + int refcount;
> +
> + /* find */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was written back */
> + spin_unlock(&tree->lock);
> + return;
> + }
> +
> + /* remove from rbtree */
> + rb_erase(&entry->rbnode, &tree->rbroot);
> +
> + /* drop the initial reference from entry creation */
> + refcount = zswap_entry_put(entry);
> +
> + spin_unlock(&tree->lock);
> +
> + if (refcount) {
> + /* writeback in progress, writeback will free */
> + return;
> + }
> +
> + /* free */
> + zswap_free_entry(tree, entry);
> +}
> +
> +/* invalidates all pages for the given swap type */
> +static void zswap_frontswap_invalidate_area(unsigned type)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct rb_node *node;
> + struct zswap_entry *entry;
> +
> + if (!tree)
> + return;
> +
> + /* walk the tree and free everything */
> + spin_lock(&tree->lock);
> + /*
> + * TODO: Even though this code should not be executed because
> + * the try_to_unuse() in swapoff should have emptied the tree,
> + * it is very wasteful to rebalance the tree after every
> + * removal when we are freeing the whole tree.
> + *
> + * If post-order traversal code is ever added to the rbtree
> + * implementation, it should be used here.
> + */
> + while ((node = rb_first(&tree->rbroot))) {
> + entry = rb_entry(node, struct zswap_entry, rbnode);
> + rb_erase(&entry->rbnode, &tree->rbroot);
> + zbud_free(tree->pool, entry->handle);
> + zswap_entry_cache_free(entry);
> + atomic_dec(&zswap_stored_pages);
> + }
> + tree->rbroot = RB_ROOT;
> + spin_unlock(&tree->lock);
> +}
> +
> +static struct zbud_ops zswap_zbud_ops = {
> + .evict = zswap_writeback_entry
> +};
> +
> +/* NOTE: this is called in atomic context from swapon and must not sleep */
> +static void zswap_frontswap_init(unsigned type)
> +{
> + struct zswap_tree *tree;
> +
> + tree = kzalloc(sizeof(struct zswap_tree), GFP_ATOMIC);
> + if (!tree)
> + goto err;
> + tree->pool = zbud_create_pool(GFP_NOWAIT, &zswap_zbud_ops);
> + if (!tree->pool)
> + goto freetree;
> + tree->rbroot = RB_ROOT;
> + spin_lock_init(&tree->lock);
> + tree->type = type;
> + zswap_trees[type] = tree;
> + return;
> +
> +freetree:
> + kfree(tree);
> +err:
> + pr_err("alloc failed, zswap disabled for swap type %d\n", type);
> +}
> +
> +static struct frontswap_ops zswap_frontswap_ops = {
> + .store = zswap_frontswap_store,
> + .load = zswap_frontswap_load,
> + .invalidate_page = zswap_frontswap_invalidate_page,
> + .invalidate_area = zswap_frontswap_invalidate_area,
> + .init = zswap_frontswap_init
> +};
> +
> +/*********************************
> +* debugfs functions
> +**********************************/
> +#ifdef CONFIG_DEBUG_FS
> +#include <linux/debugfs.h>
> +
> +static struct dentry *zswap_debugfs_root;
> +
> +static int __init zswap_debugfs_init(void)
> +{
> + if (!debugfs_initialized())
> + return -ENODEV;
> +
> + zswap_debugfs_root = debugfs_create_dir("zswap", NULL);
> + if (!zswap_debugfs_root)
> + return -ENOMEM;
> +
> + debugfs_create_u64("pool_limit_hit", S_IRUGO,
> + zswap_debugfs_root, &zswap_pool_limit_hit);
> + debugfs_create_u64("reject_reclaim_fail", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_reclaim_fail);
> + debugfs_create_u64("reject_alloc_fail", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_alloc_fail);
> + debugfs_create_u64("reject_kmemcache_fail", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_kmemcache_fail);
> + debugfs_create_u64("reject_compress_poor", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_compress_poor);
> + debugfs_create_u64("written_back_pages", S_IRUGO,
> + zswap_debugfs_root, &zswap_written_back_pages);
> + debugfs_create_u64("duplicate_entry", S_IRUGO,
> + zswap_debugfs_root, &zswap_duplicate_entry);
> + debugfs_create_atomic_t("pool_pages", S_IRUGO,
> + zswap_debugfs_root, &zswap_pool_pages);
> + debugfs_create_atomic_t("stored_pages", S_IRUGO,
> + zswap_debugfs_root, &zswap_stored_pages);
> +
> + return 0;
> +}
> +
> +static void __exit zswap_debugfs_exit(void)
> +{
> + debugfs_remove_recursive(zswap_debugfs_root);
> +}
> +#else
> +static inline int __init zswap_debugfs_init(void)
> +{
> + return 0;
> +}
> +
> +static inline void __exit zswap_debugfs_exit(void) { }
> +#endif
> +
> +/*********************************
> +* module init and exit
> +**********************************/
> +static int __init init_zswap(void)
> +{
> + if (!zswap_enabled)
> + return 0;
> +
> + pr_info("loading zswap\n");
> + if (zswap_entry_cache_create()) {
> + pr_err("entry cache creation failed\n");
> + goto error;
> + }
> + if (zswap_comp_init()) {
> + pr_err("compressor initialization failed\n");
> + goto compfail;
> + }
> + if (zswap_cpu_init()) {
> + pr_err("per-cpu initialization failed\n");
> + goto pcpufail;
> + }
> + frontswap_register_ops(&zswap_frontswap_ops);
> + if (zswap_debugfs_init())
> + pr_warn("debugfs initialization failed\n");
> + return 0;
> +pcpufail:
> + zswap_comp_exit();
> +compfail:
> + zswap_entry_cache_destory();
> +error:
> + return -ENOMEM;
> +}
> +/* must be late so crypto has time to come up */
> +late_initcall(init_zswap);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> +MODULE_DESCRIPTION("Compressed cache for swap pages");
> --
> 1.7.9.5

2013-05-14 08:48:44

by Bob Liu

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

Hi Seth,

On 05/13/2013 08:40 PM, Seth Jennings wrote:
> zbud is an special purpose allocator for storing compressed pages. It is
> designed to store up to two compressed pages per physical page. While this
> design limits storage density, it has simple and deterministic reclaim
> properties that make it preferable to a higher density approach when reclaim
> will be used.
>
> zbud works by storing compressed pages, or "zpages", together in pairs in a
> single memory page called a "zbud page". The first buddy is "left
> justifed" at the beginning of the zbud page, and the last buddy is "right
> justified" at the end of the zbud page. The benefit is that if either
> buddy is freed, the freed buddy space, coalesced with whatever slack space
> that existed between the buddies, results in the largest possible free region
> within the zbud page.
>
> zbud also provides an attractive lower bound on density. The ratio of zpages
> to zbud pages can not be less than 1. This ensures that zbud can never "do
> harm" by using more pages to store zpages than the uncompressed zpages would
> have used on their own.
>
> This patch adds zbud to mm/ for later use by zswap.
>
> Signed-off-by: Seth Jennings <[email protected]>
> ---

Good job! And I'm testing it!

> include/linux/zbud.h | 22 ++
> mm/Kconfig | 10 +
> mm/Makefile | 1 +
> mm/zbud.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 597 insertions(+)
> create mode 100644 include/linux/zbud.h
> create mode 100644 mm/zbud.c
>
> diff --git a/include/linux/zbud.h b/include/linux/zbud.h
> new file mode 100644
> index 0000000..954252b
> --- /dev/null
> +++ b/include/linux/zbud.h
> @@ -0,0 +1,22 @@
> +#ifndef _ZBUD_H_
> +#define _ZBUD_H_
> +
> +#include <linux/types.h>
> +
> +struct zbud_pool;
> +
> +struct zbud_ops {
> + int (*evict)(struct zbud_pool *pool, unsigned long handle);
> +};
> +
> +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops);
> +void zbud_destroy_pool(struct zbud_pool *pool);
> +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> + unsigned long *handle);
> +void zbud_free(struct zbud_pool *pool, unsigned long handle);
> +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries);
> +void *zbud_map(struct zbud_pool *pool, unsigned long handle);
> +void zbud_unmap(struct zbud_pool *pool, unsigned long handle);
> +int zbud_get_pool_size(struct zbud_pool *pool);
> +
> +#endif /* _ZBUD_H_ */
> diff --git a/mm/Kconfig b/mm/Kconfig
> index e742d06..908f41b 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -477,3 +477,13 @@ config FRONTSWAP
> and swap data is stored as normal on the matching swap device.
>
> If unsure, say Y to enable frontswap.
> +
> +config ZBUD
> + tristate "Buddy allocator for compressed pages"
> + default n
> + help
> + zbud is an special purpose allocator for storing compressed pages.
> + It is designed to store up to two compressed pages per physical page.
> + While this design limits storage density, it has simple and
> + deterministic reclaim properties that make it preferable to a higher
> + density approach when reclaim will be used.
> diff --git a/mm/Makefile b/mm/Makefile
> index 72c5acb..95f0197 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -58,3 +58,4 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
> obj-$(CONFIG_DEBUG_KMEMLEAK_TEST) += kmemleak-test.o
> obj-$(CONFIG_CLEANCACHE) += cleancache.o
> obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
> +obj-$(CONFIG_ZBUD) += zbud.o
> diff --git a/mm/zbud.c b/mm/zbud.c
> new file mode 100644
> index 0000000..e5bd0e6
> --- /dev/null
> +++ b/mm/zbud.c
> @@ -0,0 +1,564 @@
> +/*
> + * zbud.c - Buddy Allocator for Compressed Pages
> + *
> + * Copyright (C) 2013, Seth Jennings, IBM
> + *
> + * Concepts based on zcache internal zbud allocator by Dan Magenheimer.
> + *
> + * zbud is an special purpose allocator for storing compressed pages. It is
> + * designed to store up to two compressed pages per physical page. While this
> + * design limits storage density, it has simple and deterministic reclaim
> + * properties that make it preferable to a higher density approach when reclaim
> + * will be used.
> + *
> + * zbud works by storing compressed pages, or "zpages", together in pairs in a
> + * single memory page called a "zbud page". The first buddy is "left
> + * justifed" at the beginning of the zbud page, and the last buddy is "right
> + * justified" at the end of the zbud page. The benefit is that if either
> + * buddy is freed, the freed buddy space, coalesced with whatever slack space
> + * that existed between the buddies, results in the largest possible free region
> + * within the zbud page.
> + *
> + * zbud also provides an attractive lower bound on density. The ratio of zpages
> + * to zbud pages can not be less than 1. This ensures that zbud can never "do
> + * harm" by using more pages to store zpages than the uncompressed zpages would
> + * have used on their own.
> + *
> + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> + * into chunks allows organizing unbuddied zbud pages into a manageable number
> + * of unbuddied lists according to the number of free chunks available in the
> + * zbud page.
> + *
> + * The zbud API differs from that of conventional allocators in that the
> + * allocation function, zbud_alloc(), returns an opaque handle to the user,
> + * not a dereferenceable pointer. The user must map the handle using
> + * zbud_map() in order to get a usable pointer by which to access the
> + * allocation data and unmap the handle with zbud_unmap() when operations
> + * on the allocation data are complete.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/atomic.h>
> +#include <linux/list.h>
> +#include <linux/mm.h>
> +#include <linux/module.h>
> +#include <linux/preempt.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/zbud.h>
> +
> +/*****************
> + * Structures
> +*****************/
> +/**
> + * struct zbud_page - zbud page metadata overlay
> + * @page: typed reference to the underlying struct page
> + * @donotuse: this overlays the page flags and should not be used
> + * @first_chunks: the size of the first buddy in chunks, 0 if free

Nitpick, the name here seems not directly to me.
But I don't have a better idea yet.
Maybe first_buddy_size/first_buddy_nrchunks or buddy0_size.

> + * @last_chunks: the size of the last buddy in chunks, 0 if free
> + * @buddy: links the zbud page into the unbuddied/buddied lists in the pool
> + * @lru: links the zbud page into the lru list in the pool
> + *
> + * This structure overlays the struct page to store metadata needed for a
> + * single storage page in for zbud. There is a BUILD_BUG_ON in zbud_init()
> + * that ensures this structure is not larger that struct page.
> + *
> + * The PG_reclaim flag of the underlying page is used for indicating
> + * that this zbud page is under reclaim (see zbud_reclaim_page())
> + */
> +struct zbud_page {
> + union {
> + struct page page;
> + struct {
> + unsigned long donotuse;
> + u16 first_chunks;
> + u16 last_chunks;
> + struct list_head buddy;
> + struct list_head lru;
> + };
> + };
> +};
> +
> +/*
> + * NCHUNKS_ORDER determines the internal allocation granularity, effectively
> + * adjusting internal fragmentation. It also determines the number of
> + * freelists maintained in each pool. NCHUNKS_ORDER of 6 means that the
> + * allocation granularity will be in chunks of size PAGE_SIZE/64, and there
> + * will be 64 freelists per pool.
> + */
> +#define NCHUNKS_ORDER 6
> +
> +#define CHUNK_SHIFT (PAGE_SHIFT - NCHUNKS_ORDER)
> +#define CHUNK_SIZE (1 << CHUNK_SHIFT)
> +#define NCHUNKS (PAGE_SIZE >> CHUNK_SHIFT)
> +
> +/**
> + * struct zbud_pool - stores metadata for each zbud pool
> + * @lock: protects all pool lists and first|last_chunk fields of any
> + * zbud page in the pool
> + * @unbuddied: array of lists tracking zbud pages that only contain one buddy;
> + * the lists each zbud page is added to depends on the size of
> + * its free region.
> + * @buddied: list tracking the zbud pages that contain two buddies;
> + * these zbud pages are full

Lack of list_head lru.

> + * @pages_nr: number of zbud pages in the pool.
> + * @ops: pointer to a structure of user defined operations specified at
> + * pool creation time.
> + *
> + * This structure is allocated at pool creation time and maintains metadata
> + * pertaining to a particular zbud pool.
> + */
> +struct zbud_pool {
> + spinlock_t lock;
> + struct list_head unbuddied[NCHUNKS];
> + struct list_head buddied;
> + struct list_head lru;
> + atomic_t pages_nr;
> + struct zbud_ops *ops;
> +};
> +
> +/*****************
> + * Helpers
> +*****************/
> +/* Just to make the code easier to read */
> +enum buddy {
> + FIRST,
> + LAST
> +};
> +
> +/* Converts an allocation size in bytes to size in zbud chunks */
> +static inline int size_to_chunks(int size)
> +{
> + return (size + CHUNK_SIZE - 1) >> CHUNK_SHIFT;
> +}
> +
> +#define for_each_unbuddied_list(_iter, _begin) \
> + for ((_iter) = (_begin); (_iter) < NCHUNKS; (_iter)++)
> +
> +/* Initializes a zbud page from a newly allocated page */
> +static inline struct zbud_page *init_zbud_page(struct page *page)
> +{
> + struct zbud_page *zbpage = (struct zbud_page *)page;
> + zbpage->first_chunks = 0;
> + zbpage->last_chunks = 0;
> + INIT_LIST_HEAD(&zbpage->buddy);
> + INIT_LIST_HEAD(&zbpage->lru);
> + return zbpage;
> +}
> +
> +/* Resets a zbud page so that it can be properly freed */

Better with comment: the caller must hold the pool->lock?

> +static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
> +{
> + struct page *page = &zbpage->page;
> + set_page_private(page, 0);
> + page->mapping = NULL;
> + page->index = 0;
> + page_mapcount_reset(page);
> + init_page_count(page);
> + INIT_LIST_HEAD(&page->lru);
> + return page;
> +}
> +
> +/*
> + * Encodes the handle of a particular buddy within a zbud page
> + * Pool lock should be held as this function accesses first|last_chunks
> + */
> +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> + enum buddy bud)
> +{
> + unsigned long handle;
> +
> + /*
> + * For now, the encoded handle is actually just the pointer to the data
> + * but this might not always be the case. A little information hiding.
> + */
> + handle = (unsigned long)page_address(&zbpage->page);
> + if (bud == FIRST)
> + return handle;
> + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> + return handle;
> +}
> +
> +/* Returns the zbud page where a given handle is stored */
> +static inline struct zbud_page *handle_to_zbud_page(unsigned long handle)
> +{
> + return (struct zbud_page *)(virt_to_page(handle));
> +}
> +
> +/* Returns the number of free chunks in a zbud page */
> +static inline int num_free_chunks(struct zbud_page *zbpage)
> +{
> + /*
> + * Rather than branch for different situations, just use the fact that
> + * free buddies have a length of zero to simplify everything.
> + */
> + return NCHUNKS - zbpage->first_chunks - zbpage->last_chunks;
> +}
> +
> +/*****************
> + * API Functions
> +*****************/
> +/**
> + * zbud_create_pool() - create a new zbud pool
> + * @gfp: gfp flags when allocating the zbud pool structure
> + * @ops: user-defined operations for the zbud pool
> + *
> + * Return: pointer to the new zbud pool or NULL if the metadata allocation
> + * failed.
> + */
> +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
> +{
> + struct zbud_pool *pool;
> + int i;
> +
> + pool = kmalloc(sizeof(struct zbud_pool), gfp);
> + if (!pool)
> + return NULL;
> + spin_lock_init(&pool->lock);
> + for_each_unbuddied_list(i, 0)
> + INIT_LIST_HEAD(&pool->unbuddied[i]);
> + INIT_LIST_HEAD(&pool->buddied);
> + INIT_LIST_HEAD(&pool->lru);
> + atomic_set(&pool->pages_nr, 0);
> + pool->ops = ops;
> + return pool;
> +}
> +EXPORT_SYMBOL_GPL(zbud_create_pool);
> +
> +/**
> + * zbud_destroy_pool() - destroys an existing zbud pool
> + * @pool: the zbud pool to be destroyed
> + */
> +void zbud_destroy_pool(struct zbud_pool *pool)
> +{
> + kfree(pool);

Pages in zbud pool should also be freed here? or if they are freed
before call this function some check may be needed.
But there isn't a problem currently since no actual user.

> +}
> +EXPORT_SYMBOL_GPL(zbud_destroy_pool);
> +
> +/**
> + * zbud_alloc() - allocates a region of a given size
> + * @pool: zbud pool from which to allocate
> + * @size: size in bytes of the desired allocation
> + * @gfp: gfp flags used if the pool needs to grow
> + * @handle: handle of the new allocation
> + *
> + * This function will attempt to find a free region in the pool large
> + * enough to satisfy the allocation request. First, it tries to use
> + * free space in the most recently used zbud page, at the beginning of
> + * the pool LRU list. If that zbud page is full or doesn't have the
> + * required free space, a best fit search of the unbuddied lists is
> + * performed. If no suitable free region is found, then a new page
> + * is allocated and added to the pool to satisfy the request.
> + *
> + * gfp should not set __GFP_HIGHMEM as highmem pages cannot be used
> + * as zbud pool pages.
> + *
> + * Return: 0 if success and handle is set, otherwise -EINVAL is the size or
> + * gfp arguments are invalid or -ENOMEM if the pool was unable to allocate
> + * a new page.
> + */
> +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> + unsigned long *handle)
> +{
> + int chunks, i, freechunks;
> + struct zbud_page *zbpage = NULL;
> + enum buddy bud;
> + struct page *page;
> +
> + if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
> + return -EINVAL;
> + chunks = size_to_chunks(size);
> + spin_lock(&pool->lock);
> +
> + /*
> + * First, try to use the zbpage we last used (at the head of the
> + * LRU) to increase LRU locality of the buddies. This is first fit.
> + */
> + if (!list_empty(&pool->lru)) {
> + zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
> + if (num_free_chunks(zbpage) >= chunks) {
> + if (zbpage->first_chunks == 0) {
> + list_del(&zbpage->buddy);
> + bud = FIRST;
> + goto found;
> + }
> + if (zbpage->last_chunks == 0) {
> + list_del(&zbpage->buddy);
> + bud = LAST;
> + goto found;
> + }
> + }
> + }

I'd prefer to drop above lines to keep things simple since no way to
prove the benefit of it.

> +
> + /* Second, try to find an unbuddied zbpage. This is best fit. */
> + zbpage = NULL;
> + for_each_unbuddied_list(i, chunks) {
> + if (!list_empty(&pool->unbuddied[i])) {
> + zbpage = list_first_entry(&pool->unbuddied[i],
> + struct zbud_page, buddy);
> + list_del(&zbpage->buddy);
> + if (zbpage->first_chunks == 0)
> + bud = FIRST;
> + else
> + bud = LAST;
> + goto found;
> + }
> + }
> +
> + /* Lastly, couldn't find unbuddied zbpage, create new one */
> + spin_unlock(&pool->lock);
> + page = alloc_page(gfp);
> + if (!page)
> + return -ENOMEM;
> + spin_lock(&pool->lock);
> + atomic_inc(&pool->pages_nr);
> + zbpage = init_zbud_page(page);
> + bud = FIRST;
> +
> +found:
> + if (bud == FIRST)
> + zbpage->first_chunks = chunks;
> + else
> + zbpage->last_chunks = chunks;
> +
> + if (zbpage->first_chunks == 0 || zbpage->last_chunks == 0) {
> + /* Add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + } else {
> + /* Add to buddied list */
> + list_add(&zbpage->buddy, &pool->buddied);
> + }
> +
> + /* Add/move zbpage to beginning of LRU */
> + if (!list_empty(&zbpage->lru))
> + list_del(&zbpage->lru);
> + list_add(&zbpage->lru, &pool->lru);
> +
> + *handle = encode_handle(zbpage, bud);
> + spin_unlock(&pool->lock);
> +
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(zbud_alloc);
> +
> +/**
> + * zbud_free() - frees the allocation associated with the given handle
> + * @pool: pool in which the allocation resided
> + * @handle: handle associated with the allocation returned by zbud_alloc()
> + *
> + * In the case that the zbud page in which the allocation resides is under
> + * reclaim, as indicated by the PG_reclaim flag being set, this function
> + * only sets the first|last_chunks to 0. The page is actually freed
> + * once both buddies are evicted (see zbud_reclaim_page() below).
> + */
> +void zbud_free(struct zbud_pool *pool, unsigned long handle)
> +{
> + struct zbud_page *zbpage;
> + int freechunks;
> +
> + spin_lock(&pool->lock);
> + zbpage = handle_to_zbud_page(handle);
> +
> + /* If first buddy, handle will be page aligned */
> + if (handle & ~PAGE_MASK)
> + zbpage->last_chunks = 0;
> + else
> + zbpage->first_chunks = 0;
> +
> + if (PageReclaim(&zbpage->page)) {
> + /* zbpage is under reclaim, reclaim will free */
> + spin_unlock(&pool->lock);
> + return;
> + }
> +
> + /* Remove from existing buddy list */
> + list_del(&zbpage->buddy);
> +
> + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> + /* zbpage is empty, free */
> + list_del(&zbpage->lru);
> + __free_page(reset_zbud_page(zbpage));
> + atomic_dec(&pool->pages_nr);
> + } else {
> + /* Add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + }
> +
> + spin_unlock(&pool->lock);
> +}
> +EXPORT_SYMBOL_GPL(zbud_free);
> +
> +#define list_tail_entry(ptr, type, member) \
> + list_entry((ptr)->prev, type, member)
> +
> +/**
> + * zbud_reclaim_page() - evicts allocations from a pool page and frees it
> + * @pool: pool from which a page will attempt to be evicted
> + * @retires: number of pages on the LRU list for which eviction will
> + * be attempted before failing
> + *
> + * zbud reclaim is different from normal system reclaim in that the reclaim is
> + * done from the bottom, up. This is because only the bottom layer, zbud, has
> + * information on how the allocations are organized within each zbud page. This
> + * has the potential to create interesting locking situations between zbud and
> + * the user, however.
> + *
> + * To avoid these, this is how zbud_reclaim_page() should be called:
> +
> + * The user detects a page should be reclaimed and calls zbud_reclaim_page().
> + * zbud_reclaim_page() will remove a zbud page from the pool LRU list and call
> + * the user-defined eviction handler with the pool and handle as arguments.
> + *
> + * If the handle can not be evicted, the eviction handler should return
> + * non-zero. zbud_reclaim_page() will add the zbud page back to the
> + * appropriate list and try the next zbud page on the LRU up to
> + * a user defined number of retries.
> + *
> + * If the handle is successfully evicted, the eviction handler should
> + * return 0 _and_ should have called zbud_free() on the handle. zbud_free()
> + * contains logic to delay freeing the page if the page is under reclaim,
> + * as indicated by the setting of the PG_reclaim flag on the underlying page.
> + *
> + * If all buddies in the zbud page are successfully evicted, then the
> + * zbud page can be freed.
> + *
> + * Returns: 0 if page is successfully freed, otherwise -EINVAL if there are
> + * no pages to evict or an eviction handler is not registered, -EAGAIN if
> + * the retry limit was hit.
> + */
> +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries)
> +{
> + int i, ret, freechunks;
> + struct zbud_page *zbpage;
> + unsigned long first_handle = 0, last_handle = 0;
> +
> + spin_lock(&pool->lock);
> + if (!pool->ops || !pool->ops->evict || list_empty(&pool->lru) ||
> + retries == 0) {
> + spin_unlock(&pool->lock);
> + return -EINVAL;
> + }
> + for (i = 0; i < retries; i++) {
> + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> + list_del(&zbpage->lru);
> + list_del(&zbpage->buddy);
> + /* Protect zbpage against free */
> + SetPageReclaim(&zbpage->page);
> + /*
> + * We need encode the handles before unlocking, since we can
> + * race with free that will set (first|last)_chunks to 0
> + */
> + first_handle = 0;
> + last_handle = 0;
> + if (zbpage->first_chunks)
> + first_handle = encode_handle(zbpage, FIRST);
> + if (zbpage->last_chunks)
> + last_handle = encode_handle(zbpage, LAST);
> + spin_unlock(&pool->lock);
> +
> + /* Issue the eviction callback(s) */
> + if (first_handle) {
> + ret = pool->ops->evict(pool, first_handle);
> + if (ret)
> + goto next;
> + }
> + if (last_handle) {
> + ret = pool->ops->evict(pool, last_handle);
> + if (ret)
> + goto next;

Will go to next anyway!

> + }
> +next:
> + spin_lock(&pool->lock);
> + ClearPageReclaim(&zbpage->page);
> + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> + /*
> + * Both buddies are now free, free the zbpage and
> + * return success.
> + */
> + __free_page(reset_zbud_page(zbpage));
> + atomic_dec(&pool->pages_nr);
> + spin_unlock(&pool->lock);
> + return 0;
> + } else if (zbpage->first_chunks == 0 ||
> + zbpage->last_chunks == 0) {
> + /* add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + } else {
> + /* add to buddied list */
> + list_add(&zbpage->buddy, &pool->buddied);
> + }
> +
> + /* add to beginning of LRU */
> + list_add(&zbpage->lru, &pool->lru);
> + }
> + spin_unlock(&pool->lock);
> + return -EAGAIN;
> +}
> +EXPORT_SYMBOL_GPL(zbud_reclaim_page);
> +
> +/**
> + * zbud_map() - maps the allocation associated with the given handle
> + * @pool: pool in which the allocation resides
> + * @handle: handle associated with the allocation to be mapped
> + *
> + * While trivial for zbud, the mapping functions for others allocators
> + * implementing this allocation API could have more complex information encoded
> + * in the handle and could create temporary mappings to make the data
> + * accessible to the user.
> + *
> + * Returns: a pointer to the mapped allocation
> + */
> +void *zbud_map(struct zbud_pool *pool, unsigned long handle)
> +{
> + return (void *)(handle);
> +}
> +EXPORT_SYMBOL_GPL(zbud_map);
> +
> +/**
> + * zbud_unmap() - maps the allocation associated with the given handle
> + * @pool: pool in which the allocation resides
> + * @handle: handle associated with the allocation to be unmapped
> + */
> +void zbud_unmap(struct zbud_pool *pool, unsigned long handle)
> +{
> +}
> +EXPORT_SYMBOL_GPL(zbud_unmap);
> +
> +/**
> + * zbud_get_pool_size() - gets the zbud pool size in pages
> + * @pool: pool whose size is being queried
> + *
> + * Returns: size in pages of the given pool
> + */
> +int zbud_get_pool_size(struct zbud_pool *pool)
> +{
> + return atomic_read(&pool->pages_nr);

Should hold pool->lock?
I saw some other place dec/inc pool->pages_nr with holding pool->lock.

> +}
> +EXPORT_SYMBOL_GPL(zbud_get_pool_size);
> +
> +static int __init init_zbud(void)
> +{
> + /* Make sure we aren't overflowing the underlying struct page */
> + BUILD_BUG_ON(sizeof(struct zbud_page) != sizeof(struct page));
> + /* Make sure we can represent any chunk offset with a u16 */
> + BUILD_BUG_ON(sizeof(u16) * BITS_PER_BYTE < PAGE_SHIFT - CHUNK_SHIFT);
> + pr_info("loaded\n");
> + return 0;
> +}
> +
> +static void __exit exit_zbud(void)
> +{
> + pr_info("unloaded\n");
> +}
> +
> +module_init(init_zbud);
> +module_exit(exit_zbud);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> +MODULE_DESCRIPTION("Buddy Allocator for Compressed Pages");
>

--
Regards,
-Bob

2013-05-14 09:21:18

by Bob Liu

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

Hi Seth,

On 05/13/2013 08:40 PM, Seth Jennings wrote:
> zswap is a thin compression backend for frontswap. It receives pages from
> frontswap and attempts to store them in a compressed memory pool, resulting in
> an effective partial memory reclaim and dramatically reduced swap device I/O.
>
> Additionally, in most cases, pages can be retrieved from this compressed store
> much more quickly than reading from tradition swap devices resulting in faster
> performance for many workloads.
>
> It also has support for evicting swap pages that are currently compressed in
> zswap to the swap device on an LRU(ish) basis. This functionality is very
> important and make zswap a true cache in that, once the cache is full or can't
> grow due to memory pressure, the oldest pages can be moved out of zswap to the
> swap device so newer pages can be compressed and stored in zswap.
>
> This patch adds the zswap driver to mm/
>
> Signed-off-by: Seth Jennings <[email protected]>

It seems that you didn't address some comments from Mel in
[PATCHv9 4/8] zswap: add to mm/

> ---
> mm/Kconfig | 15 +
> mm/Makefile | 1 +
> mm/zswap.c | 952 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 968 insertions(+)
> create mode 100644 mm/zswap.c
>
> diff --git a/mm/Kconfig b/mm/Kconfig
> index 908f41b..4042e07 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -487,3 +487,18 @@ config ZBUD
> While this design limits storage density, it has simple and
> deterministic reclaim properties that make it preferable to a higher
> density approach when reclaim will be used.
> +
> +config ZSWAP
> + bool "In-kernel swap page compression"
> + depends on FRONTSWAP && CRYPTO
> + select CRYPTO_LZO
> + select ZBUD
> + default n
> + help
> + Zswap is a backend for the frontswap mechanism in the VMM.
> + It receives pages from frontswap and attempts to store them
> + in a compressed memory pool, resulting in an effective
> + partial memory reclaim. In addition, pages and be retrieved
> + from this compressed store much faster than most tradition
> + swap devices resulting in reduced I/O and faster performance
> + for many workloads.
> diff --git a/mm/Makefile b/mm/Makefile
> index 95f0197..f008033 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -32,6 +32,7 @@ obj-$(CONFIG_HAVE_MEMBLOCK) += memblock.o
> obj-$(CONFIG_BOUNCE) += bounce.o
> obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
> obj-$(CONFIG_FRONTSWAP) += frontswap.o
> +obj-$(CONFIG_ZSWAP) += zswap.o
> obj-$(CONFIG_HAS_DMA) += dmapool.o
> obj-$(CONFIG_HUGETLBFS) += hugetlb.o
> obj-$(CONFIG_NUMA) += mempolicy.o
> diff --git a/mm/zswap.c b/mm/zswap.c
> new file mode 100644
> index 0000000..b1070ca
> --- /dev/null
> +++ b/mm/zswap.c
> @@ -0,0 +1,952 @@
> +/*
> + * zswap.c - zswap driver file
> + *
> + * zswap is a backend for frontswap that takes pages that are in the
> + * process of being swapped out and attempts to compress them and store
> + * them in a RAM-based memory pool. This results in a significant I/O
> + * reduction on the real swap device and, in the case of a slow swap
> + * device, can also improve workload performance.
> + *
> + * Copyright (C) 2012 Seth Jennings <[email protected]>
> + *
> + * This program is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU General Public License
> + * as published by the Free Software Foundation; either version 2
> + * of the License, or (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> +*/
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/module.h>
> +#include <linux/cpu.h>
> +#include <linux/highmem.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/types.h>
> +#include <linux/atomic.h>
> +#include <linux/frontswap.h>
> +#include <linux/rbtree.h>
> +#include <linux/swap.h>
> +#include <linux/crypto.h>
> +#include <linux/mempool.h>
> +#include <linux/zbud.h>
> +
> +#include <linux/mm_types.h>
> +#include <linux/page-flags.h>
> +#include <linux/swapops.h>
> +#include <linux/writeback.h>
> +#include <linux/pagemap.h>
> +
> +/*********************************
> +* statistics
> +**********************************/
> +/* Number of memory pages used by the compressed pool */
> +static atomic_t zswap_pool_pages = ATOMIC_INIT(0);
> +/* The number of compressed pages currently stored in zswap */
> +static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
> +
> +/*
> + * The statistics below are not protected from concurrent access for
> + * performance reasons so they may not be a 100% accurate. However,
> + * they do provide useful information on roughly how many times a
> + * certain event is occurring.
> +*/
> +static u64 zswap_pool_limit_hit;
> +static u64 zswap_written_back_pages;
> +static u64 zswap_reject_reclaim_fail;
> +static u64 zswap_reject_compress_poor;
> +static u64 zswap_reject_alloc_fail;
> +static u64 zswap_reject_kmemcache_fail;
> +static u64 zswap_duplicate_entry;
> +
> +/*********************************
> +* tunables
> +**********************************/
> +/* Enable/disable zswap (disabled by default, fixed at boot for now) */
> +static bool zswap_enabled;
> +module_param_named(enabled, zswap_enabled, bool, 0);
> +
> +/* Compressor to be used by zswap (fixed at boot for now) */
> +#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
> +static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> +module_param_named(compressor, zswap_compressor, charp, 0);
> +
> +/* The maximum percentage of memory that the compressed pool can occupy */
> +static unsigned int zswap_max_pool_percent = 20;
> +module_param_named(max_pool_percent,
> + zswap_max_pool_percent, uint, 0644);
> +

I think it's reasonable but... see comments in zbud_reclaim_page().

> +/*
> + * Maximum compression ratio, as as percentage, for an acceptable
> + * compressed page. Any pages that do not compress by at least
> + * this ratio will be rejected.
> +*/
> +static unsigned int zswap_max_compression_ratio = 80;
> +module_param_named(max_compression_ratio,
> + zswap_max_compression_ratio, uint, 0644);
> +

Prefer not export it, it's hard for use to know what value should set to.

> +/*********************************
> +* compression functions
> +**********************************/
> +/* per-cpu compression transforms */
> +static struct crypto_comp * __percpu *zswap_comp_pcpu_tfms;
> +
> +enum comp_op {
> + ZSWAP_COMPOP_COMPRESS,
> + ZSWAP_COMPOP_DECOMPRESS
> +};
> +
> +static int zswap_comp_op(enum comp_op op, const u8 *src, unsigned int slen,
> + u8 *dst, unsigned int *dlen)
> +{
> + struct crypto_comp *tfm;
> + int ret;
> +
> + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, get_cpu());
> + switch (op) {
> + case ZSWAP_COMPOP_COMPRESS:
> + ret = crypto_comp_compress(tfm, src, slen, dst, dlen);
> + break;
> + case ZSWAP_COMPOP_DECOMPRESS:
> + ret = crypto_comp_decompress(tfm, src, slen, dst, dlen);
> + break;
> + default:
> + ret = -EINVAL;
> + }
> +
> + put_cpu();
> + return ret;
> +}
> +
> +static int __init zswap_comp_init(void)
> +{
> + if (!crypto_has_comp(zswap_compressor, 0, 0)) {
> + pr_info("%s compressor not available\n", zswap_compressor);
> + /* fall back to default compressor */
> + zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> + if (!crypto_has_comp(zswap_compressor, 0, 0))
> + /* can't even load the default compressor */
> + return -ENODEV;
> + }
> + pr_info("using %s compressor\n", zswap_compressor);
> +
> + /* alloc percpu transforms */
> + zswap_comp_pcpu_tfms = alloc_percpu(struct crypto_comp *);
> + if (!zswap_comp_pcpu_tfms)
> + return -ENOMEM;
> + return 0;
> +}
> +
> +static void zswap_comp_exit(void)
> +{
> + /* free percpu transforms */
> + if (zswap_comp_pcpu_tfms)
> + free_percpu(zswap_comp_pcpu_tfms);
> +}
> +
> +/*********************************
> +* data structures
> +**********************************/
> +/*
> + * struct zswap_entry
> + *
> + * This structure contains the metadata for tracking a single compressed
> + * page within zswap.
> + *
> + * rbnode - links the entry into red-black tree for the appropriate swap type
> + * refcount - the number of outstanding reference to the entry. This is needed
> + * to protect against premature freeing of the entry by code
> + * concurent calls to load, invalidate, and writeback. The lock
> + * for the zswap_tree structure that contains the entry must
> + * be held while changing the refcount. Since the lock must
> + * be held, there is no reason to also make refcount atomic.
> + * type - the swap type for the entry. Used to map back to the zswap_tree
> + * structure that contains the entry.
> + * offset - the swap offset for the entry. Index into the red-black tree.
> + * handle - zsmalloc allocation handle that stores the compressed page data
> + * length - the length in bytes of the compressed page data. Needed during
> + * decompression

The sequence is different from the struct define?

> + */
> +struct zswap_entry {
> + struct rb_node rbnode;
> + pgoff_t offset;
> + int refcount;
> + unsigned int length;
> + unsigned long handle;
> +};
> +
> +struct zswap_header {
> + swp_entry_t swpentry;
> +};
> +
> +/*
> + * The tree lock in the zswap_tree struct protects a few things:
> + * - the rbtree
> + * - the refcount field of each entry in the tree
> + */
> +struct zswap_tree {
> + struct rb_root rbroot;
> + spinlock_t lock;
> + struct zbud_pool *pool;
> + unsigned type;

It seems that zswap_tree->type have no usage for zswap.

> +};
> +
> +static struct zswap_tree *zswap_trees[MAX_SWAPFILES];
> +
> +/*********************************
> +* zswap entry functions
> +**********************************/
> +#define ZSWAP_KMEM_CACHE_NAME "zswap_entry_cache"
> +static struct kmem_cache *zswap_entry_cache;
> +
> +static inline int zswap_entry_cache_create(void)
> +{
> + zswap_entry_cache =
> + kmem_cache_create(ZSWAP_KMEM_CACHE_NAME,
> + sizeof(struct zswap_entry), 0, 0, NULL);
> + return (zswap_entry_cache == NULL);
> +}
> +
> +static inline void zswap_entry_cache_destory(void)
> +{
> + kmem_cache_destroy(zswap_entry_cache);
> +}
> +
> +static inline struct zswap_entry *zswap_entry_cache_alloc(gfp_t gfp)
> +{
> + struct zswap_entry *entry;
> + entry = kmem_cache_alloc(zswap_entry_cache, gfp);
> + if (!entry)
> + return NULL;
> + entry->refcount = 1;
> + return entry;
> +}
> +
> +static inline void zswap_entry_cache_free(struct zswap_entry *entry)
> +{
> + kmem_cache_free(zswap_entry_cache, entry);
> +}
> +
> +static inline void zswap_entry_get(struct zswap_entry *entry)
> +{
> + entry->refcount++;
> +}
> +
> +static inline int zswap_entry_put(struct zswap_entry *entry)
> +{
> + entry->refcount--;
> + return entry->refcount;
> +}

Better if have lock comments here.

> +
> +/*********************************
> +* rbtree functions
> +**********************************/
> +static struct zswap_entry *zswap_rb_search(struct rb_root *root, pgoff_t offset)
> +{
> + struct rb_node *node = root->rb_node;
> + struct zswap_entry *entry;
> +
> + while (node) {
> + entry = rb_entry(node, struct zswap_entry, rbnode);
> + if (entry->offset > offset)
> + node = node->rb_left;
> + else if (entry->offset < offset)
> + node = node->rb_right;
> + else
> + return entry;
> + }
> + return NULL;
> +}
> +
> +/*
> + * In the case that a entry with the same offset is found, it a pointer to
> + * the existing entry is stored in dupentry and the function returns -EEXIST
> +*/
> +static int zswap_rb_insert(struct rb_root *root, struct zswap_entry *entry,
> + struct zswap_entry **dupentry)
> +{
> + struct rb_node **link = &root->rb_node, *parent = NULL;
> + struct zswap_entry *myentry;
> +
> + while (*link) {
> + parent = *link;
> + myentry = rb_entry(parent, struct zswap_entry, rbnode);
> + if (myentry->offset > entry->offset)
> + link = &(*link)->rb_left;
> + else if (myentry->offset < entry->offset)
> + link = &(*link)->rb_right;
> + else {
> + *dupentry = myentry;
> + return -EEXIST;
> + }
> + }
> + rb_link_node(&entry->rbnode, parent, link);
> + rb_insert_color(&entry->rbnode, root);
> + return 0;
> +}
> +
> +/*********************************
> +* per-cpu code
> +**********************************/
> +static DEFINE_PER_CPU(u8 *, zswap_dstmem);
> +
> +static int __zswap_cpu_notifier(unsigned long action, unsigned long cpu)
> +{
> + struct crypto_comp *tfm;
> + u8 *dst;
> +
> + switch (action) {
> + case CPU_UP_PREPARE:
> + tfm = crypto_alloc_comp(zswap_compressor, 0, 0);
> + if (IS_ERR(tfm)) {
> + pr_err("can't allocate compressor transform\n");
> + return NOTIFY_BAD;
> + }
> + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = tfm;
> + dst = kmalloc(PAGE_SIZE * 2, GFP_KERNEL);
> + if (!dst) {
> + pr_err("can't allocate compressor buffer\n");
> + crypto_free_comp(tfm);
> + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> + return NOTIFY_BAD;
> + }
> + per_cpu(zswap_dstmem, cpu) = dst;
> + break;
> + case CPU_DEAD:
> + case CPU_UP_CANCELED:
> + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu);
> + if (tfm) {
> + crypto_free_comp(tfm);
> + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> + }
> + dst = per_cpu(zswap_dstmem, cpu);
> + kfree(dst);
> + per_cpu(zswap_dstmem, cpu) = NULL;
> + break;
> + default:
> + break;
> + }
> + return NOTIFY_OK;
> +}
> +
> +static int zswap_cpu_notifier(struct notifier_block *nb,
> + unsigned long action, void *pcpu)
> +{
> + unsigned long cpu = (unsigned long)pcpu;
> + return __zswap_cpu_notifier(action, cpu);
> +}
> +
> +static struct notifier_block zswap_cpu_notifier_block = {
> + .notifier_call = zswap_cpu_notifier
> +};
> +
> +static int zswap_cpu_init(void)
> +{
> + unsigned long cpu;
> +
> + get_online_cpus();
> + for_each_online_cpu(cpu)
> + if (__zswap_cpu_notifier(CPU_UP_PREPARE, cpu) != NOTIFY_OK)
> + goto cleanup;
> + register_cpu_notifier(&zswap_cpu_notifier_block);
> + put_online_cpus();
> + return 0;
> +
> +cleanup:
> + for_each_online_cpu(cpu)
> + __zswap_cpu_notifier(CPU_UP_CANCELED, cpu);
> + put_online_cpus();
> + return -ENOMEM;
> +}
> +
> +/*********************************
> +* helpers
> +**********************************/
> +static inline bool zswap_is_full(void)
> +{
> + int pool_pages = atomic_read(&zswap_pool_pages);
> + return (totalram_pages * zswap_max_pool_percent / 100 < pool_pages);
> +}
> +
> +/*
> + * Carries out the common pattern of freeing and entry's zsmalloc allocation,
> + * freeing the entry itself, and decrementing the number of stored pages.
> + */
> +static void zswap_free_entry(struct zswap_tree *tree, struct zswap_entry *entry)
> +{
> + zbud_free(tree->pool, entry->handle);
> + zswap_entry_cache_free(entry);
> + atomic_dec(&zswap_stored_pages);
> + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> +}
> +
> +/*********************************
> +* writeback code
> +**********************************/
> +/* return enum for zswap_get_swap_cache_page */
> +enum zswap_get_swap_ret {
> + ZSWAP_SWAPCACHE_NEW,
> + ZSWAP_SWAPCACHE_EXIST,
> + ZSWAP_SWAPCACHE_NOMEM
> +};
> +
> +/*
> + * zswap_get_swap_cache_page
> + *
> + * This is an adaption of read_swap_cache_async()
> + *
> + * This function tries to find a page with the given swap entry
> + * in the swapper_space address space (the swap cache). If the page
> + * is found, it is returned in retpage. Otherwise, a page is allocated,
> + * added to the swap cache, and returned in retpage.
> + *
> + * If success, the swap cache page is returned in retpage
> + * Returns 0 if page was already in the swap cache, page is not locked
> + * Returns 1 if the new page needs to be populated, page is locked
> + * Returns <0 on error
> + */
> +static int zswap_get_swap_cache_page(swp_entry_t entry,
> + struct page **retpage)
> +{
> + struct page *found_page, *new_page = NULL;
> + struct address_space *swapper_space = &swapper_spaces[swp_type(entry)];
> + int err;
> +
> + *retpage = NULL;
> + do {
> + /*
> + * First check the swap cache. Since this is normally
> + * called after lookup_swap_cache() failed, re-calling
> + * that would confuse statistics.
> + */
> + found_page = find_get_page(swapper_space, entry.val);
> + if (found_page)
> + break;
> +
> + /*
> + * Get a new page to read into from swap.
> + */
> + if (!new_page) {
> + new_page = alloc_page(GFP_KERNEL);
> + if (!new_page)
> + break; /* Out of memory */
> + }
> +
> + /*
> + * call radix_tree_preload() while we can wait.
> + */
> + err = radix_tree_preload(GFP_KERNEL);
> + if (err)
> + break;
> +
> + /*
> + * Swap entry may have been freed since our caller observed it.
> + */
> + err = swapcache_prepare(entry);
> + if (err == -EEXIST) { /* seems racy */
> + radix_tree_preload_end();
> + continue;
> + }
> + if (err) { /* swp entry is obsolete ? */
> + radix_tree_preload_end();
> + break;
> + }
> +
> + /* May fail (-ENOMEM) if radix-tree node allocation failed. */
> + __set_page_locked(new_page);
> + SetPageSwapBacked(new_page);
> + err = __add_to_swap_cache(new_page, entry);
> + if (likely(!err)) {
> + radix_tree_preload_end();
> + lru_cache_add_anon(new_page);
> + *retpage = new_page;
> + return ZSWAP_SWAPCACHE_NEW;
> + }
> + radix_tree_preload_end();
> + ClearPageSwapBacked(new_page);
> + __clear_page_locked(new_page);
> + /*
> + * add_to_swap_cache() doesn't return -EEXIST, so we can safely
> + * clear SWAP_HAS_CACHE flag.
> + */
> + swapcache_free(entry, NULL);
> + } while (err != -ENOMEM);
> +
> + if (new_page)
> + page_cache_release(new_page);
> + if (!found_page)
> + return ZSWAP_SWAPCACHE_NOMEM;
> + *retpage = found_page;
> + return ZSWAP_SWAPCACHE_EXIST;
> +}
> +
> +/*
> + * Attempts to free and entry by adding a page to the swap cache,
> + * decompressing the entry data into the page, and issuing a
> + * bio write to write the page back to the swap device.
> + *
> + * This can be thought of as a "resumed writeback" of the page
> + * to the swap device. We are basically resuming the same swap
> + * writeback path that was intercepted with the frontswap_store()
> + * in the first place. After the page has been decompressed into
> + * the swap cache, the compressed version stored by zswap can be
> + * freed.
> + */
> +static int zswap_writeback_entry(struct zbud_pool *pool, unsigned long handle)
> +{
> + struct zswap_header *zhdr;
> + swp_entry_t swpentry;
> + struct zswap_tree *tree;
> + pgoff_t offset;
> + struct zswap_entry *entry;
> + struct page *page;
> + u8 *src, *dst;
> + unsigned int dlen;
> + int ret, refcount;
> + struct writeback_control wbc = {
> + .sync_mode = WB_SYNC_NONE,
> + };
> +
> + /* extract swpentry from data */
> + zhdr = zbud_map(pool, handle);
> + swpentry = zhdr->swpentry; /* here */
> + zbud_unmap(pool, handle);
> + tree = zswap_trees[swp_type(swpentry)];
> + offset = swp_offset(swpentry);
> + BUG_ON(pool != tree->pool);
> +
> + /* find and ref zswap entry */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was invalidated */
> + spin_unlock(&tree->lock);
> + return 0;
> + }
> + zswap_entry_get(entry);
> + spin_unlock(&tree->lock);
> + BUG_ON(offset != entry->offset);
> +
> + /* try to allocate swap cache page */
> + switch (zswap_get_swap_cache_page(swpentry, &page)) {
> + case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
> + ret = -ENOMEM;
> + goto fail;
> +
> + case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
> + /* page is already in the swap cache, ignore for now */
> + page_cache_release(page);
> + ret = -EEXIST;
> + goto fail;
> +
> + case ZSWAP_SWAPCACHE_NEW: /* page is locked */
> + /* decompress */
> + dlen = PAGE_SIZE;
> + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> + sizeof(struct zswap_header);
> + dst = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
> + entry->length, dst, &dlen);
> + kunmap_atomic(dst);
> + zbud_unmap(tree->pool, entry->handle);
> + BUG_ON(ret);
> + BUG_ON(dlen != PAGE_SIZE);
> +
> + /* page is up to date */
> + SetPageUptodate(page);
> + }
> +
> + /* start writeback */
> + SetPageReclaim(page);
> + __swap_writepage(page, &wbc, end_swap_bio_write);
> + page_cache_release(page);
> + zswap_written_back_pages++;
> +
> + spin_lock(&tree->lock);
> +
> + /* drop local reference */
> + zswap_entry_put(entry);
> + /* drop the initial reference from entry creation */
> + refcount = zswap_entry_put(entry);
> +
> + /*
> + * There are three possible values for refcount here:
> + * (1) refcount is 1, load is in progress, unlink from rbtree,
> + * load will free
> + * (2) refcount is 0, (normal case) entry is valid,
> + * remove from rbtree and free entry
> + * (3) refcount is -1, invalidate happened during writeback,
> + * free entry
> + */
> + if (refcount >= 0) {
> + /* no invalidate yet, remove from rbtree */
> + rb_erase(&entry->rbnode, &tree->rbroot);
> + }
> + spin_unlock(&tree->lock);
> + if (refcount <= 0) {
> + /* free the entry */
> + zswap_free_entry(tree, entry);
> + return 0;
> + }
> + return -EAGAIN;
> +
> +fail:
> + spin_lock(&tree->lock);
> + zswap_entry_put(entry);
> + spin_unlock(&tree->lock);
> + return ret;
> +}
> +
> +/*********************************
> +* frontswap hooks
> +**********************************/
> +/* attempts to compress and store an single page */
> +static int zswap_frontswap_store(unsigned type, pgoff_t offset,
> + struct page *page)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry, *dupentry;
> + int ret;
> + unsigned int dlen = PAGE_SIZE, len;
> + unsigned long handle;
> + char *buf;
> + u8 *src, *dst;
> + struct zswap_header *zhdr;
> +
> + if (!tree) {
> + ret = -ENODEV;
> + goto reject;
> + }
> +
> + /* reclaim space if needed */
> + if (zswap_is_full()) {
> + zswap_pool_limit_hit++;
> + if (zbud_reclaim_page(tree->pool, 8)) {

My idea is to wake up a kernel thread here to do the reclaim.
Once zswap is full(20% percent of total mem currently), the kernel
thread should reclaim pages from it. Not only reclaim one page, it
should depend on the current memory pressure.
And then the API in zbud may like this:
zbud_reclaim_page(pool, nr_pages_to_reclaim, nr_retry);


> + zswap_reject_reclaim_fail++;
> + ret = -ENOMEM;
> + goto reject;
> + }
> + }
> +


--
Regards,
-Bob

2013-05-14 16:02:10

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Tue, May 14, 2013 at 05:19:19PM +0800, Bob Liu wrote:
> Hi Seth,

Hi Bob, thanks for the review!

>
> On 05/13/2013 08:40 PM, Seth Jennings wrote:
> > zswap is a thin compression backend for frontswap. It receives pages from
> > frontswap and attempts to store them in a compressed memory pool, resulting in
> > an effective partial memory reclaim and dramatically reduced swap device I/O.
> >
> > Additionally, in most cases, pages can be retrieved from this compressed store
> > much more quickly than reading from tradition swap devices resulting in faster
> > performance for many workloads.
> >
> > It also has support for evicting swap pages that are currently compressed in
> > zswap to the swap device on an LRU(ish) basis. This functionality is very
> > important and make zswap a true cache in that, once the cache is full or can't
> > grow due to memory pressure, the oldest pages can be moved out of zswap to the
> > swap device so newer pages can be compressed and stored in zswap.
> >
> > This patch adds the zswap driver to mm/
> >
> > Signed-off-by: Seth Jennings <[email protected]>
>
> It seems that you didn't address some comments from Mel in
> [PATCHv9 4/8] zswap: add to mm/
>
> > ---
> > mm/Kconfig | 15 +
> > mm/Makefile | 1 +
> > mm/zswap.c | 952 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
> > 3 files changed, 968 insertions(+)
> > create mode 100644 mm/zswap.c
> >
> > diff --git a/mm/Kconfig b/mm/Kconfig
> > index 908f41b..4042e07 100644
> > --- a/mm/Kconfig
> > +++ b/mm/Kconfig
> > @@ -487,3 +487,18 @@ config ZBUD
> > While this design limits storage density, it has simple and
> > deterministic reclaim properties that make it preferable to a higher
> > density approach when reclaim will be used.
> > +
> > +config ZSWAP
> > + bool "In-kernel swap page compression"
> > + depends on FRONTSWAP && CRYPTO
> > + select CRYPTO_LZO
> > + select ZBUD
> > + default n
> > + help
> > + Zswap is a backend for the frontswap mechanism in the VMM.
> > + It receives pages from frontswap and attempts to store them
> > + in a compressed memory pool, resulting in an effective
> > + partial memory reclaim. In addition, pages and be retrieved
> > + from this compressed store much faster than most tradition
> > + swap devices resulting in reduced I/O and faster performance
> > + for many workloads.
> > diff --git a/mm/Makefile b/mm/Makefile
> > index 95f0197..f008033 100644
> > --- a/mm/Makefile
> > +++ b/mm/Makefile
> > @@ -32,6 +32,7 @@ obj-$(CONFIG_HAVE_MEMBLOCK) += memblock.o
> > obj-$(CONFIG_BOUNCE) += bounce.o
> > obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
> > obj-$(CONFIG_FRONTSWAP) += frontswap.o
> > +obj-$(CONFIG_ZSWAP) += zswap.o
> > obj-$(CONFIG_HAS_DMA) += dmapool.o
> > obj-$(CONFIG_HUGETLBFS) += hugetlb.o
> > obj-$(CONFIG_NUMA) += mempolicy.o
> > diff --git a/mm/zswap.c b/mm/zswap.c
> > new file mode 100644
> > index 0000000..b1070ca
> > --- /dev/null
> > +++ b/mm/zswap.c
> > @@ -0,0 +1,952 @@
> > +/*
> > + * zswap.c - zswap driver file
> > + *
> > + * zswap is a backend for frontswap that takes pages that are in the
> > + * process of being swapped out and attempts to compress them and store
> > + * them in a RAM-based memory pool. This results in a significant I/O
> > + * reduction on the real swap device and, in the case of a slow swap
> > + * device, can also improve workload performance.
> > + *
> > + * Copyright (C) 2012 Seth Jennings <[email protected]>
> > + *
> > + * This program is free software; you can redistribute it and/or
> > + * modify it under the terms of the GNU General Public License
> > + * as published by the Free Software Foundation; either version 2
> > + * of the License, or (at your option) any later version.
> > + *
> > + * This program is distributed in the hope that it will be useful,
> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> > + * GNU General Public License for more details.
> > +*/
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/module.h>
> > +#include <linux/cpu.h>
> > +#include <linux/highmem.h>
> > +#include <linux/slab.h>
> > +#include <linux/spinlock.h>
> > +#include <linux/types.h>
> > +#include <linux/atomic.h>
> > +#include <linux/frontswap.h>
> > +#include <linux/rbtree.h>
> > +#include <linux/swap.h>
> > +#include <linux/crypto.h>
> > +#include <linux/mempool.h>
> > +#include <linux/zbud.h>
> > +
> > +#include <linux/mm_types.h>
> > +#include <linux/page-flags.h>
> > +#include <linux/swapops.h>
> > +#include <linux/writeback.h>
> > +#include <linux/pagemap.h>
> > +
> > +/*********************************
> > +* statistics
> > +**********************************/
> > +/* Number of memory pages used by the compressed pool */
> > +static atomic_t zswap_pool_pages = ATOMIC_INIT(0);
> > +/* The number of compressed pages currently stored in zswap */
> > +static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
> > +
> > +/*
> > + * The statistics below are not protected from concurrent access for
> > + * performance reasons so they may not be a 100% accurate. However,
> > + * they do provide useful information on roughly how many times a
> > + * certain event is occurring.
> > +*/
> > +static u64 zswap_pool_limit_hit;
> > +static u64 zswap_written_back_pages;
> > +static u64 zswap_reject_reclaim_fail;
> > +static u64 zswap_reject_compress_poor;
> > +static u64 zswap_reject_alloc_fail;
> > +static u64 zswap_reject_kmemcache_fail;
> > +static u64 zswap_duplicate_entry;
> > +
> > +/*********************************
> > +* tunables
> > +**********************************/
> > +/* Enable/disable zswap (disabled by default, fixed at boot for now) */
> > +static bool zswap_enabled;
> > +module_param_named(enabled, zswap_enabled, bool, 0);
> > +
> > +/* Compressor to be used by zswap (fixed at boot for now) */
> > +#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
> > +static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> > +module_param_named(compressor, zswap_compressor, charp, 0);
> > +
> > +/* The maximum percentage of memory that the compressed pool can occupy */
> > +static unsigned int zswap_max_pool_percent = 20;
> > +module_param_named(max_pool_percent,
> > + zswap_max_pool_percent, uint, 0644);
> > +
>
> I think it's reasonable but... see comments in zbud_reclaim_page().
>
> > +/*
> > + * Maximum compression ratio, as as percentage, for an acceptable
> > + * compressed page. Any pages that do not compress by at least
> > + * this ratio will be rejected.
> > +*/
> > +static unsigned int zswap_max_compression_ratio = 80;
> > +module_param_named(max_compression_ratio,
> > + zswap_max_compression_ratio, uint, 0644);
> > +
>
> Prefer not export it, it's hard for use to know what value should set to.

True, this is kind of a remnant from zsmalloc. I can remove it.

>
> > +/*********************************
> > +* compression functions
> > +**********************************/
> > +/* per-cpu compression transforms */
> > +static struct crypto_comp * __percpu *zswap_comp_pcpu_tfms;
> > +
> > +enum comp_op {
> > + ZSWAP_COMPOP_COMPRESS,
> > + ZSWAP_COMPOP_DECOMPRESS
> > +};
> > +
> > +static int zswap_comp_op(enum comp_op op, const u8 *src, unsigned int slen,
> > + u8 *dst, unsigned int *dlen)
> > +{
> > + struct crypto_comp *tfm;
> > + int ret;
> > +
> > + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, get_cpu());
> > + switch (op) {
> > + case ZSWAP_COMPOP_COMPRESS:
> > + ret = crypto_comp_compress(tfm, src, slen, dst, dlen);
> > + break;
> > + case ZSWAP_COMPOP_DECOMPRESS:
> > + ret = crypto_comp_decompress(tfm, src, slen, dst, dlen);
> > + break;
> > + default:
> > + ret = -EINVAL;
> > + }
> > +
> > + put_cpu();
> > + return ret;
> > +}
> > +
> > +static int __init zswap_comp_init(void)
> > +{
> > + if (!crypto_has_comp(zswap_compressor, 0, 0)) {
> > + pr_info("%s compressor not available\n", zswap_compressor);
> > + /* fall back to default compressor */
> > + zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> > + if (!crypto_has_comp(zswap_compressor, 0, 0))
> > + /* can't even load the default compressor */
> > + return -ENODEV;
> > + }
> > + pr_info("using %s compressor\n", zswap_compressor);
> > +
> > + /* alloc percpu transforms */
> > + zswap_comp_pcpu_tfms = alloc_percpu(struct crypto_comp *);
> > + if (!zswap_comp_pcpu_tfms)
> > + return -ENOMEM;
> > + return 0;
> > +}
> > +
> > +static void zswap_comp_exit(void)
> > +{
> > + /* free percpu transforms */
> > + if (zswap_comp_pcpu_tfms)
> > + free_percpu(zswap_comp_pcpu_tfms);
> > +}
> > +
> > +/*********************************
> > +* data structures
> > +**********************************/
> > +/*
> > + * struct zswap_entry
> > + *
> > + * This structure contains the metadata for tracking a single compressed
> > + * page within zswap.
> > + *
> > + * rbnode - links the entry into red-black tree for the appropriate swap type
> > + * refcount - the number of outstanding reference to the entry. This is needed
> > + * to protect against premature freeing of the entry by code
> > + * concurent calls to load, invalidate, and writeback. The lock
> > + * for the zswap_tree structure that contains the entry must
> > + * be held while changing the refcount. Since the lock must
> > + * be held, there is no reason to also make refcount atomic.
> > + * type - the swap type for the entry. Used to map back to the zswap_tree
> > + * structure that contains the entry.
> > + * offset - the swap offset for the entry. Index into the red-black tree.
> > + * handle - zsmalloc allocation handle that stores the compressed page data
> > + * length - the length in bytes of the compressed page data. Needed during
> > + * decompression
>
> The sequence is different from the struct define?

Yes, Mel pointed this out too and I forgot to make the change.

>
> > + */
> > +struct zswap_entry {
> > + struct rb_node rbnode;
> > + pgoff_t offset;
> > + int refcount;
> > + unsigned int length;
> > + unsigned long handle;
> > +};
> > +
> > +struct zswap_header {
> > + swp_entry_t swpentry;
> > +};
> > +
> > +/*
> > + * The tree lock in the zswap_tree struct protects a few things:
> > + * - the rbtree
> > + * - the refcount field of each entry in the tree
> > + */
> > +struct zswap_tree {
> > + struct rb_root rbroot;
> > + spinlock_t lock;
> > + struct zbud_pool *pool;
> > + unsigned type;
>
> It seems that zswap_tree->type have no usage for zswap.

Good catch. Seems that it isn't used anymore.

>
> > +};
> > +
> > +static struct zswap_tree *zswap_trees[MAX_SWAPFILES];
> > +
> > +/*********************************
> > +* zswap entry functions
> > +**********************************/
> > +#define ZSWAP_KMEM_CACHE_NAME "zswap_entry_cache"
> > +static struct kmem_cache *zswap_entry_cache;
> > +
> > +static inline int zswap_entry_cache_create(void)
> > +{
> > + zswap_entry_cache =
> > + kmem_cache_create(ZSWAP_KMEM_CACHE_NAME,
> > + sizeof(struct zswap_entry), 0, 0, NULL);
> > + return (zswap_entry_cache == NULL);
> > +}
> > +
> > +static inline void zswap_entry_cache_destory(void)
> > +{
> > + kmem_cache_destroy(zswap_entry_cache);
> > +}
> > +
> > +static inline struct zswap_entry *zswap_entry_cache_alloc(gfp_t gfp)
> > +{
> > + struct zswap_entry *entry;
> > + entry = kmem_cache_alloc(zswap_entry_cache, gfp);
> > + if (!entry)
> > + return NULL;
> > + entry->refcount = 1;
> > + return entry;
> > +}
> > +
> > +static inline void zswap_entry_cache_free(struct zswap_entry *entry)
> > +{
> > + kmem_cache_free(zswap_entry_cache, entry);
> > +}
> > +
> > +static inline void zswap_entry_get(struct zswap_entry *entry)
> > +{
> > + entry->refcount++;
> > +}
> > +
> > +static inline int zswap_entry_put(struct zswap_entry *entry)
> > +{
> > + entry->refcount--;
> > + return entry->refcount;
> > +}
>
> Better if have lock comments here.

will do.

>
> > +
> > +/*********************************
> > +* rbtree functions
> > +**********************************/
> > +static struct zswap_entry *zswap_rb_search(struct rb_root *root, pgoff_t offset)
> > +{
> > + struct rb_node *node = root->rb_node;
> > + struct zswap_entry *entry;
> > +
> > + while (node) {
> > + entry = rb_entry(node, struct zswap_entry, rbnode);
> > + if (entry->offset > offset)
> > + node = node->rb_left;
> > + else if (entry->offset < offset)
> > + node = node->rb_right;
> > + else
> > + return entry;
> > + }
> > + return NULL;
> > +}
> > +
> > +/*
> > + * In the case that a entry with the same offset is found, it a pointer to
> > + * the existing entry is stored in dupentry and the function returns -EEXIST
> > +*/
> > +static int zswap_rb_insert(struct rb_root *root, struct zswap_entry *entry,
> > + struct zswap_entry **dupentry)
> > +{
> > + struct rb_node **link = &root->rb_node, *parent = NULL;
> > + struct zswap_entry *myentry;
> > +
> > + while (*link) {
> > + parent = *link;
> > + myentry = rb_entry(parent, struct zswap_entry, rbnode);
> > + if (myentry->offset > entry->offset)
> > + link = &(*link)->rb_left;
> > + else if (myentry->offset < entry->offset)
> > + link = &(*link)->rb_right;
> > + else {
> > + *dupentry = myentry;
> > + return -EEXIST;
> > + }
> > + }
> > + rb_link_node(&entry->rbnode, parent, link);
> > + rb_insert_color(&entry->rbnode, root);
> > + return 0;
> > +}
> > +
> > +/*********************************
> > +* per-cpu code
> > +**********************************/
> > +static DEFINE_PER_CPU(u8 *, zswap_dstmem);
> > +
> > +static int __zswap_cpu_notifier(unsigned long action, unsigned long cpu)
> > +{
> > + struct crypto_comp *tfm;
> > + u8 *dst;
> > +
> > + switch (action) {
> > + case CPU_UP_PREPARE:
> > + tfm = crypto_alloc_comp(zswap_compressor, 0, 0);
> > + if (IS_ERR(tfm)) {
> > + pr_err("can't allocate compressor transform\n");
> > + return NOTIFY_BAD;
> > + }
> > + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = tfm;
> > + dst = kmalloc(PAGE_SIZE * 2, GFP_KERNEL);
> > + if (!dst) {
> > + pr_err("can't allocate compressor buffer\n");
> > + crypto_free_comp(tfm);
> > + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> > + return NOTIFY_BAD;
> > + }
> > + per_cpu(zswap_dstmem, cpu) = dst;
> > + break;
> > + case CPU_DEAD:
> > + case CPU_UP_CANCELED:
> > + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu);
> > + if (tfm) {
> > + crypto_free_comp(tfm);
> > + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> > + }
> > + dst = per_cpu(zswap_dstmem, cpu);
> > + kfree(dst);
> > + per_cpu(zswap_dstmem, cpu) = NULL;
> > + break;
> > + default:
> > + break;
> > + }
> > + return NOTIFY_OK;
> > +}
> > +
> > +static int zswap_cpu_notifier(struct notifier_block *nb,
> > + unsigned long action, void *pcpu)
> > +{
> > + unsigned long cpu = (unsigned long)pcpu;
> > + return __zswap_cpu_notifier(action, cpu);
> > +}
> > +
> > +static struct notifier_block zswap_cpu_notifier_block = {
> > + .notifier_call = zswap_cpu_notifier
> > +};
> > +
> > +static int zswap_cpu_init(void)
> > +{
> > + unsigned long cpu;
> > +
> > + get_online_cpus();
> > + for_each_online_cpu(cpu)
> > + if (__zswap_cpu_notifier(CPU_UP_PREPARE, cpu) != NOTIFY_OK)
> > + goto cleanup;
> > + register_cpu_notifier(&zswap_cpu_notifier_block);
> > + put_online_cpus();
> > + return 0;
> > +
> > +cleanup:
> > + for_each_online_cpu(cpu)
> > + __zswap_cpu_notifier(CPU_UP_CANCELED, cpu);
> > + put_online_cpus();
> > + return -ENOMEM;
> > +}
> > +
> > +/*********************************
> > +* helpers
> > +**********************************/
> > +static inline bool zswap_is_full(void)
> > +{
> > + int pool_pages = atomic_read(&zswap_pool_pages);
> > + return (totalram_pages * zswap_max_pool_percent / 100 < pool_pages);
> > +}
> > +
> > +/*
> > + * Carries out the common pattern of freeing and entry's zsmalloc allocation,
> > + * freeing the entry itself, and decrementing the number of stored pages.
> > + */
> > +static void zswap_free_entry(struct zswap_tree *tree, struct zswap_entry *entry)
> > +{
> > + zbud_free(tree->pool, entry->handle);
> > + zswap_entry_cache_free(entry);
> > + atomic_dec(&zswap_stored_pages);
> > + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> > +}
> > +
> > +/*********************************
> > +* writeback code
> > +**********************************/
> > +/* return enum for zswap_get_swap_cache_page */
> > +enum zswap_get_swap_ret {
> > + ZSWAP_SWAPCACHE_NEW,
> > + ZSWAP_SWAPCACHE_EXIST,
> > + ZSWAP_SWAPCACHE_NOMEM
> > +};
> > +
> > +/*
> > + * zswap_get_swap_cache_page
> > + *
> > + * This is an adaption of read_swap_cache_async()
> > + *
> > + * This function tries to find a page with the given swap entry
> > + * in the swapper_space address space (the swap cache). If the page
> > + * is found, it is returned in retpage. Otherwise, a page is allocated,
> > + * added to the swap cache, and returned in retpage.
> > + *
> > + * If success, the swap cache page is returned in retpage
> > + * Returns 0 if page was already in the swap cache, page is not locked
> > + * Returns 1 if the new page needs to be populated, page is locked
> > + * Returns <0 on error
> > + */
> > +static int zswap_get_swap_cache_page(swp_entry_t entry,
> > + struct page **retpage)
> > +{
> > + struct page *found_page, *new_page = NULL;
> > + struct address_space *swapper_space = &swapper_spaces[swp_type(entry)];
> > + int err;
> > +
> > + *retpage = NULL;
> > + do {
> > + /*
> > + * First check the swap cache. Since this is normally
> > + * called after lookup_swap_cache() failed, re-calling
> > + * that would confuse statistics.
> > + */
> > + found_page = find_get_page(swapper_space, entry.val);
> > + if (found_page)
> > + break;
> > +
> > + /*
> > + * Get a new page to read into from swap.
> > + */
> > + if (!new_page) {
> > + new_page = alloc_page(GFP_KERNEL);
> > + if (!new_page)
> > + break; /* Out of memory */
> > + }
> > +
> > + /*
> > + * call radix_tree_preload() while we can wait.
> > + */
> > + err = radix_tree_preload(GFP_KERNEL);
> > + if (err)
> > + break;
> > +
> > + /*
> > + * Swap entry may have been freed since our caller observed it.
> > + */
> > + err = swapcache_prepare(entry);
> > + if (err == -EEXIST) { /* seems racy */
> > + radix_tree_preload_end();
> > + continue;
> > + }
> > + if (err) { /* swp entry is obsolete ? */
> > + radix_tree_preload_end();
> > + break;
> > + }
> > +
> > + /* May fail (-ENOMEM) if radix-tree node allocation failed. */
> > + __set_page_locked(new_page);
> > + SetPageSwapBacked(new_page);
> > + err = __add_to_swap_cache(new_page, entry);
> > + if (likely(!err)) {
> > + radix_tree_preload_end();
> > + lru_cache_add_anon(new_page);
> > + *retpage = new_page;
> > + return ZSWAP_SWAPCACHE_NEW;
> > + }
> > + radix_tree_preload_end();
> > + ClearPageSwapBacked(new_page);
> > + __clear_page_locked(new_page);
> > + /*
> > + * add_to_swap_cache() doesn't return -EEXIST, so we can safely
> > + * clear SWAP_HAS_CACHE flag.
> > + */
> > + swapcache_free(entry, NULL);
> > + } while (err != -ENOMEM);
> > +
> > + if (new_page)
> > + page_cache_release(new_page);
> > + if (!found_page)
> > + return ZSWAP_SWAPCACHE_NOMEM;
> > + *retpage = found_page;
> > + return ZSWAP_SWAPCACHE_EXIST;
> > +}
> > +
> > +/*
> > + * Attempts to free and entry by adding a page to the swap cache,
> > + * decompressing the entry data into the page, and issuing a
> > + * bio write to write the page back to the swap device.
> > + *
> > + * This can be thought of as a "resumed writeback" of the page
> > + * to the swap device. We are basically resuming the same swap
> > + * writeback path that was intercepted with the frontswap_store()
> > + * in the first place. After the page has been decompressed into
> > + * the swap cache, the compressed version stored by zswap can be
> > + * freed.
> > + */
> > +static int zswap_writeback_entry(struct zbud_pool *pool, unsigned long handle)
> > +{
> > + struct zswap_header *zhdr;
> > + swp_entry_t swpentry;
> > + struct zswap_tree *tree;
> > + pgoff_t offset;
> > + struct zswap_entry *entry;
> > + struct page *page;
> > + u8 *src, *dst;
> > + unsigned int dlen;
> > + int ret, refcount;
> > + struct writeback_control wbc = {
> > + .sync_mode = WB_SYNC_NONE,
> > + };
> > +
> > + /* extract swpentry from data */
> > + zhdr = zbud_map(pool, handle);
> > + swpentry = zhdr->swpentry; /* here */
> > + zbud_unmap(pool, handle);
> > + tree = zswap_trees[swp_type(swpentry)];
> > + offset = swp_offset(swpentry);
> > + BUG_ON(pool != tree->pool);
> > +
> > + /* find and ref zswap entry */
> > + spin_lock(&tree->lock);
> > + entry = zswap_rb_search(&tree->rbroot, offset);
> > + if (!entry) {
> > + /* entry was invalidated */
> > + spin_unlock(&tree->lock);
> > + return 0;
> > + }
> > + zswap_entry_get(entry);
> > + spin_unlock(&tree->lock);
> > + BUG_ON(offset != entry->offset);
> > +
> > + /* try to allocate swap cache page */
> > + switch (zswap_get_swap_cache_page(swpentry, &page)) {
> > + case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
> > + ret = -ENOMEM;
> > + goto fail;
> > +
> > + case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
> > + /* page is already in the swap cache, ignore for now */
> > + page_cache_release(page);
> > + ret = -EEXIST;
> > + goto fail;
> > +
> > + case ZSWAP_SWAPCACHE_NEW: /* page is locked */
> > + /* decompress */
> > + dlen = PAGE_SIZE;
> > + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> > + sizeof(struct zswap_header);
> > + dst = kmap_atomic(page);
> > + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
> > + entry->length, dst, &dlen);
> > + kunmap_atomic(dst);
> > + zbud_unmap(tree->pool, entry->handle);
> > + BUG_ON(ret);
> > + BUG_ON(dlen != PAGE_SIZE);
> > +
> > + /* page is up to date */
> > + SetPageUptodate(page);
> > + }
> > +
> > + /* start writeback */
> > + SetPageReclaim(page);
> > + __swap_writepage(page, &wbc, end_swap_bio_write);
> > + page_cache_release(page);
> > + zswap_written_back_pages++;
> > +
> > + spin_lock(&tree->lock);
> > +
> > + /* drop local reference */
> > + zswap_entry_put(entry);
> > + /* drop the initial reference from entry creation */
> > + refcount = zswap_entry_put(entry);
> > +
> > + /*
> > + * There are three possible values for refcount here:
> > + * (1) refcount is 1, load is in progress, unlink from rbtree,
> > + * load will free
> > + * (2) refcount is 0, (normal case) entry is valid,
> > + * remove from rbtree and free entry
> > + * (3) refcount is -1, invalidate happened during writeback,
> > + * free entry
> > + */
> > + if (refcount >= 0) {
> > + /* no invalidate yet, remove from rbtree */
> > + rb_erase(&entry->rbnode, &tree->rbroot);
> > + }
> > + spin_unlock(&tree->lock);
> > + if (refcount <= 0) {
> > + /* free the entry */
> > + zswap_free_entry(tree, entry);
> > + return 0;
> > + }
> > + return -EAGAIN;
> > +
> > +fail:
> > + spin_lock(&tree->lock);
> > + zswap_entry_put(entry);
> > + spin_unlock(&tree->lock);
> > + return ret;
> > +}
> > +
> > +/*********************************
> > +* frontswap hooks
> > +**********************************/
> > +/* attempts to compress and store an single page */
> > +static int zswap_frontswap_store(unsigned type, pgoff_t offset,
> > + struct page *page)
> > +{
> > + struct zswap_tree *tree = zswap_trees[type];
> > + struct zswap_entry *entry, *dupentry;
> > + int ret;
> > + unsigned int dlen = PAGE_SIZE, len;
> > + unsigned long handle;
> > + char *buf;
> > + u8 *src, *dst;
> > + struct zswap_header *zhdr;
> > +
> > + if (!tree) {
> > + ret = -ENODEV;
> > + goto reject;
> > + }
> > +
> > + /* reclaim space if needed */
> > + if (zswap_is_full()) {
> > + zswap_pool_limit_hit++;
> > + if (zbud_reclaim_page(tree->pool, 8)) {
>
> My idea is to wake up a kernel thread here to do the reclaim.
> Once zswap is full(20% percent of total mem currently), the kernel
> thread should reclaim pages from it. Not only reclaim one page, it
> should depend on the current memory pressure.
> And then the API in zbud may like this:
> zbud_reclaim_page(pool, nr_pages_to_reclaim, nr_retry);

So kswapd for zswap. I'm not opposed to the idea if a case can be
made for the complexity. I must say, I don't see that case though.

The policy can evolve as deficiencies are demonstrated and solutions are
found. Can I get your ack on this pending the other changes?

Thanks,
Seth

2013-05-14 16:36:20

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Mon, May 13, 2013 at 03:31:42PM -0700, Dan Magenheimer wrote:
> > From: Seth Jennings [mailto:[email protected]]
> > Subject: [PATCHv11 3/4] zswap: add to mm/
> >
> > zswap is a thin compression backend for frontswap. It receives pages from
> > frontswap and attempts to store them in a compressed memory pool, resulting in
> > an effective partial memory reclaim and dramatically reduced swap device I/O.
> >
> > Additionally, in most cases, pages can be retrieved from this compressed store
> > much more quickly than reading from tradition swap devices resulting in faster
> > performance for many workloads.
> >
> > It also has support for evicting swap pages that are currently compressed in
> > zswap to the swap device on an LRU(ish) basis. This functionality is very
> > important and make zswap a true cache in that, once the cache is full or can't
> > grow due to memory pressure, the oldest pages can be moved out of zswap to the
> > swap device so newer pages can be compressed and stored in zswap.
> >
> > This patch adds the zswap driver to mm/
> >
> > Signed-off-by: Seth Jennings <[email protected]>
>
> A couple of comments below...

Thanks for the review!

>
> > ---
> > mm/Kconfig | 15 +
> > mm/Makefile | 1 +
> > mm/zswap.c | 952 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
> > 3 files changed, 968 insertions(+)
> > create mode 100644 mm/zswap.c
> >
> > diff --git a/mm/Kconfig b/mm/Kconfig
> > index 908f41b..4042e07 100644
> > --- a/mm/Kconfig
> > +++ b/mm/Kconfig
> > @@ -487,3 +487,18 @@ config ZBUD
> > While this design limits storage density, it has simple and
> > deterministic reclaim properties that make it preferable to a higher
> > density approach when reclaim will be used.
> > +
> > +config ZSWAP
> > + bool "In-kernel swap page compression"
> > + depends on FRONTSWAP && CRYPTO
> > + select CRYPTO_LZO
> > + select ZBUD
> > + default n
> > + help
> > + Zswap is a backend for the frontswap mechanism in the VMM.
> > + It receives pages from frontswap and attempts to store them
> > + in a compressed memory pool, resulting in an effective
> > + partial memory reclaim. In addition, pages and be retrieved
> > + from this compressed store much faster than most tradition
> > + swap devices resulting in reduced I/O and faster performance
> > + for many workloads.
> > diff --git a/mm/Makefile b/mm/Makefile
> > index 95f0197..f008033 100644
> > --- a/mm/Makefile
> > +++ b/mm/Makefile
> > @@ -32,6 +32,7 @@ obj-$(CONFIG_HAVE_MEMBLOCK) += memblock.o
> > obj-$(CONFIG_BOUNCE) += bounce.o
> > obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
> > obj-$(CONFIG_FRONTSWAP) += frontswap.o
> > +obj-$(CONFIG_ZSWAP) += zswap.o
> > obj-$(CONFIG_HAS_DMA) += dmapool.o
> > obj-$(CONFIG_HUGETLBFS) += hugetlb.o
> > obj-$(CONFIG_NUMA) += mempolicy.o
> > diff --git a/mm/zswap.c b/mm/zswap.c
> > new file mode 100644
> > index 0000000..b1070ca
> > --- /dev/null
> > +++ b/mm/zswap.c
> > @@ -0,0 +1,952 @@
> > +/*
> > + * zswap.c - zswap driver file
> > + *
> > + * zswap is a backend for frontswap that takes pages that are in the
> > + * process of being swapped out and attempts to compress them and store
> > + * them in a RAM-based memory pool. This results in a significant I/O
> > + * reduction on the real swap device and, in the case of a slow swap
> > + * device, can also improve workload performance.
> > + *
> > + * Copyright (C) 2012 Seth Jennings <[email protected]>
> > + *
> > + * This program is free software; you can redistribute it and/or
> > + * modify it under the terms of the GNU General Public License
> > + * as published by the Free Software Foundation; either version 2
> > + * of the License, or (at your option) any later version.
> > + *
> > + * This program is distributed in the hope that it will be useful,
> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> > + * GNU General Public License for more details.
> > +*/
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/module.h>
> > +#include <linux/cpu.h>
> > +#include <linux/highmem.h>
> > +#include <linux/slab.h>
> > +#include <linux/spinlock.h>
> > +#include <linux/types.h>
> > +#include <linux/atomic.h>
> > +#include <linux/frontswap.h>
> > +#include <linux/rbtree.h>
> > +#include <linux/swap.h>
> > +#include <linux/crypto.h>
> > +#include <linux/mempool.h>
> > +#include <linux/zbud.h>
> > +
> > +#include <linux/mm_types.h>
> > +#include <linux/page-flags.h>
> > +#include <linux/swapops.h>
> > +#include <linux/writeback.h>
> > +#include <linux/pagemap.h>
> > +
> > +/*********************************
> > +* statistics
> > +**********************************/
> > +/* Number of memory pages used by the compressed pool */
> > +static atomic_t zswap_pool_pages = ATOMIC_INIT(0);
> > +/* The number of compressed pages currently stored in zswap */
> > +static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
> > +
> > +/*
> > + * The statistics below are not protected from concurrent access for
> > + * performance reasons so they may not be a 100% accurate. However,
> > + * they do provide useful information on roughly how many times a
> > + * certain event is occurring.
> > +*/
> > +static u64 zswap_pool_limit_hit;
> > +static u64 zswap_written_back_pages;
> > +static u64 zswap_reject_reclaim_fail;
> > +static u64 zswap_reject_compress_poor;
> > +static u64 zswap_reject_alloc_fail;
> > +static u64 zswap_reject_kmemcache_fail;
> > +static u64 zswap_duplicate_entry;
> > +
> > +/*********************************
> > +* tunables
> > +**********************************/
> > +/* Enable/disable zswap (disabled by default, fixed at boot for now) */
> > +static bool zswap_enabled;
> > +module_param_named(enabled, zswap_enabled, bool, 0);
> > +
> > +/* Compressor to be used by zswap (fixed at boot for now) */
> > +#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
> > +static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> > +module_param_named(compressor, zswap_compressor, charp, 0);
> > +
> > +/* The maximum percentage of memory that the compressed pool can occupy */
> > +static unsigned int zswap_max_pool_percent = 20;
> > +module_param_named(max_pool_percent,
> > + zswap_max_pool_percent, uint, 0644);
>
> This limit, along with the code that enforces it (by calling reclaim
> when the limit is reached), is IMHO questionable. Is there any
> other kernel memory allocation that is constrained by a percentage
> of total memory rather than dynamically according to current
> system conditions? As Mel pointed out (approx.), if this limit
> is reached by a zswap-storm and filled with pages of long-running,
> rarely-used processes, 20% of RAM (by default here) becomes forever
> clogged.

So there are two comments here 1) dynamic pool limit and 2) writeback
of pages in zswap that won't be faulted in or forced out by pressure.

Comment 1 feeds from the point of view that compressed pages should just be
another type of memory managed by the core MM. While ideal, very hard to
implement in practice. We are starting to realize that even the policy
governing to active vs inactive list is very hard to get right. Then shrinkers
add more complexity to the policy problem. Throwing another type in the mix
would just that much more complex and hard to get right (assuming there even
_is_ a "right" policy for everyone in such a complex system).

This max_pool_percent policy is simple, works well, and provides a
deterministic policy that users can understand. Users can be assured that a
dynamic policy heuristic won't go nuts and allow the compressed pool to grow
unbounded or be so aggressively reclaimed that it offers no value.

Comment 2 I agree is an issue. I already have patches for a "periodic
writeback" functionality that starts to shrink the zswap pool via
writeback if zswap goes idle for a period of time. This addresses
the issue with long-lived, never-accessed pages getting stuck in
zswap forever.

>
> Zswap reclaim/writeback needs to be cognizant of (and perhaps driven
> by) system memory pressure, not some user-settable percentage.
> There's some tough policy questions that need to be answered here,
> perhaps not before zswap gets merged, but certainly before it
> gets enabled by default by distros.

Agreed that it shouldn't block merging. I guess the distros will
have to make the call if the policy is good enough.

>
> > +/*
> > + * Maximum compression ratio, as as percentage, for an acceptable
> > + * compressed page. Any pages that do not compress by at least
> > + * this ratio will be rejected.
> > +*/
> > +static unsigned int zswap_max_compression_ratio = 80;
> > +module_param_named(max_compression_ratio,
> > + zswap_max_compression_ratio, uint, 0644);
>
> Per earlier discussion, this number is actually derived
> from a zsmalloc constraint and doesn't necessarily apply
> to zbud. And I don't think any mortal user or system
> administrator would have any idea what value to change
> this to or the potential impact of changing it. IMHO
> it should be removed, or at least moved to and enforced
> by the specific allocator code.

Yes, Bob pointed this out too. I'm removing it.

Can I get your ack pending this change?

Thanks,
Seth

>
> > +/*********************************
> > +* compression functions
> > +**********************************/
> > +/* per-cpu compression transforms */
> > +static struct crypto_comp * __percpu *zswap_comp_pcpu_tfms;
> > +
> > +enum comp_op {
> > + ZSWAP_COMPOP_COMPRESS,
> > + ZSWAP_COMPOP_DECOMPRESS
> > +};
> > +
> > +static int zswap_comp_op(enum comp_op op, const u8 *src, unsigned int slen,
> > + u8 *dst, unsigned int *dlen)
> > +{
> > + struct crypto_comp *tfm;
> > + int ret;
> > +
> > + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, get_cpu());
> > + switch (op) {
> > + case ZSWAP_COMPOP_COMPRESS:
> > + ret = crypto_comp_compress(tfm, src, slen, dst, dlen);
> > + break;
> > + case ZSWAP_COMPOP_DECOMPRESS:
> > + ret = crypto_comp_decompress(tfm, src, slen, dst, dlen);
> > + break;
> > + default:
> > + ret = -EINVAL;
> > + }
> > +
> > + put_cpu();
> > + return ret;
> > +}
> > +
> > +static int __init zswap_comp_init(void)
> > +{
> > + if (!crypto_has_comp(zswap_compressor, 0, 0)) {
> > + pr_info("%s compressor not available\n", zswap_compressor);
> > + /* fall back to default compressor */
> > + zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> > + if (!crypto_has_comp(zswap_compressor, 0, 0))
> > + /* can't even load the default compressor */
> > + return -ENODEV;
> > + }
> > + pr_info("using %s compressor\n", zswap_compressor);
> > +
> > + /* alloc percpu transforms */
> > + zswap_comp_pcpu_tfms = alloc_percpu(struct crypto_comp *);
> > + if (!zswap_comp_pcpu_tfms)
> > + return -ENOMEM;
> > + return 0;
> > +}
> > +
> > +static void zswap_comp_exit(void)
> > +{
> > + /* free percpu transforms */
> > + if (zswap_comp_pcpu_tfms)
> > + free_percpu(zswap_comp_pcpu_tfms);
> > +}
> > +
> > +/*********************************
> > +* data structures
> > +**********************************/
> > +/*
> > + * struct zswap_entry
> > + *
> > + * This structure contains the metadata for tracking a single compressed
> > + * page within zswap.
> > + *
> > + * rbnode - links the entry into red-black tree for the appropriate swap type
> > + * refcount - the number of outstanding reference to the entry. This is needed
> > + * to protect against premature freeing of the entry by code
> > + * concurent calls to load, invalidate, and writeback. The lock
> > + * for the zswap_tree structure that contains the entry must
> > + * be held while changing the refcount. Since the lock must
> > + * be held, there is no reason to also make refcount atomic.
> > + * type - the swap type for the entry. Used to map back to the zswap_tree
> > + * structure that contains the entry.
> > + * offset - the swap offset for the entry. Index into the red-black tree.
> > + * handle - zsmalloc allocation handle that stores the compressed page data
> > + * length - the length in bytes of the compressed page data. Needed during
> > + * decompression
> > + */
> > +struct zswap_entry {
> > + struct rb_node rbnode;
> > + pgoff_t offset;
> > + int refcount;
> > + unsigned int length;
> > + unsigned long handle;
> > +};
> > +
> > +struct zswap_header {
> > + swp_entry_t swpentry;
> > +};
> > +
> > +/*
> > + * The tree lock in the zswap_tree struct protects a few things:
> > + * - the rbtree
> > + * - the refcount field of each entry in the tree
> > + */
> > +struct zswap_tree {
> > + struct rb_root rbroot;
> > + spinlock_t lock;
> > + struct zbud_pool *pool;
> > + unsigned type;
> > +};
> > +
> > +static struct zswap_tree *zswap_trees[MAX_SWAPFILES];
> > +
> > +/*********************************
> > +* zswap entry functions
> > +**********************************/
> > +#define ZSWAP_KMEM_CACHE_NAME "zswap_entry_cache"
> > +static struct kmem_cache *zswap_entry_cache;
> > +
> > +static inline int zswap_entry_cache_create(void)
> > +{
> > + zswap_entry_cache =
> > + kmem_cache_create(ZSWAP_KMEM_CACHE_NAME,
> > + sizeof(struct zswap_entry), 0, 0, NULL);
> > + return (zswap_entry_cache == NULL);
> > +}
> > +
> > +static inline void zswap_entry_cache_destory(void)
> > +{
> > + kmem_cache_destroy(zswap_entry_cache);
> > +}
> > +
> > +static inline struct zswap_entry *zswap_entry_cache_alloc(gfp_t gfp)
> > +{
> > + struct zswap_entry *entry;
> > + entry = kmem_cache_alloc(zswap_entry_cache, gfp);
> > + if (!entry)
> > + return NULL;
> > + entry->refcount = 1;
> > + return entry;
> > +}
> > +
> > +static inline void zswap_entry_cache_free(struct zswap_entry *entry)
> > +{
> > + kmem_cache_free(zswap_entry_cache, entry);
> > +}
> > +
> > +static inline void zswap_entry_get(struct zswap_entry *entry)
> > +{
> > + entry->refcount++;
> > +}
> > +
> > +static inline int zswap_entry_put(struct zswap_entry *entry)
> > +{
> > + entry->refcount--;
> > + return entry->refcount;
> > +}
> > +
> > +/*********************************
> > +* rbtree functions
> > +**********************************/
> > +static struct zswap_entry *zswap_rb_search(struct rb_root *root, pgoff_t offset)
> > +{
> > + struct rb_node *node = root->rb_node;
> > + struct zswap_entry *entry;
> > +
> > + while (node) {
> > + entry = rb_entry(node, struct zswap_entry, rbnode);
> > + if (entry->offset > offset)
> > + node = node->rb_left;
> > + else if (entry->offset < offset)
> > + node = node->rb_right;
> > + else
> > + return entry;
> > + }
> > + return NULL;
> > +}
> > +
> > +/*
> > + * In the case that a entry with the same offset is found, it a pointer to
> > + * the existing entry is stored in dupentry and the function returns -EEXIST
> > +*/
> > +static int zswap_rb_insert(struct rb_root *root, struct zswap_entry *entry,
> > + struct zswap_entry **dupentry)
> > +{
> > + struct rb_node **link = &root->rb_node, *parent = NULL;
> > + struct zswap_entry *myentry;
> > +
> > + while (*link) {
> > + parent = *link;
> > + myentry = rb_entry(parent, struct zswap_entry, rbnode);
> > + if (myentry->offset > entry->offset)
> > + link = &(*link)->rb_left;
> > + else if (myentry->offset < entry->offset)
> > + link = &(*link)->rb_right;
> > + else {
> > + *dupentry = myentry;
> > + return -EEXIST;
> > + }
> > + }
> > + rb_link_node(&entry->rbnode, parent, link);
> > + rb_insert_color(&entry->rbnode, root);
> > + return 0;
> > +}
> > +
> > +/*********************************
> > +* per-cpu code
> > +**********************************/
> > +static DEFINE_PER_CPU(u8 *, zswap_dstmem);
> > +
> > +static int __zswap_cpu_notifier(unsigned long action, unsigned long cpu)
> > +{
> > + struct crypto_comp *tfm;
> > + u8 *dst;
> > +
> > + switch (action) {
> > + case CPU_UP_PREPARE:
> > + tfm = crypto_alloc_comp(zswap_compressor, 0, 0);
> > + if (IS_ERR(tfm)) {
> > + pr_err("can't allocate compressor transform\n");
> > + return NOTIFY_BAD;
> > + }
> > + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = tfm;
> > + dst = kmalloc(PAGE_SIZE * 2, GFP_KERNEL);
> > + if (!dst) {
> > + pr_err("can't allocate compressor buffer\n");
> > + crypto_free_comp(tfm);
> > + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> > + return NOTIFY_BAD;
> > + }
> > + per_cpu(zswap_dstmem, cpu) = dst;
> > + break;
> > + case CPU_DEAD:
> > + case CPU_UP_CANCELED:
> > + tfm = *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu);
> > + if (tfm) {
> > + crypto_free_comp(tfm);
> > + *per_cpu_ptr(zswap_comp_pcpu_tfms, cpu) = NULL;
> > + }
> > + dst = per_cpu(zswap_dstmem, cpu);
> > + kfree(dst);
> > + per_cpu(zswap_dstmem, cpu) = NULL;
> > + break;
> > + default:
> > + break;
> > + }
> > + return NOTIFY_OK;
> > +}
> > +
> > +static int zswap_cpu_notifier(struct notifier_block *nb,
> > + unsigned long action, void *pcpu)
> > +{
> > + unsigned long cpu = (unsigned long)pcpu;
> > + return __zswap_cpu_notifier(action, cpu);
> > +}
> > +
> > +static struct notifier_block zswap_cpu_notifier_block = {
> > + .notifier_call = zswap_cpu_notifier
> > +};
> > +
> > +static int zswap_cpu_init(void)
> > +{
> > + unsigned long cpu;
> > +
> > + get_online_cpus();
> > + for_each_online_cpu(cpu)
> > + if (__zswap_cpu_notifier(CPU_UP_PREPARE, cpu) != NOTIFY_OK)
> > + goto cleanup;
> > + register_cpu_notifier(&zswap_cpu_notifier_block);
> > + put_online_cpus();
> > + return 0;
> > +
> > +cleanup:
> > + for_each_online_cpu(cpu)
> > + __zswap_cpu_notifier(CPU_UP_CANCELED, cpu);
> > + put_online_cpus();
> > + return -ENOMEM;
> > +}
> > +
> > +/*********************************
> > +* helpers
> > +**********************************/
> > +static inline bool zswap_is_full(void)
> > +{
> > + int pool_pages = atomic_read(&zswap_pool_pages);
> > + return (totalram_pages * zswap_max_pool_percent / 100 < pool_pages);
> > +}
> > +
> > +/*
> > + * Carries out the common pattern of freeing and entry's zsmalloc allocation,
> > + * freeing the entry itself, and decrementing the number of stored pages.
> > + */
> > +static void zswap_free_entry(struct zswap_tree *tree, struct zswap_entry *entry)
> > +{
> > + zbud_free(tree->pool, entry->handle);
> > + zswap_entry_cache_free(entry);
> > + atomic_dec(&zswap_stored_pages);
> > + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> > +}
> > +
> > +/*********************************
> > +* writeback code
> > +**********************************/
> > +/* return enum for zswap_get_swap_cache_page */
> > +enum zswap_get_swap_ret {
> > + ZSWAP_SWAPCACHE_NEW,
> > + ZSWAP_SWAPCACHE_EXIST,
> > + ZSWAP_SWAPCACHE_NOMEM
> > +};
> > +
> > +/*
> > + * zswap_get_swap_cache_page
> > + *
> > + * This is an adaption of read_swap_cache_async()
> > + *
> > + * This function tries to find a page with the given swap entry
> > + * in the swapper_space address space (the swap cache). If the page
> > + * is found, it is returned in retpage. Otherwise, a page is allocated,
> > + * added to the swap cache, and returned in retpage.
> > + *
> > + * If success, the swap cache page is returned in retpage
> > + * Returns 0 if page was already in the swap cache, page is not locked
> > + * Returns 1 if the new page needs to be populated, page is locked
> > + * Returns <0 on error
> > + */
> > +static int zswap_get_swap_cache_page(swp_entry_t entry,
> > + struct page **retpage)
> > +{
> > + struct page *found_page, *new_page = NULL;
> > + struct address_space *swapper_space = &swapper_spaces[swp_type(entry)];
> > + int err;
> > +
> > + *retpage = NULL;
> > + do {
> > + /*
> > + * First check the swap cache. Since this is normally
> > + * called after lookup_swap_cache() failed, re-calling
> > + * that would confuse statistics.
> > + */
> > + found_page = find_get_page(swapper_space, entry.val);
> > + if (found_page)
> > + break;
> > +
> > + /*
> > + * Get a new page to read into from swap.
> > + */
> > + if (!new_page) {
> > + new_page = alloc_page(GFP_KERNEL);
> > + if (!new_page)
> > + break; /* Out of memory */
> > + }
> > +
> > + /*
> > + * call radix_tree_preload() while we can wait.
> > + */
> > + err = radix_tree_preload(GFP_KERNEL);
> > + if (err)
> > + break;
> > +
> > + /*
> > + * Swap entry may have been freed since our caller observed it.
> > + */
> > + err = swapcache_prepare(entry);
> > + if (err == -EEXIST) { /* seems racy */
> > + radix_tree_preload_end();
> > + continue;
> > + }
> > + if (err) { /* swp entry is obsolete ? */
> > + radix_tree_preload_end();
> > + break;
> > + }
> > +
> > + /* May fail (-ENOMEM) if radix-tree node allocation failed. */
> > + __set_page_locked(new_page);
> > + SetPageSwapBacked(new_page);
> > + err = __add_to_swap_cache(new_page, entry);
> > + if (likely(!err)) {
> > + radix_tree_preload_end();
> > + lru_cache_add_anon(new_page);
> > + *retpage = new_page;
> > + return ZSWAP_SWAPCACHE_NEW;
> > + }
> > + radix_tree_preload_end();
> > + ClearPageSwapBacked(new_page);
> > + __clear_page_locked(new_page);
> > + /*
> > + * add_to_swap_cache() doesn't return -EEXIST, so we can safely
> > + * clear SWAP_HAS_CACHE flag.
> > + */
> > + swapcache_free(entry, NULL);
> > + } while (err != -ENOMEM);
> > +
> > + if (new_page)
> > + page_cache_release(new_page);
> > + if (!found_page)
> > + return ZSWAP_SWAPCACHE_NOMEM;
> > + *retpage = found_page;
> > + return ZSWAP_SWAPCACHE_EXIST;
> > +}
> > +
> > +/*
> > + * Attempts to free and entry by adding a page to the swap cache,
> > + * decompressing the entry data into the page, and issuing a
> > + * bio write to write the page back to the swap device.
> > + *
> > + * This can be thought of as a "resumed writeback" of the page
> > + * to the swap device. We are basically resuming the same swap
> > + * writeback path that was intercepted with the frontswap_store()
> > + * in the first place. After the page has been decompressed into
> > + * the swap cache, the compressed version stored by zswap can be
> > + * freed.
> > + */
> > +static int zswap_writeback_entry(struct zbud_pool *pool, unsigned long handle)
> > +{
> > + struct zswap_header *zhdr;
> > + swp_entry_t swpentry;
> > + struct zswap_tree *tree;
> > + pgoff_t offset;
> > + struct zswap_entry *entry;
> > + struct page *page;
> > + u8 *src, *dst;
> > + unsigned int dlen;
> > + int ret, refcount;
> > + struct writeback_control wbc = {
> > + .sync_mode = WB_SYNC_NONE,
> > + };
> > +
> > + /* extract swpentry from data */
> > + zhdr = zbud_map(pool, handle);
> > + swpentry = zhdr->swpentry; /* here */
> > + zbud_unmap(pool, handle);
> > + tree = zswap_trees[swp_type(swpentry)];
> > + offset = swp_offset(swpentry);
> > + BUG_ON(pool != tree->pool);
> > +
> > + /* find and ref zswap entry */
> > + spin_lock(&tree->lock);
> > + entry = zswap_rb_search(&tree->rbroot, offset);
> > + if (!entry) {
> > + /* entry was invalidated */
> > + spin_unlock(&tree->lock);
> > + return 0;
> > + }
> > + zswap_entry_get(entry);
> > + spin_unlock(&tree->lock);
> > + BUG_ON(offset != entry->offset);
> > +
> > + /* try to allocate swap cache page */
> > + switch (zswap_get_swap_cache_page(swpentry, &page)) {
> > + case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
> > + ret = -ENOMEM;
> > + goto fail;
> > +
> > + case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
> > + /* page is already in the swap cache, ignore for now */
> > + page_cache_release(page);
> > + ret = -EEXIST;
> > + goto fail;
> > +
> > + case ZSWAP_SWAPCACHE_NEW: /* page is locked */
> > + /* decompress */
> > + dlen = PAGE_SIZE;
> > + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> > + sizeof(struct zswap_header);
> > + dst = kmap_atomic(page);
> > + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
> > + entry->length, dst, &dlen);
> > + kunmap_atomic(dst);
> > + zbud_unmap(tree->pool, entry->handle);
> > + BUG_ON(ret);
> > + BUG_ON(dlen != PAGE_SIZE);
> > +
> > + /* page is up to date */
> > + SetPageUptodate(page);
> > + }
> > +
> > + /* start writeback */
> > + SetPageReclaim(page);
> > + __swap_writepage(page, &wbc, end_swap_bio_write);
> > + page_cache_release(page);
> > + zswap_written_back_pages++;
> > +
> > + spin_lock(&tree->lock);
> > +
> > + /* drop local reference */
> > + zswap_entry_put(entry);
> > + /* drop the initial reference from entry creation */
> > + refcount = zswap_entry_put(entry);
> > +
> > + /*
> > + * There are three possible values for refcount here:
> > + * (1) refcount is 1, load is in progress, unlink from rbtree,
> > + * load will free
> > + * (2) refcount is 0, (normal case) entry is valid,
> > + * remove from rbtree and free entry
> > + * (3) refcount is -1, invalidate happened during writeback,
> > + * free entry
> > + */
> > + if (refcount >= 0) {
> > + /* no invalidate yet, remove from rbtree */
> > + rb_erase(&entry->rbnode, &tree->rbroot);
> > + }
> > + spin_unlock(&tree->lock);
> > + if (refcount <= 0) {
> > + /* free the entry */
> > + zswap_free_entry(tree, entry);
> > + return 0;
> > + }
> > + return -EAGAIN;
> > +
> > +fail:
> > + spin_lock(&tree->lock);
> > + zswap_entry_put(entry);
> > + spin_unlock(&tree->lock);
> > + return ret;
> > +}
> > +
> > +/*********************************
> > +* frontswap hooks
> > +**********************************/
> > +/* attempts to compress and store an single page */
> > +static int zswap_frontswap_store(unsigned type, pgoff_t offset,
> > + struct page *page)
> > +{
> > + struct zswap_tree *tree = zswap_trees[type];
> > + struct zswap_entry *entry, *dupentry;
> > + int ret;
> > + unsigned int dlen = PAGE_SIZE, len;
> > + unsigned long handle;
> > + char *buf;
> > + u8 *src, *dst;
> > + struct zswap_header *zhdr;
> > +
> > + if (!tree) {
> > + ret = -ENODEV;
> > + goto reject;
> > + }
> > +
> > + /* reclaim space if needed */
> > + if (zswap_is_full()) {
> > + zswap_pool_limit_hit++;
> > + if (zbud_reclaim_page(tree->pool, 8)) {
> > + zswap_reject_reclaim_fail++;
> > + ret = -ENOMEM;
> > + goto reject;
> > + }
> > + }
>
> See comment above about enforcing "full".
>
> (No further comments below... Thanks, Dan)
>
> > + /* allocate entry */
> > + entry = zswap_entry_cache_alloc(GFP_KERNEL);
> > + if (!entry) {
> > + zswap_reject_kmemcache_fail++;
> > + ret = -ENOMEM;
> > + goto reject;
> > + }
> > +
> > + /* compress */
> > + dst = get_cpu_var(zswap_dstmem);
> > + src = kmap_atomic(page);
> > + ret = zswap_comp_op(ZSWAP_COMPOP_COMPRESS, src, PAGE_SIZE, dst, &dlen);
> > + kunmap_atomic(src);
> > + if (ret) {
> > + ret = -EINVAL;
> > + goto freepage;
> > + }
> > + len = dlen + sizeof(struct zswap_header);
> > + if ((len * 100 / PAGE_SIZE) > zswap_max_compression_ratio) {
> > + zswap_reject_compress_poor++;
> > + ret = -E2BIG;
> > + goto freepage;
> > + }
> > +
> > + /* store */
> > + ret = zbud_alloc(tree->pool, len, __GFP_NORETRY | __GFP_NOWARN,
> > + &handle);
> > + if (ret) {
> > + zswap_reject_alloc_fail++;
> > + goto freepage;
> > + }
> > + zhdr = zbud_map(tree->pool, handle);
> > + zhdr->swpentry = swp_entry(type, offset);
> > + buf = (u8 *)(zhdr + 1);
> > + memcpy(buf, dst, dlen);
> > + zbud_unmap(tree->pool, handle);
> > + put_cpu_var(zswap_dstmem);
> > +
> > + /* populate entry */
> > + entry->offset = offset;
> > + entry->handle = handle;
> > + entry->length = dlen;
> > +
> > + /* map */
> > + spin_lock(&tree->lock);
> > + do {
> > + ret = zswap_rb_insert(&tree->rbroot, entry, &dupentry);
> > + if (ret == -EEXIST) {
> > + zswap_duplicate_entry++;
> > + /* remove from rbtree */
> > + rb_erase(&dupentry->rbnode, &tree->rbroot);
> > + if (!zswap_entry_put(dupentry)) {
> > + /* free */
> > + zswap_free_entry(tree, dupentry);
> > + }
> > + }
> > + } while (ret == -EEXIST);
> > + spin_unlock(&tree->lock);
> > +
> > + /* update stats */
> > + atomic_inc(&zswap_stored_pages);
> > + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> > +
> > + return 0;
> > +
> > +freepage:
> > + put_cpu_var(zswap_dstmem);
> > + zswap_entry_cache_free(entry);
> > +reject:
> > + return ret;
> > +}
> > +
> > +/*
> > + * returns 0 if the page was successfully decompressed
> > + * return -1 on entry not found or error
> > +*/
> > +static int zswap_frontswap_load(unsigned type, pgoff_t offset,
> > + struct page *page)
> > +{
> > + struct zswap_tree *tree = zswap_trees[type];
> > + struct zswap_entry *entry;
> > + u8 *src, *dst;
> > + unsigned int dlen;
> > + int refcount, ret;
> > +
> > + /* find */
> > + spin_lock(&tree->lock);
> > + entry = zswap_rb_search(&tree->rbroot, offset);
> > + if (!entry) {
> > + /* entry was written back */
> > + spin_unlock(&tree->lock);
> > + return -1;
> > + }
> > + zswap_entry_get(entry);
> > + spin_unlock(&tree->lock);
> > +
> > + /* decompress */
> > + dlen = PAGE_SIZE;
> > + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> > + sizeof(struct zswap_header);
> > + dst = kmap_atomic(page);
> > + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src, entry->length,
> > + dst, &dlen);
> > + kunmap_atomic(dst);
> > + zbud_unmap(tree->pool, entry->handle);
> > + BUG_ON(ret);
> > +
> > + spin_lock(&tree->lock);
> > + refcount = zswap_entry_put(entry);
> > + if (likely(refcount)) {
> > + spin_unlock(&tree->lock);
> > + return 0;
> > + }
> > + spin_unlock(&tree->lock);
> > +
> > + /*
> > + * We don't have to unlink from the rbtree because
> > + * zswap_writeback_entry() or zswap_frontswap_invalidate page()
> > + * has already done this for us if we are the last reference.
> > + */
> > + /* free */
> > +
> > + zswap_free_entry(tree, entry);
> > +
> > + return 0;
> > +}
> > +
> > +/* invalidates a single page */
> > +static void zswap_frontswap_invalidate_page(unsigned type, pgoff_t offset)
> > +{
> > + struct zswap_tree *tree = zswap_trees[type];
> > + struct zswap_entry *entry;
> > + int refcount;
> > +
> > + /* find */
> > + spin_lock(&tree->lock);
> > + entry = zswap_rb_search(&tree->rbroot, offset);
> > + if (!entry) {
> > + /* entry was written back */
> > + spin_unlock(&tree->lock);
> > + return;
> > + }
> > +
> > + /* remove from rbtree */
> > + rb_erase(&entry->rbnode, &tree->rbroot);
> > +
> > + /* drop the initial reference from entry creation */
> > + refcount = zswap_entry_put(entry);
> > +
> > + spin_unlock(&tree->lock);
> > +
> > + if (refcount) {
> > + /* writeback in progress, writeback will free */
> > + return;
> > + }
> > +
> > + /* free */
> > + zswap_free_entry(tree, entry);
> > +}
> > +
> > +/* invalidates all pages for the given swap type */
> > +static void zswap_frontswap_invalidate_area(unsigned type)
> > +{
> > + struct zswap_tree *tree = zswap_trees[type];
> > + struct rb_node *node;
> > + struct zswap_entry *entry;
> > +
> > + if (!tree)
> > + return;
> > +
> > + /* walk the tree and free everything */
> > + spin_lock(&tree->lock);
> > + /*
> > + * TODO: Even though this code should not be executed because
> > + * the try_to_unuse() in swapoff should have emptied the tree,
> > + * it is very wasteful to rebalance the tree after every
> > + * removal when we are freeing the whole tree.
> > + *
> > + * If post-order traversal code is ever added to the rbtree
> > + * implementation, it should be used here.
> > + */
> > + while ((node = rb_first(&tree->rbroot))) {
> > + entry = rb_entry(node, struct zswap_entry, rbnode);
> > + rb_erase(&entry->rbnode, &tree->rbroot);
> > + zbud_free(tree->pool, entry->handle);
> > + zswap_entry_cache_free(entry);
> > + atomic_dec(&zswap_stored_pages);
> > + }
> > + tree->rbroot = RB_ROOT;
> > + spin_unlock(&tree->lock);
> > +}
> > +
> > +static struct zbud_ops zswap_zbud_ops = {
> > + .evict = zswap_writeback_entry
> > +};
> > +
> > +/* NOTE: this is called in atomic context from swapon and must not sleep */
> > +static void zswap_frontswap_init(unsigned type)
> > +{
> > + struct zswap_tree *tree;
> > +
> > + tree = kzalloc(sizeof(struct zswap_tree), GFP_ATOMIC);
> > + if (!tree)
> > + goto err;
> > + tree->pool = zbud_create_pool(GFP_NOWAIT, &zswap_zbud_ops);
> > + if (!tree->pool)
> > + goto freetree;
> > + tree->rbroot = RB_ROOT;
> > + spin_lock_init(&tree->lock);
> > + tree->type = type;
> > + zswap_trees[type] = tree;
> > + return;
> > +
> > +freetree:
> > + kfree(tree);
> > +err:
> > + pr_err("alloc failed, zswap disabled for swap type %d\n", type);
> > +}
> > +
> > +static struct frontswap_ops zswap_frontswap_ops = {
> > + .store = zswap_frontswap_store,
> > + .load = zswap_frontswap_load,
> > + .invalidate_page = zswap_frontswap_invalidate_page,
> > + .invalidate_area = zswap_frontswap_invalidate_area,
> > + .init = zswap_frontswap_init
> > +};
> > +
> > +/*********************************
> > +* debugfs functions
> > +**********************************/
> > +#ifdef CONFIG_DEBUG_FS
> > +#include <linux/debugfs.h>
> > +
> > +static struct dentry *zswap_debugfs_root;
> > +
> > +static int __init zswap_debugfs_init(void)
> > +{
> > + if (!debugfs_initialized())
> > + return -ENODEV;
> > +
> > + zswap_debugfs_root = debugfs_create_dir("zswap", NULL);
> > + if (!zswap_debugfs_root)
> > + return -ENOMEM;
> > +
> > + debugfs_create_u64("pool_limit_hit", S_IRUGO,
> > + zswap_debugfs_root, &zswap_pool_limit_hit);
> > + debugfs_create_u64("reject_reclaim_fail", S_IRUGO,
> > + zswap_debugfs_root, &zswap_reject_reclaim_fail);
> > + debugfs_create_u64("reject_alloc_fail", S_IRUGO,
> > + zswap_debugfs_root, &zswap_reject_alloc_fail);
> > + debugfs_create_u64("reject_kmemcache_fail", S_IRUGO,
> > + zswap_debugfs_root, &zswap_reject_kmemcache_fail);
> > + debugfs_create_u64("reject_compress_poor", S_IRUGO,
> > + zswap_debugfs_root, &zswap_reject_compress_poor);
> > + debugfs_create_u64("written_back_pages", S_IRUGO,
> > + zswap_debugfs_root, &zswap_written_back_pages);
> > + debugfs_create_u64("duplicate_entry", S_IRUGO,
> > + zswap_debugfs_root, &zswap_duplicate_entry);
> > + debugfs_create_atomic_t("pool_pages", S_IRUGO,
> > + zswap_debugfs_root, &zswap_pool_pages);
> > + debugfs_create_atomic_t("stored_pages", S_IRUGO,
> > + zswap_debugfs_root, &zswap_stored_pages);
> > +
> > + return 0;
> > +}
> > +
> > +static void __exit zswap_debugfs_exit(void)
> > +{
> > + debugfs_remove_recursive(zswap_debugfs_root);
> > +}
> > +#else
> > +static inline int __init zswap_debugfs_init(void)
> > +{
> > + return 0;
> > +}
> > +
> > +static inline void __exit zswap_debugfs_exit(void) { }
> > +#endif
> > +
> > +/*********************************
> > +* module init and exit
> > +**********************************/
> > +static int __init init_zswap(void)
> > +{
> > + if (!zswap_enabled)
> > + return 0;
> > +
> > + pr_info("loading zswap\n");
> > + if (zswap_entry_cache_create()) {
> > + pr_err("entry cache creation failed\n");
> > + goto error;
> > + }
> > + if (zswap_comp_init()) {
> > + pr_err("compressor initialization failed\n");
> > + goto compfail;
> > + }
> > + if (zswap_cpu_init()) {
> > + pr_err("per-cpu initialization failed\n");
> > + goto pcpufail;
> > + }
> > + frontswap_register_ops(&zswap_frontswap_ops);
> > + if (zswap_debugfs_init())
> > + pr_warn("debugfs initialization failed\n");
> > + return 0;
> > +pcpufail:
> > + zswap_comp_exit();
> > +compfail:
> > + zswap_entry_cache_destory();
> > +error:
> > + return -ENOMEM;
> > +}
> > +/* must be late so crypto has time to come up */
> > +late_initcall(init_zswap);
> > +
> > +MODULE_LICENSE("GPL");
> > +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> > +MODULE_DESCRIPTION("Compressed cache for swap pages");
> > --
> > 1.7.9.5
>
> --
> To unsubscribe, send a message with 'unsubscribe linux-mm' in
> the body to [email protected]. For more info on Linux MM,
> see: http://www.linux-mm.org/ .
> Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
>

2013-05-14 16:40:29

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On Tue, May 14, 2013 at 05:19:19PM +0800, Bob Liu wrote:
> > Hi Seth,
>
> Hi Bob, thanks for the review!
>
> >
> > > + /* reclaim space if needed */
> > > + if (zswap_is_full()) {
> > > + zswap_pool_limit_hit++;
> > > + if (zbud_reclaim_page(tree->pool, 8)) {
> >
> > My idea is to wake up a kernel thread here to do the reclaim.
> > Once zswap is full(20% percent of total mem currently), the kernel
> > thread should reclaim pages from it. Not only reclaim one page, it
> > should depend on the current memory pressure.
> > And then the API in zbud may like this:
> > zbud_reclaim_page(pool, nr_pages_to_reclaim, nr_retry);
>
> So kswapd for zswap. I'm not opposed to the idea if a case can be
> made for the complexity. I must say, I don't see that case though.
>
> The policy can evolve as deficiencies are demonstrated and solutions are
> found.

Hmmm... it is fairly easy to demonstrate the deficiency if
one tries. I actually first saw it occur on a real (though
early) EL6 system which started some graphics-related service
that caused a very brief swapstorm that was invisible during
normal boot but clogged up RAM with compressed pages which
later caused reduced weird benchmarking performance.

I think Mel's unpredictability concern applies equally here...
this may be a "long-term source of bugs and strange memory
management behavior."

> Can I get your ack on this pending the other changes?

I'd like to hear Mel's feedback about this, but perhaps
a compromise to allow for zswap merging would be to add
something like the following to zswap's Kconfig comment:

"Zswap reclaim policy is still primitive. Until it improves,
zswap should be considered experimental and is not recommended
for production use."

If Mel agrees with the unpredictability and also agrees
with the Kconfig compromise, I am willing to ack.

2013-05-14 17:03:29

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Tue, May 14, 2013 at 04:47:24PM +0800, Bob Liu wrote:
> Hi Seth,
>
> On 05/13/2013 08:40 PM, Seth Jennings wrote:
> > zbud is an special purpose allocator for storing compressed pages. It is
> > designed to store up to two compressed pages per physical page. While this
> > design limits storage density, it has simple and deterministic reclaim
> > properties that make it preferable to a higher density approach when reclaim
> > will be used.
> >
> > zbud works by storing compressed pages, or "zpages", together in pairs in a
> > single memory page called a "zbud page". The first buddy is "left
> > justifed" at the beginning of the zbud page, and the last buddy is "right
> > justified" at the end of the zbud page. The benefit is that if either
> > buddy is freed, the freed buddy space, coalesced with whatever slack space
> > that existed between the buddies, results in the largest possible free region
> > within the zbud page.
> >
> > zbud also provides an attractive lower bound on density. The ratio of zpages
> > to zbud pages can not be less than 1. This ensures that zbud can never "do
> > harm" by using more pages to store zpages than the uncompressed zpages would
> > have used on their own.
> >
> > This patch adds zbud to mm/ for later use by zswap.
> >
> > Signed-off-by: Seth Jennings <[email protected]>
> > ---
>
> Good job! And I'm testing it!

Thanks for the review!

>
> > include/linux/zbud.h | 22 ++
> > mm/Kconfig | 10 +
> > mm/Makefile | 1 +
> > mm/zbud.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++
> > 4 files changed, 597 insertions(+)
> > create mode 100644 include/linux/zbud.h
> > create mode 100644 mm/zbud.c
> >
> > diff --git a/include/linux/zbud.h b/include/linux/zbud.h
> > new file mode 100644
> > index 0000000..954252b
> > --- /dev/null
> > +++ b/include/linux/zbud.h
> > @@ -0,0 +1,22 @@
> > +#ifndef _ZBUD_H_
> > +#define _ZBUD_H_
> > +
> > +#include <linux/types.h>
> > +
> > +struct zbud_pool;
> > +
> > +struct zbud_ops {
> > + int (*evict)(struct zbud_pool *pool, unsigned long handle);
> > +};
> > +
> > +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops);
> > +void zbud_destroy_pool(struct zbud_pool *pool);
> > +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> > + unsigned long *handle);
> > +void zbud_free(struct zbud_pool *pool, unsigned long handle);
> > +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries);
> > +void *zbud_map(struct zbud_pool *pool, unsigned long handle);
> > +void zbud_unmap(struct zbud_pool *pool, unsigned long handle);
> > +int zbud_get_pool_size(struct zbud_pool *pool);
> > +
> > +#endif /* _ZBUD_H_ */
> > diff --git a/mm/Kconfig b/mm/Kconfig
> > index e742d06..908f41b 100644
> > --- a/mm/Kconfig
> > +++ b/mm/Kconfig
> > @@ -477,3 +477,13 @@ config FRONTSWAP
> > and swap data is stored as normal on the matching swap device.
> >
> > If unsure, say Y to enable frontswap.
> > +
> > +config ZBUD
> > + tristate "Buddy allocator for compressed pages"
> > + default n
> > + help
> > + zbud is an special purpose allocator for storing compressed pages.
> > + It is designed to store up to two compressed pages per physical page.
> > + While this design limits storage density, it has simple and
> > + deterministic reclaim properties that make it preferable to a higher
> > + density approach when reclaim will be used.
> > diff --git a/mm/Makefile b/mm/Makefile
> > index 72c5acb..95f0197 100644
> > --- a/mm/Makefile
> > +++ b/mm/Makefile
> > @@ -58,3 +58,4 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
> > obj-$(CONFIG_DEBUG_KMEMLEAK_TEST) += kmemleak-test.o
> > obj-$(CONFIG_CLEANCACHE) += cleancache.o
> > obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
> > +obj-$(CONFIG_ZBUD) += zbud.o
> > diff --git a/mm/zbud.c b/mm/zbud.c
> > new file mode 100644
> > index 0000000..e5bd0e6
> > --- /dev/null
> > +++ b/mm/zbud.c
> > @@ -0,0 +1,564 @@
> > +/*
> > + * zbud.c - Buddy Allocator for Compressed Pages
> > + *
> > + * Copyright (C) 2013, Seth Jennings, IBM
> > + *
> > + * Concepts based on zcache internal zbud allocator by Dan Magenheimer.
> > + *
> > + * zbud is an special purpose allocator for storing compressed pages. It is
> > + * designed to store up to two compressed pages per physical page. While this
> > + * design limits storage density, it has simple and deterministic reclaim
> > + * properties that make it preferable to a higher density approach when reclaim
> > + * will be used.
> > + *
> > + * zbud works by storing compressed pages, or "zpages", together in pairs in a
> > + * single memory page called a "zbud page". The first buddy is "left
> > + * justifed" at the beginning of the zbud page, and the last buddy is "right
> > + * justified" at the end of the zbud page. The benefit is that if either
> > + * buddy is freed, the freed buddy space, coalesced with whatever slack space
> > + * that existed between the buddies, results in the largest possible free region
> > + * within the zbud page.
> > + *
> > + * zbud also provides an attractive lower bound on density. The ratio of zpages
> > + * to zbud pages can not be less than 1. This ensures that zbud can never "do
> > + * harm" by using more pages to store zpages than the uncompressed zpages would
> > + * have used on their own.
> > + *
> > + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> > + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> > + * into chunks allows organizing unbuddied zbud pages into a manageable number
> > + * of unbuddied lists according to the number of free chunks available in the
> > + * zbud page.
> > + *
> > + * The zbud API differs from that of conventional allocators in that the
> > + * allocation function, zbud_alloc(), returns an opaque handle to the user,
> > + * not a dereferenceable pointer. The user must map the handle using
> > + * zbud_map() in order to get a usable pointer by which to access the
> > + * allocation data and unmap the handle with zbud_unmap() when operations
> > + * on the allocation data are complete.
> > + */
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/atomic.h>
> > +#include <linux/list.h>
> > +#include <linux/mm.h>
> > +#include <linux/module.h>
> > +#include <linux/preempt.h>
> > +#include <linux/slab.h>
> > +#include <linux/spinlock.h>
> > +#include <linux/zbud.h>
> > +
> > +/*****************
> > + * Structures
> > +*****************/
> > +/**
> > + * struct zbud_page - zbud page metadata overlay
> > + * @page: typed reference to the underlying struct page
> > + * @donotuse: this overlays the page flags and should not be used
> > + * @first_chunks: the size of the first buddy in chunks, 0 if free
>
> Nitpick, the name here seems not directly to me.
> But I don't have a better idea yet.
> Maybe first_buddy_size/first_buddy_nrchunks or buddy0_size.

Yeah, I went through a few different names here. I abandoned the buddy0|1
naming in favor of "first" and "last". Adding _buddy might make it clearer
but it adds length and the fields are documented. So I'm inclined to
keep them as they are for now just to reduce churn.

>
> > + * @last_chunks: the size of the last buddy in chunks, 0 if free
> > + * @buddy: links the zbud page into the unbuddied/buddied lists in the pool
> > + * @lru: links the zbud page into the lru list in the pool
> > + *
> > + * This structure overlays the struct page to store metadata needed for a
> > + * single storage page in for zbud. There is a BUILD_BUG_ON in zbud_init()
> > + * that ensures this structure is not larger that struct page.
> > + *
> > + * The PG_reclaim flag of the underlying page is used for indicating
> > + * that this zbud page is under reclaim (see zbud_reclaim_page())
> > + */
> > +struct zbud_page {
> > + union {
> > + struct page page;
> > + struct {
> > + unsigned long donotuse;
> > + u16 first_chunks;
> > + u16 last_chunks;
> > + struct list_head buddy;
> > + struct list_head lru;
> > + };
> > + };
> > +};
> > +
> > +/*
> > + * NCHUNKS_ORDER determines the internal allocation granularity, effectively
> > + * adjusting internal fragmentation. It also determines the number of
> > + * freelists maintained in each pool. NCHUNKS_ORDER of 6 means that the
> > + * allocation granularity will be in chunks of size PAGE_SIZE/64, and there
> > + * will be 64 freelists per pool.
> > + */
> > +#define NCHUNKS_ORDER 6
> > +
> > +#define CHUNK_SHIFT (PAGE_SHIFT - NCHUNKS_ORDER)
> > +#define CHUNK_SIZE (1 << CHUNK_SHIFT)
> > +#define NCHUNKS (PAGE_SIZE >> CHUNK_SHIFT)
> > +
> > +/**
> > + * struct zbud_pool - stores metadata for each zbud pool
> > + * @lock: protects all pool lists and first|last_chunk fields of any
> > + * zbud page in the pool
> > + * @unbuddied: array of lists tracking zbud pages that only contain one buddy;
> > + * the lists each zbud page is added to depends on the size of
> > + * its free region.
> > + * @buddied: list tracking the zbud pages that contain two buddies;
> > + * these zbud pages are full
>
> Lack of list_head lru.

Ah yes, will add.

>
> > + * @pages_nr: number of zbud pages in the pool.
> > + * @ops: pointer to a structure of user defined operations specified at
> > + * pool creation time.
> > + *
> > + * This structure is allocated at pool creation time and maintains metadata
> > + * pertaining to a particular zbud pool.
> > + */
> > +struct zbud_pool {
> > + spinlock_t lock;
> > + struct list_head unbuddied[NCHUNKS];
> > + struct list_head buddied;
> > + struct list_head lru;
> > + atomic_t pages_nr;
> > + struct zbud_ops *ops;
> > +};
> > +
> > +/*****************
> > + * Helpers
> > +*****************/
> > +/* Just to make the code easier to read */
> > +enum buddy {
> > + FIRST,
> > + LAST
> > +};
> > +
> > +/* Converts an allocation size in bytes to size in zbud chunks */
> > +static inline int size_to_chunks(int size)
> > +{
> > + return (size + CHUNK_SIZE - 1) >> CHUNK_SHIFT;
> > +}
> > +
> > +#define for_each_unbuddied_list(_iter, _begin) \
> > + for ((_iter) = (_begin); (_iter) < NCHUNKS; (_iter)++)
> > +
> > +/* Initializes a zbud page from a newly allocated page */
> > +static inline struct zbud_page *init_zbud_page(struct page *page)
> > +{
> > + struct zbud_page *zbpage = (struct zbud_page *)page;
> > + zbpage->first_chunks = 0;
> > + zbpage->last_chunks = 0;
> > + INIT_LIST_HEAD(&zbpage->buddy);
> > + INIT_LIST_HEAD(&zbpage->lru);
> > + return zbpage;
> > +}
> > +
> > +/* Resets a zbud page so that it can be properly freed */
>
> Better with comment: the caller must hold the pool->lock?

You don't have to hold the pool lock for reset_zbud_page(). At the point
you call it, the page is already removed from all the zbud pool
structures.

>
> > +static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
> > +{
> > + struct page *page = &zbpage->page;
> > + set_page_private(page, 0);
> > + page->mapping = NULL;
> > + page->index = 0;
> > + page_mapcount_reset(page);
> > + init_page_count(page);
> > + INIT_LIST_HEAD(&page->lru);
> > + return page;
> > +}
> > +
> > +/*
> > + * Encodes the handle of a particular buddy within a zbud page
> > + * Pool lock should be held as this function accesses first|last_chunks
> > + */
> > +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> > + enum buddy bud)
> > +{
> > + unsigned long handle;
> > +
> > + /*
> > + * For now, the encoded handle is actually just the pointer to the data
> > + * but this might not always be the case. A little information hiding.
> > + */
> > + handle = (unsigned long)page_address(&zbpage->page);
> > + if (bud == FIRST)
> > + return handle;
> > + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> > + return handle;
> > +}
> > +
> > +/* Returns the zbud page where a given handle is stored */
> > +static inline struct zbud_page *handle_to_zbud_page(unsigned long handle)
> > +{
> > + return (struct zbud_page *)(virt_to_page(handle));
> > +}
> > +
> > +/* Returns the number of free chunks in a zbud page */
> > +static inline int num_free_chunks(struct zbud_page *zbpage)
> > +{
> > + /*
> > + * Rather than branch for different situations, just use the fact that
> > + * free buddies have a length of zero to simplify everything.
> > + */
> > + return NCHUNKS - zbpage->first_chunks - zbpage->last_chunks;
> > +}
> > +
> > +/*****************
> > + * API Functions
> > +*****************/
> > +/**
> > + * zbud_create_pool() - create a new zbud pool
> > + * @gfp: gfp flags when allocating the zbud pool structure
> > + * @ops: user-defined operations for the zbud pool
> > + *
> > + * Return: pointer to the new zbud pool or NULL if the metadata allocation
> > + * failed.
> > + */
> > +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
> > +{
> > + struct zbud_pool *pool;
> > + int i;
> > +
> > + pool = kmalloc(sizeof(struct zbud_pool), gfp);
> > + if (!pool)
> > + return NULL;
> > + spin_lock_init(&pool->lock);
> > + for_each_unbuddied_list(i, 0)
> > + INIT_LIST_HEAD(&pool->unbuddied[i]);
> > + INIT_LIST_HEAD(&pool->buddied);
> > + INIT_LIST_HEAD(&pool->lru);
> > + atomic_set(&pool->pages_nr, 0);
> > + pool->ops = ops;
> > + return pool;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_create_pool);
> > +
> > +/**
> > + * zbud_destroy_pool() - destroys an existing zbud pool
> > + * @pool: the zbud pool to be destroyed
> > + */
> > +void zbud_destroy_pool(struct zbud_pool *pool)
> > +{
> > + kfree(pool);
>
> Pages in zbud pool should also be freed here? or if they are freed
> before call this function some check may be needed.
> But there isn't a problem currently since no actual user.

Calling zbud_destroy_pool() on a pool with outstanding allocations is
a usage error. I can add that to the documentation. Not sure if we
want to go as far as adding checks to make sure that all the lists
are empty.

>
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_destroy_pool);
> > +
> > +/**
> > + * zbud_alloc() - allocates a region of a given size
> > + * @pool: zbud pool from which to allocate
> > + * @size: size in bytes of the desired allocation
> > + * @gfp: gfp flags used if the pool needs to grow
> > + * @handle: handle of the new allocation
> > + *
> > + * This function will attempt to find a free region in the pool large
> > + * enough to satisfy the allocation request. First, it tries to use
> > + * free space in the most recently used zbud page, at the beginning of
> > + * the pool LRU list. If that zbud page is full or doesn't have the
> > + * required free space, a best fit search of the unbuddied lists is
> > + * performed. If no suitable free region is found, then a new page
> > + * is allocated and added to the pool to satisfy the request.
> > + *
> > + * gfp should not set __GFP_HIGHMEM as highmem pages cannot be used
> > + * as zbud pool pages.
> > + *
> > + * Return: 0 if success and handle is set, otherwise -EINVAL is the size or
> > + * gfp arguments are invalid or -ENOMEM if the pool was unable to allocate
> > + * a new page.
> > + */
> > +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> > + unsigned long *handle)
> > +{
> > + int chunks, i, freechunks;
> > + struct zbud_page *zbpage = NULL;
> > + enum buddy bud;
> > + struct page *page;
> > +
> > + if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
> > + return -EINVAL;
> > + chunks = size_to_chunks(size);
> > + spin_lock(&pool->lock);
> > +
> > + /*
> > + * First, try to use the zbpage we last used (at the head of the
> > + * LRU) to increase LRU locality of the buddies. This is first fit.
> > + */
> > + if (!list_empty(&pool->lru)) {
> > + zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
> > + if (num_free_chunks(zbpage) >= chunks) {
> > + if (zbpage->first_chunks == 0) {
> > + list_del(&zbpage->buddy);
> > + bud = FIRST;
> > + goto found;
> > + }
> > + if (zbpage->last_chunks == 0) {
> > + list_del(&zbpage->buddy);
> > + bud = LAST;
> > + goto found;
> > + }
> > + }
> > + }
>
> I'd prefer to drop above lines to keep things simple since no way to
> prove the benefit of it.

Both you and Dan have pointed this out. While I think there is benefit, I
can't prove it. In an effort to not be hypocritical, I'll pull it out
until such time as I can demonstrate and measure the benefit.

>
> > +
> > + /* Second, try to find an unbuddied zbpage. This is best fit. */
> > + zbpage = NULL;
> > + for_each_unbuddied_list(i, chunks) {
> > + if (!list_empty(&pool->unbuddied[i])) {
> > + zbpage = list_first_entry(&pool->unbuddied[i],
> > + struct zbud_page, buddy);
> > + list_del(&zbpage->buddy);
> > + if (zbpage->first_chunks == 0)
> > + bud = FIRST;
> > + else
> > + bud = LAST;
> > + goto found;
> > + }
> > + }
> > +
> > + /* Lastly, couldn't find unbuddied zbpage, create new one */
> > + spin_unlock(&pool->lock);
> > + page = alloc_page(gfp);
> > + if (!page)
> > + return -ENOMEM;
> > + spin_lock(&pool->lock);
> > + atomic_inc(&pool->pages_nr);
> > + zbpage = init_zbud_page(page);
> > + bud = FIRST;
> > +
> > +found:
> > + if (bud == FIRST)
> > + zbpage->first_chunks = chunks;
> > + else
> > + zbpage->last_chunks = chunks;
> > +
> > + if (zbpage->first_chunks == 0 || zbpage->last_chunks == 0) {
> > + /* Add to unbuddied list */
> > + freechunks = num_free_chunks(zbpage);
> > + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> > + } else {
> > + /* Add to buddied list */
> > + list_add(&zbpage->buddy, &pool->buddied);
> > + }
> > +
> > + /* Add/move zbpage to beginning of LRU */
> > + if (!list_empty(&zbpage->lru))
> > + list_del(&zbpage->lru);
> > + list_add(&zbpage->lru, &pool->lru);
> > +
> > + *handle = encode_handle(zbpage, bud);
> > + spin_unlock(&pool->lock);
> > +
> > + return 0;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_alloc);
> > +
> > +/**
> > + * zbud_free() - frees the allocation associated with the given handle
> > + * @pool: pool in which the allocation resided
> > + * @handle: handle associated with the allocation returned by zbud_alloc()
> > + *
> > + * In the case that the zbud page in which the allocation resides is under
> > + * reclaim, as indicated by the PG_reclaim flag being set, this function
> > + * only sets the first|last_chunks to 0. The page is actually freed
> > + * once both buddies are evicted (see zbud_reclaim_page() below).
> > + */
> > +void zbud_free(struct zbud_pool *pool, unsigned long handle)
> > +{
> > + struct zbud_page *zbpage;
> > + int freechunks;
> > +
> > + spin_lock(&pool->lock);
> > + zbpage = handle_to_zbud_page(handle);
> > +
> > + /* If first buddy, handle will be page aligned */
> > + if (handle & ~PAGE_MASK)
> > + zbpage->last_chunks = 0;
> > + else
> > + zbpage->first_chunks = 0;
> > +
> > + if (PageReclaim(&zbpage->page)) {
> > + /* zbpage is under reclaim, reclaim will free */
> > + spin_unlock(&pool->lock);
> > + return;
> > + }
> > +
> > + /* Remove from existing buddy list */
> > + list_del(&zbpage->buddy);
> > +
> > + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> > + /* zbpage is empty, free */
> > + list_del(&zbpage->lru);
> > + __free_page(reset_zbud_page(zbpage));
> > + atomic_dec(&pool->pages_nr);
> > + } else {
> > + /* Add to unbuddied list */
> > + freechunks = num_free_chunks(zbpage);
> > + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> > + }
> > +
> > + spin_unlock(&pool->lock);
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_free);
> > +
> > +#define list_tail_entry(ptr, type, member) \
> > + list_entry((ptr)->prev, type, member)
> > +
> > +/**
> > + * zbud_reclaim_page() - evicts allocations from a pool page and frees it
> > + * @pool: pool from which a page will attempt to be evicted
> > + * @retires: number of pages on the LRU list for which eviction will
> > + * be attempted before failing
> > + *
> > + * zbud reclaim is different from normal system reclaim in that the reclaim is
> > + * done from the bottom, up. This is because only the bottom layer, zbud, has
> > + * information on how the allocations are organized within each zbud page. This
> > + * has the potential to create interesting locking situations between zbud and
> > + * the user, however.
> > + *
> > + * To avoid these, this is how zbud_reclaim_page() should be called:
> > +
> > + * The user detects a page should be reclaimed and calls zbud_reclaim_page().
> > + * zbud_reclaim_page() will remove a zbud page from the pool LRU list and call
> > + * the user-defined eviction handler with the pool and handle as arguments.
> > + *
> > + * If the handle can not be evicted, the eviction handler should return
> > + * non-zero. zbud_reclaim_page() will add the zbud page back to the
> > + * appropriate list and try the next zbud page on the LRU up to
> > + * a user defined number of retries.
> > + *
> > + * If the handle is successfully evicted, the eviction handler should
> > + * return 0 _and_ should have called zbud_free() on the handle. zbud_free()
> > + * contains logic to delay freeing the page if the page is under reclaim,
> > + * as indicated by the setting of the PG_reclaim flag on the underlying page.
> > + *
> > + * If all buddies in the zbud page are successfully evicted, then the
> > + * zbud page can be freed.
> > + *
> > + * Returns: 0 if page is successfully freed, otherwise -EINVAL if there are
> > + * no pages to evict or an eviction handler is not registered, -EAGAIN if
> > + * the retry limit was hit.
> > + */
> > +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries)
> > +{
> > + int i, ret, freechunks;
> > + struct zbud_page *zbpage;
> > + unsigned long first_handle = 0, last_handle = 0;
> > +
> > + spin_lock(&pool->lock);
> > + if (!pool->ops || !pool->ops->evict || list_empty(&pool->lru) ||
> > + retries == 0) {
> > + spin_unlock(&pool->lock);
> > + return -EINVAL;
> > + }
> > + for (i = 0; i < retries; i++) {
> > + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> > + list_del(&zbpage->lru);
> > + list_del(&zbpage->buddy);
> > + /* Protect zbpage against free */
> > + SetPageReclaim(&zbpage->page);
> > + /*
> > + * We need encode the handles before unlocking, since we can
> > + * race with free that will set (first|last)_chunks to 0
> > + */
> > + first_handle = 0;
> > + last_handle = 0;
> > + if (zbpage->first_chunks)
> > + first_handle = encode_handle(zbpage, FIRST);
> > + if (zbpage->last_chunks)
> > + last_handle = encode_handle(zbpage, LAST);
> > + spin_unlock(&pool->lock);
> > +
> > + /* Issue the eviction callback(s) */
> > + if (first_handle) {
> > + ret = pool->ops->evict(pool, first_handle);
> > + if (ret)
> > + goto next;
> > + }
> > + if (last_handle) {
> > + ret = pool->ops->evict(pool, last_handle);
> > + if (ret)
> > + goto next;
>
> Will go to next anyway!

Yes, yes it will :)

>
> > + }
> > +next:
> > + spin_lock(&pool->lock);
> > + ClearPageReclaim(&zbpage->page);
> > + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> > + /*
> > + * Both buddies are now free, free the zbpage and
> > + * return success.
> > + */
> > + __free_page(reset_zbud_page(zbpage));
> > + atomic_dec(&pool->pages_nr);
> > + spin_unlock(&pool->lock);
> > + return 0;
> > + } else if (zbpage->first_chunks == 0 ||
> > + zbpage->last_chunks == 0) {
> > + /* add to unbuddied list */
> > + freechunks = num_free_chunks(zbpage);
> > + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> > + } else {
> > + /* add to buddied list */
> > + list_add(&zbpage->buddy, &pool->buddied);
> > + }
> > +
> > + /* add to beginning of LRU */
> > + list_add(&zbpage->lru, &pool->lru);
> > + }
> > + spin_unlock(&pool->lock);
> > + return -EAGAIN;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_reclaim_page);
> > +
> > +/**
> > + * zbud_map() - maps the allocation associated with the given handle
> > + * @pool: pool in which the allocation resides
> > + * @handle: handle associated with the allocation to be mapped
> > + *
> > + * While trivial for zbud, the mapping functions for others allocators
> > + * implementing this allocation API could have more complex information encoded
> > + * in the handle and could create temporary mappings to make the data
> > + * accessible to the user.
> > + *
> > + * Returns: a pointer to the mapped allocation
> > + */
> > +void *zbud_map(struct zbud_pool *pool, unsigned long handle)
> > +{
> > + return (void *)(handle);
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_map);
> > +
> > +/**
> > + * zbud_unmap() - maps the allocation associated with the given handle
> > + * @pool: pool in which the allocation resides
> > + * @handle: handle associated with the allocation to be unmapped
> > + */
> > +void zbud_unmap(struct zbud_pool *pool, unsigned long handle)
> > +{
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_unmap);
> > +
> > +/**
> > + * zbud_get_pool_size() - gets the zbud pool size in pages
> > + * @pool: pool whose size is being queried
> > + *
> > + * Returns: size in pages of the given pool
> > + */
> > +int zbud_get_pool_size(struct zbud_pool *pool)
> > +{
> > + return atomic_read(&pool->pages_nr);
>
> Should hold pool->lock?
> I saw some other place dec/inc pool->pages_nr with holding pool->lock.

It is atomic specifically to avoid having to hold the lock. However,
I think I do hold the lock everywhere else, so it is confusing. I'll
document this and possibly move the other references outside the lock
so it is obvious.

Pending these changes, can I get your ack?

Thanks,
Seth

>
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_get_pool_size);
> > +
> > +static int __init init_zbud(void)
> > +{
> > + /* Make sure we aren't overflowing the underlying struct page */
> > + BUILD_BUG_ON(sizeof(struct zbud_page) != sizeof(struct page));
> > + /* Make sure we can represent any chunk offset with a u16 */
> > + BUILD_BUG_ON(sizeof(u16) * BITS_PER_BYTE < PAGE_SHIFT - CHUNK_SHIFT);
> > + pr_info("loaded\n");
> > + return 0;
> > +}
> > +
> > +static void __exit exit_zbud(void)
> > +{
> > + pr_info("unloaded\n");
> > +}
> > +
> > +module_init(init_zbud);
> > +module_exit(exit_zbud);
> > +
> > +MODULE_LICENSE("GPL");
> > +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> > +MODULE_DESCRIPTION("Buddy Allocator for Compressed Pages");
> >
>
> --
> Regards,
> -Bob
>

2013-05-14 17:29:15

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Tue, May 14, 2013 at 09:37:08AM -0700, Dan Magenheimer wrote:
> > From: Seth Jennings [mailto:[email protected]]
> > Subject: Re: [PATCHv11 3/4] zswap: add to mm/
> >
> > On Tue, May 14, 2013 at 05:19:19PM +0800, Bob Liu wrote:
> > > Hi Seth,
> >
> > Hi Bob, thanks for the review!
> >
> > >
> > > > + /* reclaim space if needed */
> > > > + if (zswap_is_full()) {
> > > > + zswap_pool_limit_hit++;
> > > > + if (zbud_reclaim_page(tree->pool, 8)) {
> > >
> > > My idea is to wake up a kernel thread here to do the reclaim.
> > > Once zswap is full(20% percent of total mem currently), the kernel
> > > thread should reclaim pages from it. Not only reclaim one page, it
> > > should depend on the current memory pressure.
> > > And then the API in zbud may like this:
> > > zbud_reclaim_page(pool, nr_pages_to_reclaim, nr_retry);
> >
> > So kswapd for zswap. I'm not opposed to the idea if a case can be
> > made for the complexity. I must say, I don't see that case though.
> >
> > The policy can evolve as deficiencies are demonstrated and solutions are
> > found.
>
> Hmmm... it is fairly easy to demonstrate the deficiency if
> one tries. I actually first saw it occur on a real (though
> early) EL6 system which started some graphics-related service
> that caused a very brief swapstorm that was invisible during
> normal boot but clogged up RAM with compressed pages which
> later caused reduced weird benchmarking performance.

Without any specifics, I'm not sure what I can do with this.

I'm hearing you say that the source of the benchmark degradation
are the idle pages in zswap. In that case, the periodic writeback
patches I have in the wings should address this.

I think we are on the same page without realizing it. Right now
zswap supports a kind of "direct reclaim" model at allocation time.
The periodic writeback patches will handle the proactive writeback
part to free up the zswap pool when it has idle pages in it.

>
> I think Mel's unpredictability concern applies equally here...
> this may be a "long-term source of bugs and strange memory
> management behavior."
>
> > Can I get your ack on this pending the other changes?
>
> I'd like to hear Mel's feedback about this, but perhaps
> a compromise to allow for zswap merging would be to add
> something like the following to zswap's Kconfig comment:
>
> "Zswap reclaim policy is still primitive. Until it improves,
> zswap should be considered experimental and is not recommended
> for production use."

Just for the record, an "experimental" tag in the Kconfig won't
work for me.

The reclaim policy for zswap is not primitive, it's simple. There
is a difference. Plus zswap is already runtime disabled by default.
If distros/customers enabled it, it is because they purposely
enabled it.

Seth

>
> If Mel agrees with the unpredictability and also agrees
> with the Kconfig compromise, I am willing to ack.
>
> --
> To unsubscribe, send a message with 'unsubscribe linux-mm' in
> the body to [email protected]. For more info on Linux MM,
> see: http://www.linux-mm.org/ .
> Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
>

2013-05-14 20:19:54

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> <snip>
>
> > > +/* The maximum percentage of memory that the compressed pool can occupy */
> > > +static unsigned int zswap_max_pool_percent = 20;
> > > +module_param_named(max_pool_percent,
> > > + zswap_max_pool_percent, uint, 0644);
>
> <snip>
>
> > This limit, along with the code that enforces it (by calling reclaim
> > when the limit is reached), is IMHO questionable. Is there any
> > other kernel memory allocation that is constrained by a percentage
> > of total memory rather than dynamically according to current
> > system conditions? As Mel pointed out (approx.), if this limit
> > is reached by a zswap-storm and filled with pages of long-running,
> > rarely-used processes, 20% of RAM (by default here) becomes forever
> > clogged.
>
> So there are two comments here 1) dynamic pool limit and 2) writeback
> of pages in zswap that won't be faulted in or forced out by pressure.
>
> Comment 1 feeds from the point of view that compressed pages should just be
> another type of memory managed by the core MM. While ideal, very hard to
> implement in practice. We are starting to realize that even the policy
> governing to active vs inactive list is very hard to get right. Then shrinkers
> add more complexity to the policy problem. Throwing another type in the mix
> would just that much more complex and hard to get right (assuming there even
> _is_ a "right" policy for everyone in such a complex system).
>
> This max_pool_percent policy is simple, works well, and provides a
> deterministic policy that users can understand. Users can be assured that a
> dynamic policy heuristic won't go nuts and allow the compressed pool to grow
> unbounded or be so aggressively reclaimed that it offers no value.

Hi Seth --

Hmmm... I'm not sure how to politely say "bullshit". :-)

The default 20% was randomly pulled out of the air long ago for zcache
experiments. If you can explain why 20% is better than 19% or 21%, or
better than 10% or 30% or even 50%, that would be a start. Then please try
to explain -- in terms an average sysadmin can understand -- under
what circumstances this number should be higher or lower, that would
be even better. In fact if you can explain it in even very broadbrush
terms like "higher for embedded" and "lower for server" that would be
useful. If the top Linux experts in compression can't answer these
questions (and the default is a random number, which it is), I don't
know how we can expect users to be "assured".

What you mean is "works well"... on the two benchmarks you've tried it
on. You say it's too hard to do dynamically... even though every other
significant RAM user in the kernel has to do it dynamically.
Workloads are dynamic and heavy users of RAM needs to deal with that.
You don't see a limit on the number of anonymous pages in the MM subsystem,
and you don't see a limit on the number of inodes in btrfs. Linus
would rightfully barf all over those limits and (if he was paying attention
to this discussion) he would barf on this limit too.

It's unfortunate that my proposed topic for LSFMM was pre-empted
by the zsmalloc vs zbud discussion and zswap vs zcache, because
I think the real challenge of zswap (or zcache) and the value to
distros and end users requires us to get this right BEFORE users
start filing bugs about performance weirdness. After which most
users and distros will simply default to 0% (i.e. turn zswap off)
because zswap unpredictably sometimes sucks.

<flame off> sorry...

> Comment 2 I agree is an issue. I already have patches for a "periodic
> writeback" functionality that starts to shrink the zswap pool via
> writeback if zswap goes idle for a period of time. This addresses
> the issue with long-lived, never-accessed pages getting stuck in
> zswap forever.

Pulling the call out of zswap_frontswap_store() (and ensuring there still
aren't any new races) would be a good start. But this is just a mechanism;
you haven't said anything about the policy or how you intend to
enforce the policy. Which just gets us back to Comment 1...

So Comment 1 and Comment 2 are really the same: How do we appropriately
manage the number of pages in the system that are used for storing
compressed pages?

2013-05-14 20:57:22

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On Tue, May 14, 2013 at 09:37:08AM -0700, Dan Magenheimer wrote:
> > > From: Seth Jennings [mailto:[email protected]]
> > > Subject: Re: [PATCHv11 3/4] zswap: add to mm/
> > >
> > > On Tue, May 14, 2013 at 05:19:19PM +0800, Bob Liu wrote:
> > > > Hi Seth,
> > >
> > > Hi Bob, thanks for the review!
> > >
> > > >
> > > > > + /* reclaim space if needed */
> > > > > + if (zswap_is_full()) {
> > > > > + zswap_pool_limit_hit++;
> > > > > + if (zbud_reclaim_page(tree->pool, 8)) {
> > > >
> > > > My idea is to wake up a kernel thread here to do the reclaim.
> > > > Once zswap is full(20% percent of total mem currently), the kernel
> > > > thread should reclaim pages from it. Not only reclaim one page, it
> > > > should depend on the current memory pressure.
> > > > And then the API in zbud may like this:
> > > > zbud_reclaim_page(pool, nr_pages_to_reclaim, nr_retry);
> > >
> > > So kswapd for zswap. I'm not opposed to the idea if a case can be
> > > made for the complexity. I must say, I don't see that case though.
> > >
> > > The policy can evolve as deficiencies are demonstrated and solutions are
> > > found.
> >
> > Hmmm... it is fairly easy to demonstrate the deficiency if
> > one tries. I actually first saw it occur on a real (though
> > early) EL6 system which started some graphics-related service
> > that caused a very brief swapstorm that was invisible during
> > normal boot but clogged up RAM with compressed pages which
> > later caused reduced weird benchmarking performance.
>
> Without any specifics, I'm not sure what I can do with this.

Well, I think its customary for the author of a patch to know
the limitations of the patch. I suggest you synthesize a
workload that attempts to measure worst case. That's exactly
what I did a year ago that led me to the realization that
zcache needed to solve some issues before it was ready to
promote out of staging.

> I'm hearing you say that the source of the benchmark degradation
> are the idle pages in zswap. In that case, the periodic writeback
> patches I have in the wings should address this.
>
> I think we are on the same page without realizing it. Right now
> zswap supports a kind of "direct reclaim" model at allocation time.
> The periodic writeback patches will handle the proactive writeback
> part to free up the zswap pool when it has idle pages in it.

I don't think we are on the same page though maybe you are heading
in the same direction now. I won't repeat the comments from the
previous email.

> > I think Mel's unpredictability concern applies equally here...
> > this may be a "long-term source of bugs and strange memory
> > management behavior."
> >
> > > Can I get your ack on this pending the other changes?
> >
> > I'd like to hear Mel's feedback about this, but perhaps
> > a compromise to allow for zswap merging would be to add
> > something like the following to zswap's Kconfig comment:
> >
> > "Zswap reclaim policy is still primitive. Until it improves,
> > zswap should be considered experimental and is not recommended
> > for production use."
>
> Just for the record, an "experimental" tag in the Kconfig won't
> work for me.
>
> The reclaim policy for zswap is not primitive, it's simple. There
> is a difference. Plus zswap is already runtime disabled by default.
> If distros/customers enabled it, it is because they purposely
> enabled it.

Hmmm... I think you are proposing to users/distros the following
use model: "If zswap works for you, turn it on. If it sucks,
turn it off. I can't tell you in advance whether it will work
or suck for your distro/workload, but it will probably work so
please try it."

That sounds awfully experimental to me.

The problem is not simple. Your solution is simple because
you are simply pretending that the harder parts of the problem
don't exist.

Dan

2013-05-14 22:55:23

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Tue, May 14, 2013 at 01:18:48PM -0700, Dan Magenheimer wrote:
> > From: Seth Jennings [mailto:[email protected]]
> > Subject: Re: [PATCHv11 3/4] zswap: add to mm/
> >
> > <snip>
> >
> > > > +/* The maximum percentage of memory that the compressed pool can occupy */
> > > > +static unsigned int zswap_max_pool_percent = 20;
> > > > +module_param_named(max_pool_percent,
> > > > + zswap_max_pool_percent, uint, 0644);
> >
> > <snip>
> >
> > > This limit, along with the code that enforces it (by calling reclaim
> > > when the limit is reached), is IMHO questionable. Is there any
> > > other kernel memory allocation that is constrained by a percentage
> > > of total memory rather than dynamically according to current
> > > system conditions? As Mel pointed out (approx.), if this limit
> > > is reached by a zswap-storm and filled with pages of long-running,
> > > rarely-used processes, 20% of RAM (by default here) becomes forever
> > > clogged.
> >
> > So there are two comments here 1) dynamic pool limit and 2) writeback
> > of pages in zswap that won't be faulted in or forced out by pressure.
> >
> > Comment 1 feeds from the point of view that compressed pages should just be
> > another type of memory managed by the core MM. While ideal, very hard to
> > implement in practice. We are starting to realize that even the policy
> > governing to active vs inactive list is very hard to get right. Then shrinkers
> > add more complexity to the policy problem. Throwing another type in the mix
> > would just that much more complex and hard to get right (assuming there even
> > _is_ a "right" policy for everyone in such a complex system).
> >
> > This max_pool_percent policy is simple, works well, and provides a
> > deterministic policy that users can understand. Users can be assured that a
> > dynamic policy heuristic won't go nuts and allow the compressed pool to grow
> > unbounded or be so aggressively reclaimed that it offers no value.
>
> Hi Seth --
>
> Hmmm... I'm not sure how to politely say "bullshit". :-)
>
> The default 20% was randomly pulled out of the air long ago for zcache
> experiments. If you can explain why 20% is better than 19% or 21%, or
> better than 10% or 30% or even 50%, that would be a start. Then please try
> to explain -- in terms an average sysadmin can understand -- under
> what circumstances this number should be higher or lower, that would
> be even better. In fact if you can explain it in even very broadbrush
> terms like "higher for embedded" and "lower for server" that would be
> useful. If the top Linux experts in compression can't answer these
> questions (and the default is a random number, which it is), I don't
> know how we can expect users to be "assured".

20% is a default maximum. There really isn't a particular reason for the
selection other than to supply reasonable default to a tunable. 20% is enough
to show the benefit while assuring the user zswap won't eat more than that
amount of memory under any circumstance. The point is to make it a tunable,
not to launch an incredibly in-depth study on what the default should be.

As guidance on how to tune it, switching to zbud actually made the math simpler
by bounding the best case to 2 and the expected density to very near 2. I have
two methods, one based on calculation and another based on experimentation.

Yes, I understand that there are many things to consider, but for the sake of
those that honestly care about the answer to the question, I'll answer it.

Method 1:

If you have a workload running on a machine with x GB of RAM and an anonymous
working set of y GB of pages where x < y, a good starting point for
max_pool_percent is ((y/x)-1)*100.

For example, if you have 10GB of RAM and 12GB anon working set, (12/10-1)*100 =
20. During operation there would be 8GB in uncompressed memory, and 4GB worth
of compressed memory occupying 2GB (i.e. 20%) of RAM. This will reduce swap I/O
to near zero assuming the pages compress <50% on average.

Bear in mind that this formula provides a lower bound on max_pool_percent if
you want to avoid swap I/0. Setting max_pool_percent to >20 would produce
the same situation.

Method 2:

Experimentally, one can just watch swap I/O rates while the workload is running
and increase max_pool_percent until no (or acceptable level of) swap I/O is
occurring.

As max_pool_percent increases, however, there is less and less room for
uncompressed memory, the only kind of memory on which the kernel can actually
operate. Compression/decompression activity might start dominating over useful
work. Going over 80 is probably not advised. If swap I/O is still observed
for high values of max_pool_percent, then the memory load should be reduced,
memory capacity should be increased, or performance degradation should be accepted.

>
> What you mean is "works well"... on the two benchmarks you've tried it
> on. You say it's too hard to do dynamically... even though every other
> significant RAM user in the kernel has to do it dynamically.
> Workloads are dynamic and heavy users of RAM needs to deal with that.
> You don't see a limit on the number of anonymous pages in the MM subsystem,
> and you don't see a limit on the number of inodes in btrfs. Linus
> would rightfully barf all over those limits and (if he was paying attention
> to this discussion) he would barf on this limit too.

Putting a user-tunable hard limit on the size of the compressed pool is in _no
way_ analogous to putting an fixed upper bound on system-wide anonymous memory
or number of inodes. In fact, they are so dissimilar, I don't know what else to
say about the attempted comparison.

zswap is not like other caches in the kernel. Most caches make use of
unused/less recently used memory in an effort to improve performance by
avoiding rereads from persistent media. In the case of zswap, its size is near
0 until memory pressure hits a threshold; a point at which traditional caches
start shrinking. zswap _grows_ under memory pressure while all other caches
shrink. This is why traditional cache sizing policies and techniques don't
work with zswap. In the absence of any precedent policy for this kind of
caching, zswap goes with a simple, but understandable one: user-tunable cap
on the maximum size and shrink through pressure and (soon) age driven writeback.

Seth

>
> It's unfortunate that my proposed topic for LSFMM was pre-empted
> by the zsmalloc vs zbud discussion and zswap vs zcache, because
> I think the real challenge of zswap (or zcache) and the value to
> distros and end users requires us to get this right BEFORE users
> start filing bugs about performance weirdness. After which most
> users and distros will simply default to 0% (i.e. turn zswap off)
> because zswap unpredictably sometimes sucks.
>
> <flame off> sorry...
>
> > Comment 2 I agree is an issue. I already have patches for a "periodic
> > writeback" functionality that starts to shrink the zswap pool via
> > writeback if zswap goes idle for a period of time. This addresses
> > the issue with long-lived, never-accessed pages getting stuck in
> > zswap forever.
>
> Pulling the call out of zswap_frontswap_store() (and ensuring there still
> aren't any new races) would be a good start. But this is just a mechanism;
> you haven't said anything about the policy or how you intend to
> enforce the policy. Which just gets us back to Comment 1...
>
> So Comment 1 and Comment 2 are really the same: How do we appropriately
> manage the number of pages in the system that are used for storing
> compressed pages?
>
> --
> To unsubscribe, send a message with 'unsubscribe linux-mm' in
> the body to [email protected]. For more info on Linux MM,
> see: http://www.linux-mm.org/ .
> Don't email: <a href=mailto:"[email protected]"> [email protected] </a>
>

2013-05-15 17:12:30

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On Tue, May 14, 2013 at 01:18:48PM -0700, Dan Magenheimer wrote:
> > > From: Seth Jennings [mailto:[email protected]]
> > > Subject: Re: [PATCHv11 3/4] zswap: add to mm/
> > >
> > > <snip>
> > >
> > > > > +/* The maximum percentage of memory that the compressed pool can occupy */
> > > > > +static unsigned int zswap_max_pool_percent = 20;
> > > > > +module_param_named(max_pool_percent,
> > > > > + zswap_max_pool_percent, uint, 0644);
> > >
> > > <snip>
> > >
> > > > This limit, along with the code that enforces it (by calling reclaim
> > > > when the limit is reached), is IMHO questionable. Is there any
> > > > other kernel memory allocation that is constrained by a percentage
> > > > of total memory rather than dynamically according to current
> > > > system conditions? As Mel pointed out (approx.), if this limit
> > > > is reached by a zswap-storm and filled with pages of long-running,
> > > > rarely-used processes, 20% of RAM (by default here) becomes forever
> > > > clogged.
> > >
> > > So there are two comments here 1) dynamic pool limit and 2) writeback
> > > of pages in zswap that won't be faulted in or forced out by pressure.
> > >
> > > Comment 1 feeds from the point of view that compressed pages should just be
> > > another type of memory managed by the core MM. While ideal, very hard to
> > > implement in practice. We are starting to realize that even the policy
> > > governing to active vs inactive list is very hard to get right. Then shrinkers
> > > add more complexity to the policy problem. Throwing another type in the mix
> > > would just that much more complex and hard to get right (assuming there even
> > > _is_ a "right" policy for everyone in such a complex system).
> > >
> > > This max_pool_percent policy is simple, works well, and provides a
> > > deterministic policy that users can understand. Users can be assured that a
> > > dynamic policy heuristic won't go nuts and allow the compressed pool to grow
> > > unbounded or be so aggressively reclaimed that it offers no value.
> >
> > Hi Seth --
> >
> > Hmmm... I'm not sure how to politely say "bullshit". :-)
> >
> > The default 20% was randomly pulled out of the air long ago for zcache
> > experiments. If you can explain why 20% is better than 19% or 21%, or
> > better than 10% or 30% or even 50%, that would be a start. Then please try
> > to explain -- in terms an average sysadmin can understand -- under
> > what circumstances this number should be higher or lower, that would
> > be even better. In fact if you can explain it in even very broadbrush
> > terms like "higher for embedded" and "lower for server" that would be
> > useful. If the top Linux experts in compression can't answer these
> > questions (and the default is a random number, which it is), I don't
> > know how we can expect users to be "assured".
>
> 20% is a default maximum. There really isn't a particular reason for the
> selection other than to supply reasonable default to a tunable. 20% is enough
> to show the benefit while assuring the user zswap won't eat more than that
> amount of memory under any circumstance. The point is to make it a tunable,
> not to launch an incredibly in-depth study on what the default should be.

My point is that a tunable is worthless -- and essentially the same as
a fixed value -- unless you can clearly instruct target users how to
change it to match their needs.

> As guidance on how to tune it, switching to zbud actually made the math simpler
> by bounding the best case to 2 and the expected density to very near 2. I have
> two methods, one based on calculation and another based on experimentation.
>
> Yes, I understand that there are many things to consider, but for the sake of
> those that honestly care about the answer to the question, I'll answer it.
>
> Method 1:
>
> If you have a workload running on a machine with x GB of RAM and an anonymous
> working set of y GB of pages where x < y, a good starting point for
> max_pool_percent is ((y/x)-1)*100.
>
> For example, if you have 10GB of RAM and 12GB anon working set, (12/10-1)*100 =
> 20. During operation there would be 8GB in uncompressed memory, and 4GB worth
> of compressed memory occupying 2GB (i.e. 20%) of RAM. This will reduce swap I/O
> to near zero assuming the pages compress <50% on average.
>
> Bear in mind that this formula provides a lower bound on max_pool_percent if
> you want to avoid swap I/0. Setting max_pool_percent to >20 would produce
> the same situation.

OK, let's try to apply your method. You personally have undoubtedly
compiled the kernel hundreds, maybe thousands of times in the last year.
In the restricted environment where you and I have run benchmarks, this
is a fairly stable and reproducible workload == stable and reproducible
are somewhat rare in the real world.

Can you tell me what the "anon working set" is of compiling the kernel?
Have you, one of the top experts in Linux compression technology, ever
even once changed the max_pool_percent in your benchmark runs even as
an experiment?

This method also makes the assumption that the users that are
going to enable zswap are doing so because their system is currently
swapping its poor brains out (and for some reason can't increase the
RAM in their system). I sure hope that's not the only reason users
will enable it.

> Method 2:
>
> Experimentally, one can just watch swap I/O rates while the workload is running
> and increase max_pool_percent until no (or acceptable level of) swap I/O is
> occurring.
>
> As max_pool_percent increases, however, there is less and less room for
> uncompressed memory, the only kind of memory on which the kernel can actually
> operate. Compression/decompression activity might start dominating over useful
> work. Going over 80 is probably not advised. If swap I/O is still observed
> for high values of max_pool_percent, then the memory load should be reduced,
> memory capacity should be increased, or performance degradation should be accepted.

Method 2 assumes workloads are reproducible/idempotent.

So, I don't think either of these methods are answers to my question,
just handwaving.

> > What you mean is "works well"... on the two benchmarks you've tried it
> > on. You say it's too hard to do dynamically... even though every other
> > significant RAM user in the kernel has to do it dynamically.
> > Workloads are dynamic and heavy users of RAM needs to deal with that.
> > You don't see a limit on the number of anonymous pages in the MM subsystem,
> > and you don't see a limit on the number of inodes in btrfs. Linus
> > would rightfully barf all over those limits and (if he was paying attention
> > to this discussion) he would barf on this limit too.
>
> Putting a user-tunable hard limit on the size of the compressed pool is in _no
> way_ analogous to putting an fixed upper bound on system-wide anonymous memory
> or number of inodes. In fact, they are so dissimilar, I don't know what else to
> say about the attempted comparison.

I'm not sure why we disagree here, but I see them as very similar.

> zswap is not like other caches in the kernel. Most caches make use of
> unused/less recently used memory in an effort to improve performance by
> avoiding rereads from persistent media. In the case of zswap, its size is near
> 0 until memory pressure hits a threshold; a point at which traditional caches
> start shrinking. zswap _grows_ under memory pressure while all other caches
> shrink. This is why traditional cache sizing policies and techniques don't
> work with zswap. In the absence of any precedent policy for this kind of
> caching, zswap goes with a simple, but understandable one: user-tunable cap
> on the maximum size and shrink through pressure and (soon) age driven writeback.

Zswap is not like other caches in the kernel because it is not a cache
at all. It is simply a mechanism whereby the MM subsystem can increase
the total number of anonymous pages stored in RAM by identifying (via
frontswap) and compressing (via LZO) the lowest priority anonymous pages,
then decompressing them when a page fault occurs. The swap subsystem
is just a convenient place to put the hooks because it clearly identifies
the lowest priority anonymous pages -- and also provides a convenient
key (type+offset) to identify the compressed page (which obviously can't
be addressed using normal CPU direct addressing mechanisms)

The precedent policy is the MM subsystem itself, which is responsible
for managing the quantity of pages used for a wide variety of uses and
the priority of these classes against each other. It must do this
dynamically, not just because it must handle different kinds of
workloads, but also because each one of those workloads vary
dramatically across time. Compressed anonymous pages are still
a form of anonymous pages and, to the extent possible, need to be
counted and managed/prioritized as anonymous pages when the
MM subsystem is making policy decisions.

So any artificial limit in the MM subsystem (even as a percentage of
total RAM) is very suspicious and deserves scrutiny. As I said,
your max_pages solution is simple only because you are ignoring the
harder part of the problem and and now also pretending that users/distros
will ever have any clue at all as to how to solve that harder part of
the problem.

Sorry, but I don't think that's appropriate for a patch in the MM subsystem.

Thanks,
Dan

2013-05-15 18:57:12

by Konrad Rzeszutek Wilk

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

> Sorry, but I don't think that's appropriate for a patch in the MM subsystem.

I am heading to the airport shortly so this email is a bit hastily typed.

Perhaps a compromise can be reached where this code is merged as a driver
not a core mm component. There is a high bar to be in the MM - it has to
work with many many different configurations.

And drivers don't have such a high bar. They just need to work on a specific
issue and that is it. If zswap ended up in say, drivers/mm that would make
it more palpable I think.

Thoughts?
>
> Thanks,
> Dan

2013-05-15 19:38:34

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Konrad Rzeszutek Wilk
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> > Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
>
> I am heading to the airport shortly so this email is a bit hastily typed.
>
> Perhaps a compromise can be reached where this code is merged as a driver
> not a core mm component. There is a high bar to be in the MM - it has to
> work with many many different configurations.
>
> And drivers don't have such a high bar. They just need to work on a specific
> issue and that is it. If zswap ended up in say, drivers/mm that would make
> it more palpable I think.
>
> Thoughts?

Hmmm...

To me, that sounds like a really good compromise. Then anyone
who wants to experiment with compressed swap pages can do so by
enabling the zswap driver. And the harder problem of deeply integrating
compression into the MM subsystem can proceed in parallel
by leveraging and building on the best of zswap and zcache
and zram.

Seth, if you want to re-post zswap as a driver... even a
previous zswap version with zsmalloc and without writeback...
I would be willing to ack it. If I correctly understand
Mel's concerns, I suspect he might feel the same.

2013-05-15 20:09:55

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Wed, May 15, 2013 at 02:55:06PM -0400, Konrad Rzeszutek Wilk wrote:
> > Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
>
> I am heading to the airport shortly so this email is a bit hastily typed.
>
> Perhaps a compromise can be reached where this code is merged as a driver
> not a core mm component. There is a high bar to be in the MM - it has to
> work with many many different configurations.
>
> And drivers don't have such a high bar. They just need to work on a specific
> issue and that is it. If zswap ended up in say, drivers/mm that would make
> it more palpable I think.
>
> Thoughts?

zswap, the writeback code particularly, depends on a number of non-exported
kernel symbols, namely:

swapcache_free
__swap_writepage
__add_to_swap_cache
swapcache_prepare
swapper_spaces

So it can't currently be built as a module and I'm not sure what the MM
folks would think about exporting them and making them part of the KABI.

Seth

2013-05-15 20:24:15

by Dave Hansen

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On 05/15/2013 01:09 PM, Seth Jennings wrote:
> On Wed, May 15, 2013 at 02:55:06PM -0400, Konrad Rzeszutek Wilk wrote:
>>> Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
>>
>> Perhaps a compromise can be reached where this code is merged as a driver
>> not a core mm component. There is a high bar to be in the MM - it has to
>> work with many many different configurations.
>>
>> And drivers don't have such a high bar. They just need to work on a specific
>> issue and that is it. If zswap ended up in say, drivers/mm that would make
>> it more palpable I think.

The issue is not whether it is a loadable module or a driver. Nobody
here is stupid enough to say, "hey, now it's a driver/module, all of the
complex VM interactions are finally fixed!"

If folks don't want this in their system, there's a way to turn it off,
today, with the sysfs tunables. We don't need _another_ way to turn it
off at runtime (unloading the module/driver).

2013-05-15 20:46:25

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On 05/15/2013 03:35 PM, Dan Magenheimer wrote:
>> From: Konrad Rzeszutek Wilk
>> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>>
>>> Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
>>
>> I am heading to the airport shortly so this email is a bit hastily typed.
>>
>> Perhaps a compromise can be reached where this code is merged as a driver
>> not a core mm component. There is a high bar to be in the MM - it has to
>> work with many many different configurations.
>>
>> And drivers don't have such a high bar. They just need to work on a specific
>> issue and that is it. If zswap ended up in say, drivers/mm that would make
>> it more palpable I think.
>>
>> Thoughts?
>
> Hmmm...
>
> To me, that sounds like a really good compromise.

Come on, we all know that is nonsense.

Sure, the zswap and zbud code may not be in their final state yet,
but they belong in the mm/ directory, together with the cleancache
code and all the other related bits of code.

Lets put them in their final destination, and hope the code attracts
attention by as many MM developers as can spare the time to help
improve it.

--
All rights reversed

2013-05-15 20:47:10

by Konrad Rzeszutek Wilk

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

Seth Jennings <[email protected]> wrote:

>On Wed, May 15, 2013 at 02:55:06PM -0400, Konrad Rzeszutek Wilk wrote:
>> > Sorry, but I don't think that's appropriate for a patch in the MM
>subsystem.
>>
>> I am heading to the airport shortly so this email is a bit hastily
>typed.
>>
>> Perhaps a compromise can be reached where this code is merged as a
>driver
>> not a core mm component. There is a high bar to be in the MM - it has
>to
>> work with many many different configurations.
>>
>> And drivers don't have such a high bar. They just need to work on a
>specific
>> issue and that is it. If zswap ended up in say, drivers/mm that would
>make
>> it more palpable I think.
>>
>> Thoughts?
>
>zswap, the writeback code particularly, depends on a number of
>non-exported
>kernel symbols, namely:
>
>swapcache_free
>__swap_writepage
>__add_to_swap_cache
>swapcache_prepare
>swapper_spaces
>
>So it can't currently be built as a module and I'm not sure what the MM
>folks would think about exporting them and making them part of the
>KABI.
>
>Seth

Could those calls go through front swap? Meaning put the code that uses these calls in there?
--
Sent from my Android phone. Please excuse my brevity.

2013-05-15 20:53:20

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Seth Jennings [mailto:[email protected]]
> Sent: Wednesday, May 15, 2013 2:10 PM
> To: Konrad Rzeszutek Wilk
> Cc: Dan Magenheimer; Andrew Morton; Greg Kroah-Hartman; Nitin Gupta; Minchan Kim; Robert Jennings;
> Jenifer Hopper; Mel Gorman; Johannes Weiner; Rik van Riel; Larry Woodman; Benjamin Herrenschmidt; Dave
> Hansen; Joe Perches; Joonsoo Kim; Cody P Schafer; Hugh Dickens; Paul Mackerras; [email protected];
> [email protected]; [email protected]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On Wed, May 15, 2013 at 02:55:06PM -0400, Konrad Rzeszutek Wilk wrote:
> > > Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
> >
> > I am heading to the airport shortly so this email is a bit hastily typed.
> >
> > Perhaps a compromise can be reached where this code is merged as a driver
> > not a core mm component. There is a high bar to be in the MM - it has to
> > work with many many different configurations.
> >
> > And drivers don't have such a high bar. They just need to work on a specific
> > issue and that is it. If zswap ended up in say, drivers/mm that would make
> > it more palpable I think.
> >
> > Thoughts?
>
> zswap, the writeback code particularly, depends on a number of non-exported
> kernel symbols, namely:
>
> swapcache_free
> __swap_writepage
> __add_to_swap_cache
> swapcache_prepare
> swapper_spaces
>
> So it can't currently be built as a module and I'm not sure what the MM
> folks would think about exporting them and making them part of the KABI.

It can be built as a module if writeback is disabled (or ifdef'd by
a CONFIG_ZSWAP_WRITEBACK which depends on CONFIG_ZSWAP=y). The
folks at LSFMM who were planning to use zswap will be turning
off writeback anyway so an alternate is to pull writeback out
of zswap completely for now, since you don't really have a good
policy to manage it yet anyway.

2013-05-15 20:56:38

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Dave Hansen [mailto:[email protected]]
> Sent: Wednesday, May 15, 2013 2:24 PM
> To: Seth Jennings
> Cc: Konrad Rzeszutek Wilk; Dan Magenheimer; Andrew Morton; Greg Kroah-Hartman; Nitin Gupta; Minchan
> Kim; Robert Jennings; Jenifer Hopper; Mel Gorman; Johannes Weiner; Rik van Riel; Larry Woodman;
> Benjamin Herrenschmidt; Joe Perches; Joonsoo Kim; Cody P Schafer; Hugh Dickens; Paul Mackerras; linux-
> [email protected]; [email protected]; [email protected]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On 05/15/2013 01:09 PM, Seth Jennings wrote:
> > On Wed, May 15, 2013 at 02:55:06PM -0400, Konrad Rzeszutek Wilk wrote:
> >>> Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
> >>
> >> Perhaps a compromise can be reached where this code is merged as a driver
> >> not a core mm component. There is a high bar to be in the MM - it has to
> >> work with many many different configurations.
> >>
> >> And drivers don't have such a high bar. They just need to work on a specific
> >> issue and that is it. If zswap ended up in say, drivers/mm that would make
> >> it more palpable I think.
>
> The issue is not whether it is a loadable module or a driver. Nobody
> here is stupid enough to say, "hey, now it's a driver/module, all of the
> complex VM interactions are finally fixed!"
>
> If folks don't want this in their system, there's a way to turn it off,
> today, with the sysfs tunables. We don't need _another_ way to turn it
> off at runtime (unloading the module/driver).

The issue is we KNOW the complex VM interactions are NOT fixed
and there has been very very little breadth testing (i.e.
across a wide range of workloads, and any attempts to show
how much harm can come from enabling it.)

That's (at least borderline) acceptable in a driver that can
be unloaded, but not in MM code IMHO.

2013-05-15 21:36:58

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Rik van Riel [mailto:[email protected]]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On 05/15/2013 03:35 PM, Dan Magenheimer wrote:
> >> From: Konrad Rzeszutek Wilk
> >> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
> >>
> >>> Sorry, but I don't think that's appropriate for a patch in the MM subsystem.
> >>
> >> I am heading to the airport shortly so this email is a bit hastily typed.
> >>
> >> Perhaps a compromise can be reached where this code is merged as a driver
> >> not a core mm component. There is a high bar to be in the MM - it has to
> >> work with many many different configurations.
> >>
> >> And drivers don't have such a high bar. They just need to work on a specific
> >> issue and that is it. If zswap ended up in say, drivers/mm that would make
> >> it more palpable I think.
> >>
> >> Thoughts?
> >
> > Hmmm...
> >
> > To me, that sounds like a really good compromise.
>
> Come on, we all know that is nonsense.
>
> Sure, the zswap and zbud code may not be in their final state yet,
> but they belong in the mm/ directory, together with the cleancache
> code and all the other related bits of code.
>
> Lets put them in their final destination, and hope the code attracts
> attention by as many MM developers as can spare the time to help
> improve it.

Hi Rik --

Seth has been hell-bent on getting SOME code into the kernel
for over a year, since he found out that enabling zcache, a staging
driver, resulted in a tainted kernel. First it was promoting
zcache+zsmalloc out of staging. Then it was zswap+zsmalloc without
writeback, then zswap+zsmalloc with writeback, and now zswap+zbud
with writeback but without a sane policy for writeback. All of
that time, I've been arguing and trying to integrate compression more
deeply and sensibly into MM, rather than just enabling compression as
a toy that happens to speed up a few benchmarks. (This,
in a nutshell, was the feedback I got at LSFMM12 from Andrea and
Mel... and I think also from you.) Seth has resisted every
step of the way, then integrated the functionality in question,
adapted my code (or Nitin's), and called it his own.

If you disagree with any of my arguments earlier in this thread,
please say so. Else, please reinforce that the MM subsystem
needs to dynamically adapt to a broad range of workloads,
which zswap does not (yet) do. Zswap is not simple, it is
simplistic*.

IMHO, it may be OK for a driver to be ham-handed in its memory
use, but that's not OK for something in mm/. So I think merging
zswap as a driver is a perfectly sensible compromise which lets
Seth get his code upstream, allows users (and leading-edge distros)
to experiment with compression, avoids these endless arguments,
and allows those who care to move forward on how to deeply
integrate compression into MM.

Dan

* simplistic, n., The tendency to oversimplify an issue or a problem
by ignoring complexities or complications.

2013-05-15 22:03:33

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On 05/15/2013 05:36 PM, Dan Magenheimer wrote:

> If you disagree with any of my arguments earlier in this thread,
> please say so. Else, please reinforce that the MM subsystem
> needs to dynamically adapt to a broad range of workloads,
> which zswap does not (yet) do. Zswap is not simple, it is
> simplistic*.
>
> IMHO, it may be OK for a driver to be ham-handed in its memory
> use, but that's not OK for something in mm/.

It is functionality that a lot of people want.

IMHO it should be where it has most eyes on it, so its
deficiencies can be fixed. At this point all we know is
that zswap is somewhat simplistic, but we have no idea
yet what its failures modes are in practice.

The only way to find out, is to start using it.

--
All rights reversed

2013-05-15 22:15:16

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On 05/14/2013 04:18 PM, Dan Magenheimer wrote:

> It's unfortunate that my proposed topic for LSFMM was pre-empted
> by the zsmalloc vs zbud discussion and zswap vs zcache, because
> I think the real challenge of zswap (or zcache) and the value to
> distros and end users requires us to get this right BEFORE users
> start filing bugs about performance weirdness. After which most
> users and distros will simply default to 0% (i.e. turn zswap off)
> because zswap unpredictably sometimes sucks.

I'm not sure we can get it right before people actually start
using it for real world setups, instead of just running benchmarks
on it.

The sooner we get the code out there, where users can play with
it (even if it is disabled by default and needs a sysfs or
sysctl config option to enable it), the sooner we will know how
well it works, and what needs to be changed.

--
All rights reversed

2013-05-16 15:01:32

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 1/4] debugfs: add get/set for atomic types

On 05/13/2013 08:40 AM, Seth Jennings wrote:
> debugfs currently lack the ability to create attributes
> that set/get atomic_t values.
>
> This patch adds support for this through a new
> debugfs_create_atomic_t() function.
>
> Signed-off-by: Seth Jennings <[email protected]>
> Acked-by: Greg Kroah-Hartman <[email protected]>
> Acked-by: Mel Gorman <[email protected]>

Acked-by: Rik van Riel <[email protected]>


--
All rights reversed

2013-05-16 15:31:14

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On 05/13/2013 08:40 AM, Seth Jennings wrote:
> zbud is an special purpose allocator for storing compressed pages. It is
> designed to store up to two compressed pages per physical page. While this
> design limits storage density, it has simple and deterministic reclaim
> properties that make it preferable to a higher density approach when reclaim
> will be used.
>
> zbud works by storing compressed pages, or "zpages", together in pairs in a
> single memory page called a "zbud page". The first buddy is "left
> justifed" at the beginning of the zbud page, and the last buddy is "right
> justified" at the end of the zbud page. The benefit is that if either
> buddy is freed, the freed buddy space, coalesced with whatever slack space
> that existed between the buddies, results in the largest possible free region
> within the zbud page.
>
> zbud also provides an attractive lower bound on density. The ratio of zpages
> to zbud pages can not be less than 1. This ensures that zbud can never "do
> harm" by using more pages to store zpages than the uncompressed zpages would
> have used on their own.
>
> This patch adds zbud to mm/ for later use by zswap.
>
> Signed-off-by: Seth Jennings <[email protected]>

Acked-by: Rik van Riel <[email protected]>


--
All rights reversed

2013-05-16 15:31:59

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On 05/13/2013 04:59 PM, Seth Jennings wrote:
> On Mon, May 13, 2013 at 08:43:36AM -0700, Dan Magenheimer wrote:

>> The above appears to be a new addition to my original zbud design.
>> While it may appear to be a good idea for improving LRU-ness, I
>> suspect it may have unexpected side effects in that I think far
>> fewer "fat" zpages will be buddied, which will result in many more
>> unbuddied pages containing a single fat zpage, which means much worse
>> overall density on many workloads.
>
> Yes, I see what you are saying. While I can't think of a workload that would
> cause this kind of allocation pattern in practice, I also don't have a way to
> measure the impact this first-fit fast path code has on density.
>
>>
>> This may not be apparent in kernbench or specjbb or any workload
>> where the vast majority of zpages compress to less than PAGE_SIZE/2,
>> but for a zsize distribution that is symmetric or "skews fat",
>> it may become very apparent.
>
> I'd personally think it should be kept because 1) it makes a fast allocation
> path and 2) improves LRU locality. But, without numbers to demonstrate a
> performance improvements or impacts on density, I wouldn't be opposed to taking
> it out if it is a point of contention.
>
> Anyone else care to weigh in?

I have no idea how much the "LRU-ness" of the compressed swap
cache matters, since the entire thing will be full of not
recently used data.

I can certainly see Dan's point too, but there simply is not
enough data to measure this.

Would it be an idea to merge this patch, and then send a follow-up
patch that:
1) makes this optimization a (debugfs) tunable, and
2) exports statistics on how well pages are packing

That way we would be able to figure out which way should be the
default.

I'm giving the patch my Acked-by, because I want this code to
finally move forward.

--
All rights reversed

2013-05-16 16:49:35

by Dan Magenheimer

[permalink] [raw]
Subject: RE: [PATCHv11 3/4] zswap: add to mm/

> From: Rik van Riel [mailto:[email protected]]
> Sent: Wednesday, May 15, 2013 4:15 PM
> To: Dan Magenheimer
> Cc: Seth Jennings; Andrew Morton; Greg Kroah-Hartman; Nitin Gupta; Minchan Kim; Konrad Wilk; Robert
> Jennings; Jenifer Hopper; Mel Gorman; Johannes Weiner; Larry Woodman; Benjamin Herrenschmidt; Dave
> Hansen; Joe Perches; Joonsoo Kim; Cody P Schafer; Hugh Dickens; Paul Mackerras; [email protected];
> [email protected]; [email protected]
> Subject: Re: [PATCHv11 3/4] zswap: add to mm/
>
> On 05/14/2013 04:18 PM, Dan Magenheimer wrote:
>
> > It's unfortunate that my proposed topic for LSFMM was pre-empted
> > by the zsmalloc vs zbud discussion and zswap vs zcache, because
> > I think the real challenge of zswap (or zcache) and the value to
> > distros and end users requires us to get this right BEFORE users
> > start filing bugs about performance weirdness. After which most
> > users and distros will simply default to 0% (i.e. turn zswap off)
> > because zswap unpredictably sometimes sucks.
>
> I'm not sure we can get it right before people actually start
> using it for real world setups, instead of just running benchmarks
> on it.
>
> The sooner we get the code out there, where users can play with
> it (even if it is disabled by default and needs a sysfs or
> sysctl config option to enable it), the sooner we will know how
> well it works, and what needs to be changed.

/me sets stage of first Star Wars (1977)

/me envisions self as Obi-Wan Kenobi, old and tired of fighting,
in lightsaber battle with protege Darth Vader / Anakin Skywalker

/me sadly turns off lightsaber, holds useless handle at waist,
takes a deep breath, and promptly gets sliced into oblivion.

Time for A New Hope(tm).

(/me cc's Jon Corbet for a longshot last chance of making LWN's
Kernel Development Quotes of the Week.)

2013-05-16 17:08:23

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 4/4] zswap: add documentation

On 05/13/2013 08:40 AM, Seth Jennings wrote:
> This patch adds the documentation file for the zswap functionality
>
> Signed-off-by: Seth Jennings <[email protected]>

Acked-by: Rik van Riel <[email protected]>

--
All rights reversed

2013-05-16 17:10:20

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On 05/16/2013 12:45 PM, Dan Magenheimer wrote:
>> From: Rik van Riel [mailto:[email protected]]

>> I'm not sure we can get it right before people actually start
>> using it for real world setups, instead of just running benchmarks
>> on it.
>>
>> The sooner we get the code out there, where users can play with
>> it (even if it is disabled by default and needs a sysfs or
>> sysctl config option to enable it), the sooner we will know how
>> well it works, and what needs to be changed.
>
> /me sets stage of first Star Wars (1977)
>
> /me envisions self as Obi-Wan Kenobi, old and tired of fighting,
> in lightsaber battle with protege Darth Vader / Anakin Skywalker
>
> /me sadly turns off lightsaber, holds useless handle at waist,
> takes a deep breath, and promptly gets sliced into oblivion.
>
> Time for A New Hope(tm).

I would have hoped that in the battle against memory
shortages, we would all be on the same side.

--
All rights reversed

2013-05-16 17:19:12

by Rik van Riel

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On 05/13/2013 08:40 AM, Seth Jennings wrote:
> zswap is a thin compression backend for frontswap. It receives pages from
> frontswap and attempts to store them in a compressed memory pool, resulting in
> an effective partial memory reclaim and dramatically reduced swap device I/O.
>
> Additionally, in most cases, pages can be retrieved from this compressed store
> much more quickly than reading from tradition swap devices resulting in faster
> performance for many workloads.
>
> It also has support for evicting swap pages that are currently compressed in
> zswap to the swap device on an LRU(ish) basis. This functionality is very
> important and make zswap a true cache in that, once the cache is full or can't
> grow due to memory pressure, the oldest pages can be moved out of zswap to the
> swap device so newer pages can be compressed and stored in zswap.
>
> This patch adds the zswap driver to mm/
>
> Signed-off-by: Seth Jennings <[email protected]>

Acked-by: Rik van Riel <[email protected]>

As an aside, I agree with Dan that CONFIG_EXPERIMENTAL might be
appropriate here, or at least some kind of warning in the documentation
stating that this is fairly new code that still needs to be shaken
out in the real world.

--
All rights reversed

2013-05-17 15:48:54

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Mon, May 13, 2013 at 07:40:01AM -0500, Seth Jennings wrote:
> zbud is an special purpose allocator for storing compressed pages. It is
> designed to store up to two compressed pages per physical page. While this
> design limits storage density, it has simple and deterministic reclaim
> properties that make it preferable to a higher density approach when reclaim
> will be used.
>
> zbud works by storing compressed pages, or "zpages", together in pairs in a
> single memory page called a "zbud page". The first buddy is "left
> justifed" at the beginning of the zbud page, and the last buddy is "right
> justified" at the end of the zbud page. The benefit is that if either
> buddy is freed, the freed buddy space, coalesced with whatever slack space
> that existed between the buddies, results in the largest possible free region
> within the zbud page.
>
> zbud also provides an attractive lower bound on density. The ratio of zpages
> to zbud pages can not be less than 1. This ensures that zbud can never "do
> harm" by using more pages to store zpages than the uncompressed zpages would
> have used on their own.
>
> This patch adds zbud to mm/ for later use by zswap.
>
> Signed-off-by: Seth Jennings <[email protected]>

I'm not familiar with the code in staging/zcache/zbud.c and this looks
like a rewrite but I'm curious, why was an almost complete rewrite
necessary? The staging code looks like it had debugfs statistics and
the like that would help figure how well the packing was working and so
on. I guess it was probably because it was integrated tightly with other
components in staging but could that not be torn out? I'm guessing you
have a good reason but it'd be nice to see that in the changelog.

> ---
> include/linux/zbud.h | 22 ++
> mm/Kconfig | 10 +
> mm/Makefile | 1 +
> mm/zbud.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 597 insertions(+)
> create mode 100644 include/linux/zbud.h
> create mode 100644 mm/zbud.c
>
> diff --git a/include/linux/zbud.h b/include/linux/zbud.h
> new file mode 100644
> index 0000000..954252b
> --- /dev/null
> +++ b/include/linux/zbud.h
> @@ -0,0 +1,22 @@
> +#ifndef _ZBUD_H_
> +#define _ZBUD_H_
> +
> +#include <linux/types.h>
> +
> +struct zbud_pool;
> +
> +struct zbud_ops {
> + int (*evict)(struct zbud_pool *pool, unsigned long handle);
> +};
> +
> +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops);
> +void zbud_destroy_pool(struct zbud_pool *pool);
> +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> + unsigned long *handle);
> +void zbud_free(struct zbud_pool *pool, unsigned long handle);
> +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries);
> +void *zbud_map(struct zbud_pool *pool, unsigned long handle);
> +void zbud_unmap(struct zbud_pool *pool, unsigned long handle);
> +int zbud_get_pool_size(struct zbud_pool *pool);
> +
> +#endif /* _ZBUD_H_ */
> diff --git a/mm/Kconfig b/mm/Kconfig
> index e742d06..908f41b 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -477,3 +477,13 @@ config FRONTSWAP
> and swap data is stored as normal on the matching swap device.
>
> If unsure, say Y to enable frontswap.
> +
> +config ZBUD
> + tristate "Buddy allocator for compressed pages"
> + default n
> + help
> + zbud is an special purpose allocator for storing compressed pages.
> + It is designed to store up to two compressed pages per physical page.
> + While this design limits storage density, it has simple and
> + deterministic reclaim properties that make it preferable to a higher
> + density approach when reclaim will be used.
> diff --git a/mm/Makefile b/mm/Makefile
> index 72c5acb..95f0197 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -58,3 +58,4 @@ obj-$(CONFIG_DEBUG_KMEMLEAK) += kmemleak.o
> obj-$(CONFIG_DEBUG_KMEMLEAK_TEST) += kmemleak-test.o
> obj-$(CONFIG_CLEANCACHE) += cleancache.o
> obj-$(CONFIG_MEMORY_ISOLATION) += page_isolation.o
> +obj-$(CONFIG_ZBUD) += zbud.o
> diff --git a/mm/zbud.c b/mm/zbud.c
> new file mode 100644
> index 0000000..e5bd0e6
> --- /dev/null
> +++ b/mm/zbud.c
> @@ -0,0 +1,564 @@
> +/*
> + * zbud.c - Buddy Allocator for Compressed Pages
> + *
> + * Copyright (C) 2013, Seth Jennings, IBM
> + *
> + * Concepts based on zcache internal zbud allocator by Dan Magenheimer.
> + *
> + * zbud is an special purpose allocator for storing compressed pages. It is
> + * designed to store up to two compressed pages per physical page. While this
> + * design limits storage density, it has simple and deterministic reclaim
> + * properties that make it preferable to a higher density approach when reclaim
> + * will be used.
> + *
> + * zbud works by storing compressed pages, or "zpages", together in pairs in a
> + * single memory page called a "zbud page". The first buddy is "left
> + * justifed" at the beginning of the zbud page, and the last buddy is "right
> + * justified" at the end of the zbud page. The benefit is that if either
> + * buddy is freed, the freed buddy space, coalesced with whatever slack space
> + * that existed between the buddies, results in the largest possible free region
> + * within the zbud page.
> + *
> + * zbud also provides an attractive lower bound on density. The ratio of zpages
> + * to zbud pages can not be less than 1. This ensures that zbud can never "do
> + * harm" by using more pages to store zpages than the uncompressed zpages would
> + * have used on their own.
> + *
> + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> + * into chunks allows organizing unbuddied zbud pages into a manageable number
> + * of unbuddied lists according to the number of free chunks available in the
> + * zbud page.
> + *

Fixing the size of the chunks at compile time is a very strict
limitation! Distributions will have to make that decision for all workloads
that might conceivably use zswap. Having the allocator only deal with pairs
of pages limits the worst-case behaviour where reclaim can generate lots of
IO to free a single physical page. However, the chunk size directly affects
the fragmentation properties, both internal and external, of this thing.
Once NCHUNKS is > 2 it is possible to create a workload that externally
fragments this allocator such that each physical page only holds one
compressed page. If this is a problem for a user then their only option
is to rebuild the kernel which is not always possible.

Please make this configurable by a kernel boot parameter at least. At
a glance it looks like only problem would be that you have to kmalloc
unbuddied[NCHUNKS] in the pool structure but that is hardly of earth
shattering difficulty. Make the variables read_mostly to avoid cache-line
bouncing problems.

Finally, because a review would never be complete without a bitching
session about names -- I don't like the name zbud. Buddy allocators take
a large block of memory and split it iteratively (by halves for binary
buddy allocators but there are variations) until it's a best fit for the
allocation request. A key advantage of such schemes is fast searching for
free holes. That's not what this allocator does and as the page allocator
is a binary buddy allocator in Linux, calling this this a buddy allocator
is a bit misleading. Looks like the existing zbud.c also has this problem
but hey. This thing is a first-fit segmented free list allocator with
sub-allocator properties in that it takes fixed-sized blocks as inputs and
splits them into pairs, not a buddy allocator. That characterisation does
not lend itself to a snappy name but calling it zpair or something would
be slightly less misleading than calling it a buddy allocator.

First Fit Segmented-list Allocator for in-Kernel comprEssion (FFSAKE)? :/

> + * The zbud API differs from that of conventional allocators in that the
> + * allocation function, zbud_alloc(), returns an opaque handle to the user,
> + * not a dereferenceable pointer. The user must map the handle using
> + * zbud_map() in order to get a usable pointer by which to access the
> + * allocation data and unmap the handle with zbud_unmap() when operations
> + * on the allocation data are complete.
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/atomic.h>
> +#include <linux/list.h>
> +#include <linux/mm.h>
> +#include <linux/module.h>
> +#include <linux/preempt.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/zbud.h>
> +
> +/*****************
> + * Structures
> +*****************/
> +/**
> + * struct zbud_page - zbud page metadata overlay
> + * @page: typed reference to the underlying struct page
> + * @donotuse: this overlays the page flags and should not be used
> + * @first_chunks: the size of the first buddy in chunks, 0 if free
> + * @last_chunks: the size of the last buddy in chunks, 0 if free
> + * @buddy: links the zbud page into the unbuddied/buddied lists in the pool
> + * @lru: links the zbud page into the lru list in the pool
> + *
> + * This structure overlays the struct page to store metadata needed for a
> + * single storage page in for zbud. There is a BUILD_BUG_ON in zbud_init()
> + * that ensures this structure is not larger that struct page.
> + *
> + * The PG_reclaim flag of the underlying page is used for indicating
> + * that this zbud page is under reclaim (see zbud_reclaim_page())
> + */
> +struct zbud_page {
> + union {
> + struct page page;
> + struct {
> + unsigned long donotuse;
> + u16 first_chunks;
> + u16 last_chunks;
> + struct list_head buddy;
> + struct list_head lru;
> + };
> + };
> +};
> +
> +/*
> + * NCHUNKS_ORDER determines the internal allocation granularity, effectively
> + * adjusting internal fragmentation. It also determines the number of
> + * freelists maintained in each pool. NCHUNKS_ORDER of 6 means that the
> + * allocation granularity will be in chunks of size PAGE_SIZE/64, and there
> + * will be 64 freelists per pool.
> + */
> +#define NCHUNKS_ORDER 6
> +
> +#define CHUNK_SHIFT (PAGE_SHIFT - NCHUNKS_ORDER)
> +#define CHUNK_SIZE (1 << CHUNK_SHIFT)
> +#define NCHUNKS (PAGE_SIZE >> CHUNK_SHIFT)
> +
> +/**
> + * struct zbud_pool - stores metadata for each zbud pool
> + * @lock: protects all pool lists and first|last_chunk fields of any
> + * zbud page in the pool
> + * @unbuddied: array of lists tracking zbud pages that only contain one buddy;
> + * the lists each zbud page is added to depends on the size of
> + * its free region.
> + * @buddied: list tracking the zbud pages that contain two buddies;
> + * these zbud pages are full
> + * @pages_nr: number of zbud pages in the pool.
> + * @ops: pointer to a structure of user defined operations specified at
> + * pool creation time.
> + *
> + * This structure is allocated at pool creation time and maintains metadata
> + * pertaining to a particular zbud pool.
> + */
> +struct zbud_pool {
> + spinlock_t lock;
> + struct list_head unbuddied[NCHUNKS];
> + struct list_head buddied;
> + struct list_head lru;
> + atomic_t pages_nr;

There is no need for pages_nr to be atomic. It's always manipulated
under the lock. I see that the atomic is exported so someone can read it
that is outside the lock but they are goign to have to deal with races
anyway. atomic does not magically protect them

Also, pages_nr does not appear to be the number of zbud pages in the pool,
it's the number of zpages. You may want to report both for debugging
purposes as if nr_zpages != 2 * nr_zbud_pages then zswap is using more
physical pages than it should be.

> + struct zbud_ops *ops;
> +};
> +
> +/*****************
> + * Helpers
> +*****************/
> +/* Just to make the code easier to read */
> +enum buddy {
> + FIRST,
> + LAST
> +};
> +
> +/* Converts an allocation size in bytes to size in zbud chunks */
> +static inline int size_to_chunks(int size)
> +{
> + return (size + CHUNK_SIZE - 1) >> CHUNK_SHIFT;
> +}
> +
> +#define for_each_unbuddied_list(_iter, _begin) \
> + for ((_iter) = (_begin); (_iter) < NCHUNKS; (_iter)++)
> +
> +/* Initializes a zbud page from a newly allocated page */
> +static inline struct zbud_page *init_zbud_page(struct page *page)
> +{
> + struct zbud_page *zbpage = (struct zbud_page *)page;
> + zbpage->first_chunks = 0;
> + zbpage->last_chunks = 0;
> + INIT_LIST_HEAD(&zbpage->buddy);
> + INIT_LIST_HEAD(&zbpage->lru);
> + return zbpage;
> +}

No need to inline. Only has one caller so the compiler will figure it
out.

> +
> +/* Resets a zbud page so that it can be properly freed */
> +static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
> +{
> + struct page *page = &zbpage->page;
> + set_page_private(page, 0);
> + page->mapping = NULL;
> + page->index = 0;
> + page_mapcount_reset(page);
> + init_page_count(page);
> + INIT_LIST_HEAD(&page->lru);
> + return page;
> +}

This is only used for freeing so call it free_zbud_page and have it call
__free_page for clarity. Also, this is a bit long for inlining.

> +
> +/*
> + * Encodes the handle of a particular buddy within a zbud page
> + * Pool lock should be held as this function accesses first|last_chunks
> + */
> +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> + enum buddy bud)
> +{
> + unsigned long handle;
> +
> + /*
> + * For now, the encoded handle is actually just the pointer to the data
> + * but this might not always be the case. A little information hiding.
> + */
> + handle = (unsigned long)page_address(&zbpage->page);
> + if (bud == FIRST)
> + return handle;
> + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> + return handle;
> +}

Your handles are unsigned long and are addresses. Consider making it an
opaque type so someone deferencing it would take a special kind of
stupid.

> +
> +/* Returns the zbud page where a given handle is stored */
> +static inline struct zbud_page *handle_to_zbud_page(unsigned long handle)
> +{
> + return (struct zbud_page *)(virt_to_page(handle));
> +}
> +
> +/* Returns the number of free chunks in a zbud page */
> +static inline int num_free_chunks(struct zbud_page *zbpage)
> +{
> + /*
> + * Rather than branch for different situations, just use the fact that
> + * free buddies have a length of zero to simplify everything.
> + */
> + return NCHUNKS - zbpage->first_chunks - zbpage->last_chunks;
> +}
> +
> +/*****************
> + * API Functions
> +*****************/
> +/**
> + * zbud_create_pool() - create a new zbud pool
> + * @gfp: gfp flags when allocating the zbud pool structure
> + * @ops: user-defined operations for the zbud pool
> + *
> + * Return: pointer to the new zbud pool or NULL if the metadata allocation
> + * failed.
> + */
> +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
> +{
> + struct zbud_pool *pool;
> + int i;
> +
> + pool = kmalloc(sizeof(struct zbud_pool), gfp);
> + if (!pool)
> + return NULL;
> + spin_lock_init(&pool->lock);
> + for_each_unbuddied_list(i, 0)
> + INIT_LIST_HEAD(&pool->unbuddied[i]);
> + INIT_LIST_HEAD(&pool->buddied);
> + INIT_LIST_HEAD(&pool->lru);
> + atomic_set(&pool->pages_nr, 0);
> + pool->ops = ops;
> + return pool;
> +}
> +EXPORT_SYMBOL_GPL(zbud_create_pool);
> +

Why the export? It doesn't look like this thing is going to be consumed
by modules.

> +/**
> + * zbud_destroy_pool() - destroys an existing zbud pool
> + * @pool: the zbud pool to be destroyed
> + */
> +void zbud_destroy_pool(struct zbud_pool *pool)
> +{
> + kfree(pool);
> +}
> +EXPORT_SYMBOL_GPL(zbud_destroy_pool);
> +
> +/**
> + * zbud_alloc() - allocates a region of a given size
> + * @pool: zbud pool from which to allocate
> + * @size: size in bytes of the desired allocation
> + * @gfp: gfp flags used if the pool needs to grow
> + * @handle: handle of the new allocation
> + *
> + * This function will attempt to find a free region in the pool large
> + * enough to satisfy the allocation request. First, it tries to use
> + * free space in the most recently used zbud page, at the beginning of
> + * the pool LRU list. If that zbud page is full or doesn't have the
> + * required free space, a best fit search of the unbuddied lists is
> + * performed. If no suitable free region is found, then a new page
> + * is allocated and added to the pool to satisfy the request.
> + *
> + * gfp should not set __GFP_HIGHMEM as highmem pages cannot be used
> + * as zbud pool pages.
> + *
> + * Return: 0 if success and handle is set, otherwise -EINVAL is the size or
> + * gfp arguments are invalid or -ENOMEM if the pool was unable to allocate
> + * a new page.
> + */
> +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> + unsigned long *handle)
> +{
> + int chunks, i, freechunks;
> + struct zbud_page *zbpage = NULL;
> + enum buddy bud;
> + struct page *page;
> +
> + if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
> + return -EINVAL;
> + chunks = size_to_chunks(size);
> + spin_lock(&pool->lock);
> +
> + /*
> + * First, try to use the zbpage we last used (at the head of the
> + * LRU) to increase LRU locality of the buddies. This is first fit.
> + */
> + if (!list_empty(&pool->lru)) {
> + zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
> + if (num_free_chunks(zbpage) >= chunks) {
> + if (zbpage->first_chunks == 0) {
> + list_del(&zbpage->buddy);
> + bud = FIRST;
> + goto found;
> + }
> + if (zbpage->last_chunks == 0) {
> + list_del(&zbpage->buddy);
> + bud = LAST;
> + goto found;
> + }
> + }
> + }
> +
> + /* Second, try to find an unbuddied zbpage. This is best fit. */

No it isn't, it's also first fit.

Give for_each_unbuddied_list() additional smarts to always start with
the last zbpage that was used and collapse these two block of code
together and call it first-fit.

> + zbpage = NULL;
> + for_each_unbuddied_list(i, chunks) {
> + if (!list_empty(&pool->unbuddied[i])) {
> + zbpage = list_first_entry(&pool->unbuddied[i],
> + struct zbud_page, buddy);
> + list_del(&zbpage->buddy);
> + if (zbpage->first_chunks == 0)
> + bud = FIRST;
> + else
> + bud = LAST;
> + goto found;
> + }
> + }
> +
> + /* Lastly, couldn't find unbuddied zbpage, create new one */
> + spin_unlock(&pool->lock);
> + page = alloc_page(gfp);
> + if (!page)
> + return -ENOMEM;
> + spin_lock(&pool->lock);
> + atomic_inc(&pool->pages_nr);
> + zbpage = init_zbud_page(page);
> + bud = FIRST;
> +

What bounds the size of the pool? Maybe a higher layer does but should the
higher layer set the maximum size and enforce it here instead? That way the
higher layer does not need to know that the allocator is dealing with pages.

> +found:
> + if (bud == FIRST)
> + zbpage->first_chunks = chunks;
> + else
> + zbpage->last_chunks = chunks;
> +
> + if (zbpage->first_chunks == 0 || zbpage->last_chunks == 0) {
> + /* Add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + } else {
> + /* Add to buddied list */
> + list_add(&zbpage->buddy, &pool->buddied);
> + }
> +
> + /* Add/move zbpage to beginning of LRU */
> + if (!list_empty(&zbpage->lru))
> + list_del(&zbpage->lru);
> + list_add(&zbpage->lru, &pool->lru);
> +
> + *handle = encode_handle(zbpage, bud);
> + spin_unlock(&pool->lock);
> +
> + return 0;
> +}
> +EXPORT_SYMBOL_GPL(zbud_alloc);
> +
> +/**
> + * zbud_free() - frees the allocation associated with the given handle
> + * @pool: pool in which the allocation resided
> + * @handle: handle associated with the allocation returned by zbud_alloc()
> + *
> + * In the case that the zbud page in which the allocation resides is under
> + * reclaim, as indicated by the PG_reclaim flag being set, this function
> + * only sets the first|last_chunks to 0. The page is actually freed
> + * once both buddies are evicted (see zbud_reclaim_page() below).
> + */
> +void zbud_free(struct zbud_pool *pool, unsigned long handle)
> +{
> + struct zbud_page *zbpage;
> + int freechunks;
> +
> + spin_lock(&pool->lock);
> + zbpage = handle_to_zbud_page(handle);
> +
> + /* If first buddy, handle will be page aligned */
> + if (handle & ~PAGE_MASK)
> + zbpage->last_chunks = 0;
> + else
> + zbpage->first_chunks = 0;
> +
> + if (PageReclaim(&zbpage->page)) {
> + /* zbpage is under reclaim, reclaim will free */
> + spin_unlock(&pool->lock);
> + return;
> + }
> +

This implies that it is possible for a zpage to get freed twice. That
sounds wrong. It sounds like a page being reclaimed should be isolated
from other lists that makes it accessible similar to how normal pages are
isolated from the LRU and then freed.

> + /* Remove from existing buddy list */
> + list_del(&zbpage->buddy);
> +
> + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> + /* zbpage is empty, free */
> + list_del(&zbpage->lru);
> + __free_page(reset_zbud_page(zbpage));
> + atomic_dec(&pool->pages_nr);
> + } else {
> + /* Add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + }
> +
> + spin_unlock(&pool->lock);
> +}
> +EXPORT_SYMBOL_GPL(zbud_free);
> +
> +#define list_tail_entry(ptr, type, member) \
> + list_entry((ptr)->prev, type, member)
> +
> +/**
> + * zbud_reclaim_page() - evicts allocations from a pool page and frees it
> + * @pool: pool from which a page will attempt to be evicted
> + * @retires: number of pages on the LRU list for which eviction will
> + * be attempted before failing
> + *
> + * zbud reclaim is different from normal system reclaim in that the reclaim is
> + * done from the bottom, up. This is because only the bottom layer, zbud, has
> + * information on how the allocations are organized within each zbud page. This
> + * has the potential to create interesting locking situations between zbud and
> + * the user, however.
> + *
> + * To avoid these, this is how zbud_reclaim_page() should be called:
> +
> + * The user detects a page should be reclaimed and calls zbud_reclaim_page().
> + * zbud_reclaim_page() will remove a zbud page from the pool LRU list and call
> + * the user-defined eviction handler with the pool and handle as arguments.
> + *
> + * If the handle can not be evicted, the eviction handler should return
> + * non-zero. zbud_reclaim_page() will add the zbud page back to the
> + * appropriate list and try the next zbud page on the LRU up to
> + * a user defined number of retries.
> + *
> + * If the handle is successfully evicted, the eviction handler should
> + * return 0 _and_ should have called zbud_free() on the handle. zbud_free()
> + * contains logic to delay freeing the page if the page is under reclaim,
> + * as indicated by the setting of the PG_reclaim flag on the underlying page.
> + *
> + * If all buddies in the zbud page are successfully evicted, then the
> + * zbud page can be freed.
> + *
> + * Returns: 0 if page is successfully freed, otherwise -EINVAL if there are
> + * no pages to evict or an eviction handler is not registered, -EAGAIN if
> + * the retry limit was hit.
> + */
> +int zbud_reclaim_page(struct zbud_pool *pool, unsigned int retries)
> +{
> + int i, ret, freechunks;
> + struct zbud_page *zbpage;
> + unsigned long first_handle = 0, last_handle = 0;
> +
> + spin_lock(&pool->lock);
> + if (!pool->ops || !pool->ops->evict || list_empty(&pool->lru) ||
> + retries == 0) {
> + spin_unlock(&pool->lock);
> + return -EINVAL;
> + }
> + for (i = 0; i < retries; i++) {
> + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> + list_del(&zbpage->lru);
> + list_del(&zbpage->buddy);
> + /* Protect zbpage against free */
> + SetPageReclaim(&zbpage->page);

Why not isolated it instead of using a page flag?

> + /*
> + * We need encode the handles before unlocking, since we can
> + * race with free that will set (first|last)_chunks to 0
> + */
> + first_handle = 0;
> + last_handle = 0;
> + if (zbpage->first_chunks)
> + first_handle = encode_handle(zbpage, FIRST);
> + if (zbpage->last_chunks)
> + last_handle = encode_handle(zbpage, LAST);
> + spin_unlock(&pool->lock);
> +
> + /* Issue the eviction callback(s) */
> + if (first_handle) {
> + ret = pool->ops->evict(pool, first_handle);
> + if (ret)
> + goto next;
> + }
> + if (last_handle) {
> + ret = pool->ops->evict(pool, last_handle);
> + if (ret)
> + goto next;
> + }
> +next:
> + spin_lock(&pool->lock);
> + ClearPageReclaim(&zbpage->page);
> + if (zbpage->first_chunks == 0 && zbpage->last_chunks == 0) {
> + /*
> + * Both buddies are now free, free the zbpage and
> + * return success.
> + */
> + __free_page(reset_zbud_page(zbpage));
> + atomic_dec(&pool->pages_nr);
> + spin_unlock(&pool->lock);
> + return 0;
> + } else if (zbpage->first_chunks == 0 ||
> + zbpage->last_chunks == 0) {
> + /* add to unbuddied list */
> + freechunks = num_free_chunks(zbpage);
> + list_add(&zbpage->buddy, &pool->unbuddied[freechunks]);
> + } else {
> + /* add to buddied list */
> + list_add(&zbpage->buddy, &pool->buddied);
> + }
> +
> + /* add to beginning of LRU */
> + list_add(&zbpage->lru, &pool->lru);
> + }
> + spin_unlock(&pool->lock);
> + return -EAGAIN;
> +}
> +EXPORT_SYMBOL_GPL(zbud_reclaim_page);
> +
> +/**
> + * zbud_map() - maps the allocation associated with the given handle
> + * @pool: pool in which the allocation resides
> + * @handle: handle associated with the allocation to be mapped
> + *
> + * While trivial for zbud, the mapping functions for others allocators
> + * implementing this allocation API could have more complex information encoded
> + * in the handle and could create temporary mappings to make the data
> + * accessible to the user.
> + *
> + * Returns: a pointer to the mapped allocation
> + */
> +void *zbud_map(struct zbud_pool *pool, unsigned long handle)
> +{
> + return (void *)(handle);
> +}
> +EXPORT_SYMBOL_GPL(zbud_map);
> +
> +/**
> + * zbud_unmap() - maps the allocation associated with the given handle
> + * @pool: pool in which the allocation resides
> + * @handle: handle associated with the allocation to be unmapped
> + */
> +void zbud_unmap(struct zbud_pool *pool, unsigned long handle)
> +{
> +}
> +EXPORT_SYMBOL_GPL(zbud_unmap);
> +
> +/**
> + * zbud_get_pool_size() - gets the zbud pool size in pages
> + * @pool: pool whose size is being queried
> + *
> + * Returns: size in pages of the given pool
> + */
> +int zbud_get_pool_size(struct zbud_pool *pool)
> +{
> + return atomic_read(&pool->pages_nr);
> +}
> +EXPORT_SYMBOL_GPL(zbud_get_pool_size);
> +
> +static int __init init_zbud(void)
> +{
> + /* Make sure we aren't overflowing the underlying struct page */
> + BUILD_BUG_ON(sizeof(struct zbud_page) != sizeof(struct page));
> + /* Make sure we can represent any chunk offset with a u16 */
> + BUILD_BUG_ON(sizeof(u16) * BITS_PER_BYTE < PAGE_SHIFT - CHUNK_SHIFT);
> + pr_info("loaded\n");
> + return 0;
> +}
> +
> +static void __exit exit_zbud(void)
> +{
> + pr_info("unloaded\n");
> +}
> +
> +module_init(init_zbud);
> +module_exit(exit_zbud);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> +MODULE_DESCRIPTION("Buddy Allocator for Compressed Pages");
> --
> 1.7.9.5
>

--
Mel Gorman
SUSE Labs

2013-05-17 16:04:40

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 4/4] zswap: add documentation

On Mon, May 13, 2013 at 07:40:03AM -0500, Seth Jennings wrote:
> This patch adds the documentation file for the zswap functionality
>
> Signed-off-by: Seth Jennings <[email protected]>
> ---
> Documentation/vm/zswap.txt | 72 ++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 72 insertions(+)
> create mode 100644 Documentation/vm/zswap.txt
>
> diff --git a/Documentation/vm/zswap.txt b/Documentation/vm/zswap.txt
> new file mode 100644
> index 0000000..88384b3
> --- /dev/null
> +++ b/Documentation/vm/zswap.txt
> @@ -0,0 +1,72 @@
> +Overview:
> +
> +Zswap is a lightweight compressed cache for swap pages. It takes pages that are
> +in the process of being swapped out and attempts to compress them into a
> +dynamically allocated RAM-based memory pool. If this process is successful,
> +the writeback to the swap device is deferred and, in many cases, avoided
> +completely.? This results in a significant I/O reduction and performance gains
> +for systems that are swapping.
> +

*Potentially* reduces IO and *potentially* shows performance gains. If the
system is swap trashing, this may make things worse as you're generating
the same amount of IO but having to compress/decompress as well. If there
is less physical memory available because zswap pool is fragmented then an
application may be pushed to swap prematurely and again, the performance
is worse. Don't oversell this and the comment applies throughout the
documentation.

I also think it should be marked with a bit fat warning that it's a WIP
and an additional warning that the performance characteristics are very
heavily workload dependant.

--
Mel Gorman
SUSE Labs

2013-05-17 16:54:29

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Mon, May 13, 2013 at 07:40:02AM -0500, Seth Jennings wrote:
> zswap is a thin compression backend for frontswap. It receives pages from
> frontswap and attempts to store them in a compressed memory pool, resulting in
> an effective partial memory reclaim and dramatically reduced swap device I/O.
>

potentially reduces IO. No guarantees.

> Additionally, in most cases, pages can be retrieved from this compressed store
> much more quickly than reading from tradition swap devices resulting in faster
> performance for many workloads.
>

While this is likely, it's also not necessarily true if the swap device
is particularly fast. Also, swap devices can be asynchronously written,
is the same true for zswap? I doubt it as I would expect the compression
operation to slow down pages being added to swap cache.

> It also has support for evicting swap pages that are currently compressed in
> zswap to the swap device on an LRU(ish) basis.

I know I initially suggested an LRU but don't worry about this thing
being an LRU too much. A FIFO list would be just fine as the pages are
presumably idle if they ended up in zswap in the first place.

> This functionality is very
> important and make zswap a true cache in that, once the cache is full or can't
> grow due to memory pressure, the oldest pages can be moved out of zswap to the
> swap device so newer pages can be compressed and stored in zswap.
>
> This patch adds the zswap driver to mm/
>
> Signed-off-by: Seth Jennings <[email protected]>
> ---
> mm/Kconfig | 15 +
> mm/Makefile | 1 +
> mm/zswap.c | 952 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
> 3 files changed, 968 insertions(+)
> create mode 100644 mm/zswap.c
>
> diff --git a/mm/Kconfig b/mm/Kconfig
> index 908f41b..4042e07 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -487,3 +487,18 @@ config ZBUD
> While this design limits storage density, it has simple and
> deterministic reclaim properties that make it preferable to a higher
> density approach when reclaim will be used.
> +
> +config ZSWAP
> + bool "In-kernel swap page compression"
> + depends on FRONTSWAP && CRYPTO
> + select CRYPTO_LZO
> + select ZBUD
> + default n
> + help
> + Zswap is a backend for the frontswap mechanism in the VMM.
> + It receives pages from frontswap and attempts to store them
> + in a compressed memory pool, resulting in an effective
> + partial memory reclaim. In addition, pages and be retrieved
> + from this compressed store much faster than most tradition
> + swap devices resulting in reduced I/O and faster performance
> + for many workloads.
> diff --git a/mm/Makefile b/mm/Makefile
> index 95f0197..f008033 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -32,6 +32,7 @@ obj-$(CONFIG_HAVE_MEMBLOCK) += memblock.o
> obj-$(CONFIG_BOUNCE) += bounce.o
> obj-$(CONFIG_SWAP) += page_io.o swap_state.o swapfile.o
> obj-$(CONFIG_FRONTSWAP) += frontswap.o
> +obj-$(CONFIG_ZSWAP) += zswap.o
> obj-$(CONFIG_HAS_DMA) += dmapool.o
> obj-$(CONFIG_HUGETLBFS) += hugetlb.o
> obj-$(CONFIG_NUMA) += mempolicy.o
> diff --git a/mm/zswap.c b/mm/zswap.c
> new file mode 100644
> index 0000000..b1070ca
> --- /dev/null
> +++ b/mm/zswap.c
> @@ -0,0 +1,952 @@
> +/*
> + * zswap.c - zswap driver file
> + *
> + * zswap is a backend for frontswap that takes pages that are in the
> + * process of being swapped out and attempts to compress them and store
> + * them in a RAM-based memory pool. This results in a significant I/O
> + * reduction on the real swap device and, in the case of a slow swap
> + * device, can also improve workload performance.
> + *
> + * Copyright (C) 2012 Seth Jennings <[email protected]>
> + *
> + * This program is free software; you can redistribute it and/or
> + * modify it under the terms of the GNU General Public License
> + * as published by the Free Software Foundation; either version 2
> + * of the License, or (at your option) any later version.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + * GNU General Public License for more details.
> +*/
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/module.h>
> +#include <linux/cpu.h>
> +#include <linux/highmem.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/types.h>
> +#include <linux/atomic.h>
> +#include <linux/frontswap.h>
> +#include <linux/rbtree.h>
> +#include <linux/swap.h>
> +#include <linux/crypto.h>
> +#include <linux/mempool.h>
> +#include <linux/zbud.h>
> +
> +#include <linux/mm_types.h>
> +#include <linux/page-flags.h>
> +#include <linux/swapops.h>
> +#include <linux/writeback.h>
> +#include <linux/pagemap.h>
> +
> +/*********************************
> +* statistics
> +**********************************/
> +/* Number of memory pages used by the compressed pool */
> +static atomic_t zswap_pool_pages = ATOMIC_INIT(0);

They underlying allocator should be tracking the number of physical
pages used, not this layer.

> +/* The number of compressed pages currently stored in zswap */
> +static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
> +
> +/*
> + * The statistics below are not protected from concurrent access for
> + * performance reasons so they may not be a 100% accurate. However,
> + * they do provide useful information on roughly how many times a
> + * certain event is occurring.
> +*/
> +static u64 zswap_pool_limit_hit;
> +static u64 zswap_written_back_pages;
> +static u64 zswap_reject_reclaim_fail;
> +static u64 zswap_reject_compress_poor;
> +static u64 zswap_reject_alloc_fail;
> +static u64 zswap_reject_kmemcache_fail;
> +static u64 zswap_duplicate_entry;
> +

Document what these mean.

> +/*********************************
> +* tunables
> +**********************************/
> +/* Enable/disable zswap (disabled by default, fixed at boot for now) */
> +static bool zswap_enabled;

read_mostly

> +module_param_named(enabled, zswap_enabled, bool, 0);
> +
> +/* Compressor to be used by zswap (fixed at boot for now) */
> +#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
> +static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> +module_param_named(compressor, zswap_compressor, charp, 0);
> +
> +/* The maximum percentage of memory that the compressed pool can occupy */
> +static unsigned int zswap_max_pool_percent = 20;
> +module_param_named(max_pool_percent,
> + zswap_max_pool_percent, uint, 0644);
> +

This will need additional love in the future. If you have an 8 node machine
then zswap pool could completely exhaust a single NUMA node with this
parameter. This is pretty much a big fat hammer that stops zswap getting
compltely out of control and taking over the system but it'll need some
sort of sensible automatic resizing based on system activity in the future.
It's not an obstacle to merging because you have to start somewhere but
the fixed-pool size thing is fugly and you should plan on putting it down
in the future.

> +/*
> + * Maximum compression ratio, as as percentage, for an acceptable
> + * compressed page. Any pages that do not compress by at least
> + * this ratio will be rejected.
> +*/
> +static unsigned int zswap_max_compression_ratio = 80;
> +module_param_named(max_compression_ratio,
> + zswap_max_compression_ratio, uint, 0644);
> +

I would be very surprised if a user wanted to tune this. What is a sensible
recommendation for it? I don't think you can give one because it depends
entirely on the workload and the current system state. A good value for
one day may be a bad choice the next day if a backup takes place or the
workload changes pattern frequently. As there is no sensible recommendation
for this value, just don't expose it to userspace at all.

I guess you could apply the same critism to the suggestion that NCHUNKS
be tunable but that has only two settings really. The default and 2 if
the pool is continually fragmented.

> +/*********************************
> +* compression functions
> +**********************************/
> <SNIP>

I'm glossed over a lot of this. It looks fairly similar to what was reviewed
before and I'm assuming there are no major changes. Much of it is in the
category of "it'll either work or fail spectacularly early in the lifetime
of the system" and I'm assuming you tested this. Note that the comments
are out of sync with the structures. Fix that.

> +/*********************************
> +* helpers
> +**********************************/
> +static inline bool zswap_is_full(void)
> +{
> + int pool_pages = atomic_read(&zswap_pool_pages);

Does this thing really have to be an atomic? Why not move it into the tree
structure, protect it with the tree lock and then sum the individual counts
when checking if zswap_is_full? It'll be a little race but not much more
so than using atomics outside of a lock like this.

> + return (totalram_pages * zswap_max_pool_percent / 100 < pool_pages);
> +}
> +
> +/*
> + * Carries out the common pattern of freeing and entry's zsmalloc allocation,
> + * freeing the entry itself, and decrementing the number of stored pages.
> + */
> +static void zswap_free_entry(struct zswap_tree *tree, struct zswap_entry *entry)
> +{
> + zbud_free(tree->pool, entry->handle);
> + zswap_entry_cache_free(entry);
> + atomic_dec(&zswap_stored_pages);
> + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> +}
> +
> +/*********************************
> +* writeback code
> +**********************************/
> +/* return enum for zswap_get_swap_cache_page */
> +enum zswap_get_swap_ret {
> + ZSWAP_SWAPCACHE_NEW,
> + ZSWAP_SWAPCACHE_EXIST,
> + ZSWAP_SWAPCACHE_NOMEM
> +};
> +
> +/*
> + * zswap_get_swap_cache_page
> + *
> + * This is an adaption of read_swap_cache_async()
> + *
> + * This function tries to find a page with the given swap entry
> + * in the swapper_space address space (the swap cache). If the page
> + * is found, it is returned in retpage. Otherwise, a page is allocated,
> + * added to the swap cache, and returned in retpage.
> + *
> + * If success, the swap cache page is returned in retpage
> + * Returns 0 if page was already in the swap cache, page is not locked
> + * Returns 1 if the new page needs to be populated, page is locked
> + * Returns <0 on error
> + */

Still not massively happy that this is duplicating code from
read_swap_cache_async(). It's just begging for trouble. I do not have
suggestions on how it can be done cleanly at this time because I haven't
put the effort in.

> +static int zswap_get_swap_cache_page(swp_entry_t entry,
> + struct page **retpage)
> +{
> + struct page *found_page, *new_page = NULL;
> + struct address_space *swapper_space = &swapper_spaces[swp_type(entry)];
> + int err;
> +
> + *retpage = NULL;
> + do {
> + /*
> + * First check the swap cache. Since this is normally
> + * called after lookup_swap_cache() failed, re-calling
> + * that would confuse statistics.
> + */
> + found_page = find_get_page(swapper_space, entry.val);
> + if (found_page)
> + break;
> +
> + /*
> + * Get a new page to read into from swap.
> + */
> + if (!new_page) {
> + new_page = alloc_page(GFP_KERNEL);
> + if (!new_page)
> + break; /* Out of memory */
> + }
> +
> + /*
> + * call radix_tree_preload() while we can wait.
> + */
> + err = radix_tree_preload(GFP_KERNEL);
> + if (err)
> + break;
> +
> + /*
> + * Swap entry may have been freed since our caller observed it.
> + */
> + err = swapcache_prepare(entry);
> + if (err == -EEXIST) { /* seems racy */
> + radix_tree_preload_end();
> + continue;
> + }
> + if (err) { /* swp entry is obsolete ? */
> + radix_tree_preload_end();
> + break;
> + }
> +
> + /* May fail (-ENOMEM) if radix-tree node allocation failed. */
> + __set_page_locked(new_page);
> + SetPageSwapBacked(new_page);
> + err = __add_to_swap_cache(new_page, entry);
> + if (likely(!err)) {
> + radix_tree_preload_end();
> + lru_cache_add_anon(new_page);
> + *retpage = new_page;
> + return ZSWAP_SWAPCACHE_NEW;
> + }
> + radix_tree_preload_end();
> + ClearPageSwapBacked(new_page);
> + __clear_page_locked(new_page);
> + /*
> + * add_to_swap_cache() doesn't return -EEXIST, so we can safely
> + * clear SWAP_HAS_CACHE flag.
> + */
> + swapcache_free(entry, NULL);
> + } while (err != -ENOMEM);
> +
> + if (new_page)
> + page_cache_release(new_page);
> + if (!found_page)
> + return ZSWAP_SWAPCACHE_NOMEM;
> + *retpage = found_page;
> + return ZSWAP_SWAPCACHE_EXIST;
> +}
> +
> +/*
> + * Attempts to free and entry by adding a page to the swap cache,
> + * decompressing the entry data into the page, and issuing a
> + * bio write to write the page back to the swap device.
> + *
> + * This can be thought of as a "resumed writeback" of the page
> + * to the swap device. We are basically resuming the same swap
> + * writeback path that was intercepted with the frontswap_store()
> + * in the first place. After the page has been decompressed into
> + * the swap cache, the compressed version stored by zswap can be
> + * freed.
> + */
> +static int zswap_writeback_entry(struct zbud_pool *pool, unsigned long handle)
> +{
> + struct zswap_header *zhdr;
> + swp_entry_t swpentry;
> + struct zswap_tree *tree;
> + pgoff_t offset;
> + struct zswap_entry *entry;
> + struct page *page;
> + u8 *src, *dst;
> + unsigned int dlen;
> + int ret, refcount;
> + struct writeback_control wbc = {
> + .sync_mode = WB_SYNC_NONE,
> + };
> +
> + /* extract swpentry from data */
> + zhdr = zbud_map(pool, handle);
> + swpentry = zhdr->swpentry; /* here */
> + zbud_unmap(pool, handle);
> + tree = zswap_trees[swp_type(swpentry)];

This is going to further solidify the use of PTEs to store the swap file
and offset for swap pages that Hugh complained about at LSF/MM. It's
unfortunate but it's not like there is queue of people waiting to fix
that particular problem :(

> + offset = swp_offset(swpentry);
> + BUG_ON(pool != tree->pool);
> +
> + /* find and ref zswap entry */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was invalidated */
> + spin_unlock(&tree->lock);
> + return 0;
> + }
> + zswap_entry_get(entry);
> + spin_unlock(&tree->lock);
> + BUG_ON(offset != entry->offset);
> +
> + /* try to allocate swap cache page */
> + switch (zswap_get_swap_cache_page(swpentry, &page)) {
> + case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
> + ret = -ENOMEM;
> + goto fail;
> +

Yikes. So it's possible to fail a zpage writeback? Can this livelock? I
expect you are protected by a combination of the 20% memory limitation
and that it is likely that *some* file pages can be reclaimed but this
is going to cause a bug report eventually. Consider using a mempool to
guarantee that some writeback progress can always be made.

> + case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
> + /* page is already in the swap cache, ignore for now */
> + page_cache_release(page);
> + ret = -EEXIST;
> + goto fail;
> +
> + case ZSWAP_SWAPCACHE_NEW: /* page is locked */
> + /* decompress */
> + dlen = PAGE_SIZE;
> + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> + sizeof(struct zswap_header);
> + dst = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
> + entry->length, dst, &dlen);
> + kunmap_atomic(dst);
> + zbud_unmap(tree->pool, entry->handle);
> + BUG_ON(ret);
> + BUG_ON(dlen != PAGE_SIZE);
> +
> + /* page is up to date */
> + SetPageUptodate(page);
> + }
> +
> + /* start writeback */
> + SetPageReclaim(page);
> + __swap_writepage(page, &wbc, end_swap_bio_write);
> + page_cache_release(page);
> + zswap_written_back_pages++;
> +

SetPageReclaim? Why?. If the page is under writeback then why do you not
mark it as that? Do not free pages that are currently under writeback
obviously. It's likely that it was PageWriteback you wanted in zbud.c too.


> + spin_lock(&tree->lock);
> +
> + /* drop local reference */
> + zswap_entry_put(entry);
> + /* drop the initial reference from entry creation */
> + refcount = zswap_entry_put(entry);
> +
> + /*
> + * There are three possible values for refcount here:
> + * (1) refcount is 1, load is in progress, unlink from rbtree,
> + * load will free
> + * (2) refcount is 0, (normal case) entry is valid,
> + * remove from rbtree and free entry
> + * (3) refcount is -1, invalidate happened during writeback,
> + * free entry
> + */
> + if (refcount >= 0) {
> + /* no invalidate yet, remove from rbtree */
> + rb_erase(&entry->rbnode, &tree->rbroot);
> + }
> + spin_unlock(&tree->lock);
> + if (refcount <= 0) {
> + /* free the entry */
> + zswap_free_entry(tree, entry);
> + return 0;
> + }
> + return -EAGAIN;
> +
> +fail:
> + spin_lock(&tree->lock);
> + zswap_entry_put(entry);
> + spin_unlock(&tree->lock);
> + return ret;
> +}
> +
> +/*********************************
> +* frontswap hooks
> +**********************************/
> +/* attempts to compress and store an single page */
> +static int zswap_frontswap_store(unsigned type, pgoff_t offset,
> + struct page *page)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry, *dupentry;
> + int ret;
> + unsigned int dlen = PAGE_SIZE, len;
> + unsigned long handle;
> + char *buf;
> + u8 *src, *dst;
> + struct zswap_header *zhdr;
> +
> + if (!tree) {
> + ret = -ENODEV;
> + goto reject;
> + }
> +
> + /* reclaim space if needed */
> + if (zswap_is_full()) {
> + zswap_pool_limit_hit++;
> + if (zbud_reclaim_page(tree->pool, 8)) {
> + zswap_reject_reclaim_fail++;
> + ret = -ENOMEM;
> + goto reject;
> + }
> + }
> +

If the allocator layer handled the sizing limitations then you could defer
the size checks until it calls alloc_page. From a layering perspective
this would be a hell of a lot cleaner. As it is, this layer has excessive
knowledge of the zbud layer which feels wrong.

> + /* allocate entry */
> + entry = zswap_entry_cache_alloc(GFP_KERNEL);
> + if (!entry) {
> + zswap_reject_kmemcache_fail++;
> + ret = -ENOMEM;
> + goto reject;
> + }
> +
> + /* compress */
> + dst = get_cpu_var(zswap_dstmem);
> + src = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_COMPRESS, src, PAGE_SIZE, dst, &dlen);
> + kunmap_atomic(src);
> + if (ret) {
> + ret = -EINVAL;
> + goto freepage;
> + }
> + len = dlen + sizeof(struct zswap_header);
> + if ((len * 100 / PAGE_SIZE) > zswap_max_compression_ratio) {
> + zswap_reject_compress_poor++;
> + ret = -E2BIG;
> + goto freepage;
> + }
> +
> + /* store */
> + ret = zbud_alloc(tree->pool, len, __GFP_NORETRY | __GFP_NOWARN,
> + &handle);

You do all the compression work and then check if you can store it?
It's harmless, but it's a little silly. Do the alloc work first and push
the sizing checks down a layer to the time you call alloc_pages.

> + if (ret) {
> + zswap_reject_alloc_fail++;
> + goto freepage;
> + }
> + zhdr = zbud_map(tree->pool, handle);
> + zhdr->swpentry = swp_entry(type, offset);
> + buf = (u8 *)(zhdr + 1);
> + memcpy(buf, dst, dlen);
> + zbud_unmap(tree->pool, handle);
> + put_cpu_var(zswap_dstmem);
> +
> + /* populate entry */
> + entry->offset = offset;
> + entry->handle = handle;
> + entry->length = dlen;
> +
> + /* map */
> + spin_lock(&tree->lock);
> + do {
> + ret = zswap_rb_insert(&tree->rbroot, entry, &dupentry);
> + if (ret == -EEXIST) {
> + zswap_duplicate_entry++;
> + /* remove from rbtree */
> + rb_erase(&dupentry->rbnode, &tree->rbroot);
> + if (!zswap_entry_put(dupentry)) {
> + /* free */
> + zswap_free_entry(tree, dupentry);
> + }
> + }
> + } while (ret == -EEXIST);
> + spin_unlock(&tree->lock);
> +
> + /* update stats */
> + atomic_inc(&zswap_stored_pages);
> + atomic_set(&zswap_pool_pages, zbud_get_pool_size(tree->pool));
> +
> + return 0;
> +
> +freepage:
> + put_cpu_var(zswap_dstmem);
> + zswap_entry_cache_free(entry);
> +reject:
> + return ret;
> +}
> +
> +/*
> + * returns 0 if the page was successfully decompressed
> + * return -1 on entry not found or error
> +*/
> +static int zswap_frontswap_load(unsigned type, pgoff_t offset,
> + struct page *page)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry;
> + u8 *src, *dst;
> + unsigned int dlen;
> + int refcount, ret;
> +
> + /* find */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was written back */
> + spin_unlock(&tree->lock);
> + return -1;
> + }
> + zswap_entry_get(entry);
> + spin_unlock(&tree->lock);
> +
> + /* decompress */
> + dlen = PAGE_SIZE;
> + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> + sizeof(struct zswap_header);
> + dst = kmap_atomic(page);
> + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src, entry->length,
> + dst, &dlen);
> + kunmap_atomic(dst);
> + zbud_unmap(tree->pool, entry->handle);
> + BUG_ON(ret);
> +
> + spin_lock(&tree->lock);
> + refcount = zswap_entry_put(entry);
> + if (likely(refcount)) {
> + spin_unlock(&tree->lock);
> + return 0;
> + }
> + spin_unlock(&tree->lock);
> +
> + /*
> + * We don't have to unlink from the rbtree because
> + * zswap_writeback_entry() or zswap_frontswap_invalidate page()
> + * has already done this for us if we are the last reference.
> + */
> + /* free */
> +
> + zswap_free_entry(tree, entry);
> +
> + return 0;
> +}
> +
> +/* invalidates a single page */
> +static void zswap_frontswap_invalidate_page(unsigned type, pgoff_t offset)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct zswap_entry *entry;
> + int refcount;
> +
> + /* find */
> + spin_lock(&tree->lock);
> + entry = zswap_rb_search(&tree->rbroot, offset);
> + if (!entry) {
> + /* entry was written back */
> + spin_unlock(&tree->lock);
> + return;
> + }
> +
> + /* remove from rbtree */
> + rb_erase(&entry->rbnode, &tree->rbroot);
> +
> + /* drop the initial reference from entry creation */
> + refcount = zswap_entry_put(entry);
> +
> + spin_unlock(&tree->lock);
> +
> + if (refcount) {
> + /* writeback in progress, writeback will free */
> + return;
> + }
> +
> + /* free */
> + zswap_free_entry(tree, entry);
> +}
> +
> +/* invalidates all pages for the given swap type */
> +static void zswap_frontswap_invalidate_area(unsigned type)
> +{
> + struct zswap_tree *tree = zswap_trees[type];
> + struct rb_node *node;
> + struct zswap_entry *entry;
> +
> + if (!tree)
> + return;
> +
> + /* walk the tree and free everything */
> + spin_lock(&tree->lock);
> + /*
> + * TODO: Even though this code should not be executed because
> + * the try_to_unuse() in swapoff should have emptied the tree,
> + * it is very wasteful to rebalance the tree after every
> + * removal when we are freeing the whole tree.
> + *
> + * If post-order traversal code is ever added to the rbtree
> + * implementation, it should be used here.
> + */
> + while ((node = rb_first(&tree->rbroot))) {
> + entry = rb_entry(node, struct zswap_entry, rbnode);
> + rb_erase(&entry->rbnode, &tree->rbroot);
> + zbud_free(tree->pool, entry->handle);
> + zswap_entry_cache_free(entry);
> + atomic_dec(&zswap_stored_pages);
> + }
> + tree->rbroot = RB_ROOT;
> + spin_unlock(&tree->lock);
> +}
> +
> +static struct zbud_ops zswap_zbud_ops = {
> + .evict = zswap_writeback_entry
> +};
> +
> +/* NOTE: this is called in atomic context from swapon and must not sleep */
> +static void zswap_frontswap_init(unsigned type)
> +{
> + struct zswap_tree *tree;
> +
> + tree = kzalloc(sizeof(struct zswap_tree), GFP_ATOMIC);
> + if (!tree)
> + goto err;
> + tree->pool = zbud_create_pool(GFP_NOWAIT, &zswap_zbud_ops);
> + if (!tree->pool)
> + goto freetree;
> + tree->rbroot = RB_ROOT;
> + spin_lock_init(&tree->lock);
> + tree->type = type;
> + zswap_trees[type] = tree;
> + return;
> +
> +freetree:
> + kfree(tree);
> +err:
> + pr_err("alloc failed, zswap disabled for swap type %d\n", type);
> +}
> +
> +static struct frontswap_ops zswap_frontswap_ops = {
> + .store = zswap_frontswap_store,
> + .load = zswap_frontswap_load,
> + .invalidate_page = zswap_frontswap_invalidate_page,
> + .invalidate_area = zswap_frontswap_invalidate_area,
> + .init = zswap_frontswap_init
> +};
> +
> +/*********************************
> +* debugfs functions
> +**********************************/
> +#ifdef CONFIG_DEBUG_FS
> +#include <linux/debugfs.h>
> +
> +static struct dentry *zswap_debugfs_root;
> +
> +static int __init zswap_debugfs_init(void)
> +{
> + if (!debugfs_initialized())
> + return -ENODEV;
> +
> + zswap_debugfs_root = debugfs_create_dir("zswap", NULL);
> + if (!zswap_debugfs_root)
> + return -ENOMEM;
> +
> + debugfs_create_u64("pool_limit_hit", S_IRUGO,
> + zswap_debugfs_root, &zswap_pool_limit_hit);
> + debugfs_create_u64("reject_reclaim_fail", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_reclaim_fail);
> + debugfs_create_u64("reject_alloc_fail", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_alloc_fail);
> + debugfs_create_u64("reject_kmemcache_fail", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_kmemcache_fail);
> + debugfs_create_u64("reject_compress_poor", S_IRUGO,
> + zswap_debugfs_root, &zswap_reject_compress_poor);
> + debugfs_create_u64("written_back_pages", S_IRUGO,
> + zswap_debugfs_root, &zswap_written_back_pages);
> + debugfs_create_u64("duplicate_entry", S_IRUGO,
> + zswap_debugfs_root, &zswap_duplicate_entry);
> + debugfs_create_atomic_t("pool_pages", S_IRUGO,
> + zswap_debugfs_root, &zswap_pool_pages);
> + debugfs_create_atomic_t("stored_pages", S_IRUGO,
> + zswap_debugfs_root, &zswap_stored_pages);
> +
> + return 0;
> +}
> +
> +static void __exit zswap_debugfs_exit(void)
> +{
> + debugfs_remove_recursive(zswap_debugfs_root);
> +}
> +#else
> +static inline int __init zswap_debugfs_init(void)
> +{
> + return 0;
> +}
> +
> +static inline void __exit zswap_debugfs_exit(void) { }
> +#endif
> +
> +/*********************************
> +* module init and exit
> +**********************************/
> +static int __init init_zswap(void)
> +{
> + if (!zswap_enabled)
> + return 0;
> +
> + pr_info("loading zswap\n");
> + if (zswap_entry_cache_create()) {
> + pr_err("entry cache creation failed\n");
> + goto error;
> + }
> + if (zswap_comp_init()) {
> + pr_err("compressor initialization failed\n");
> + goto compfail;
> + }
> + if (zswap_cpu_init()) {
> + pr_err("per-cpu initialization failed\n");
> + goto pcpufail;
> + }
> + frontswap_register_ops(&zswap_frontswap_ops);
> + if (zswap_debugfs_init())
> + pr_warn("debugfs initialization failed\n");
> + return 0;
> +pcpufail:
> + zswap_comp_exit();
> +compfail:
> + zswap_entry_cache_destory();
> +error:
> + return -ENOMEM;
> +}
> +/* must be late so crypto has time to come up */
> +late_initcall(init_zswap);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> +MODULE_DESCRIPTION("Compressed cache for swap pages");

I think there is still a lot of ugly in here so see what you can fix up
quickly. It's not mandatory to me that you get all this fixed up prior
to merging because it's long gone past the point where dealing with it
out-of-tree or in staging is going to work. By the time you address all the
concerns, it will have reached the point where it's too complex to review
and back to square one. At least if it's in mm/ it can be incrementally
developed but it should certainly start with a big fat warning that it's
a WIP. I wouldn't slap "ready for production" sticker on this just yet :/

--
Mel Gorman
SUSE Labs

2013-05-17 17:01:02

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Tue, May 14, 2013 at 09:37:08AM -0700, Dan Magenheimer wrote:
> > Can I get your ack on this pending the other changes?
>
> I'd like to hear Mel's feedback about this, but perhaps
> a compromise to allow for zswap merging would be to add
> something like the following to zswap's Kconfig comment:
>

I think there is a lot of ugly in there and potential for weird performance
bugs. I ran out of beans complaining about different parts during the
review but fixing it out of tree or in staging like it's been happening to
date has clearly not worked out at all. As starting points go, it could be
a hell of a lot worse. I do agree that it needs a big fat warning until
some of the ugly is beaten out of it. Requiring that it address all the
issues such as automatic pool sizing, NUMA issues, proper allocation prior
to merging will just end up with an unreviewable set of patches again so
lets just bite the bullet because at least there is a chance reviewers
can follow the incremental developments. Merging it to drivers will not
address anything IMO.

--
Mel Gorman
SUSE Labs

2013-05-19 20:52:35

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Fri, May 17, 2013 at 04:48:37PM +0100, Mel Gorman wrote:
> On Mon, May 13, 2013 at 07:40:01AM -0500, Seth Jennings wrote:
> > zbud is an special purpose allocator for storing compressed pages. It is
> > designed to store up to two compressed pages per physical page. While this
> > design limits storage density, it has simple and deterministic reclaim
> > properties that make it preferable to a higher density approach when reclaim
> > will be used.
> >
> > zbud works by storing compressed pages, or "zpages", together in pairs in a
> > single memory page called a "zbud page". The first buddy is "left
> > justifed" at the beginning of the zbud page, and the last buddy is "right
> > justified" at the end of the zbud page. The benefit is that if either
> > buddy is freed, the freed buddy space, coalesced with whatever slack space
> > that existed between the buddies, results in the largest possible free region
> > within the zbud page.
> >
> > zbud also provides an attractive lower bound on density. The ratio of zpages
> > to zbud pages can not be less than 1. This ensures that zbud can never "do
> > harm" by using more pages to store zpages than the uncompressed zpages would
> > have used on their own.
> >
> > This patch adds zbud to mm/ for later use by zswap.
> >
> > Signed-off-by: Seth Jennings <[email protected]>
>
> I'm not familiar with the code in staging/zcache/zbud.c and this looks
> like a rewrite but I'm curious, why was an almost complete rewrite
> necessary? The staging code looks like it had debugfs statistics and
> the like that would help figure how well the packing was working and so
> on. I guess it was probably because it was integrated tightly with other
> components in staging but could that not be torn out? I'm guessing you
> have a good reason but it'd be nice to see that in the changelog.

I'll add a bit about that.

<snip>
> > 4 files changed, 597 insertions(+)
> > + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> > + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> > + * into chunks allows organizing unbuddied zbud pages into a manageable number
> > + * of unbuddied lists according to the number of free chunks available in the
> > + * zbud page.
> > + *
>
> Fixing the size of the chunks at compile time is a very strict
> limitation! Distributions will have to make that decision for all workloads
> that might conceivably use zswap. Having the allocator only deal with pairs
> of pages limits the worst-case behaviour where reclaim can generate lots of
> IO to free a single physical page. However, the chunk size directly affects
> the fragmentation properties, both internal and external, of this thing.

> Once NCHUNKS is > 2 it is possible to create a workload that externally
> fragments this allocator such that each physical page only holds one
> compressed page. If this is a problem for a user then their only option
> is to rebuild the kernel which is not always possible.

You lost me here. Do you mean NCHUNKS > 2 or NCHUNKS_ORDER > 2?

My first guess is that the external fragmentation situation you are referring to
is a workload in which all pages compress to greater than half a page. If so,
then it doesn't matter what NCHUCNKS_ORDER is, there won't be any pages the
compress enough to fit in the < PAGE_SIZE/2 free space that remains in the
unbuddied zbud pages.

You might also be referring to the fact that if you set NCHUNKS_ORDER to 2
(i.e. there are 4 chunks per zbud page) and you receive an allocation for size
(3/4 * PAGE_SIZE) + 1, the allocator will use all 4 chunks for that allocation
and the rest of the zbud page is lost to internal fragmentation.

That is simply an argument for not choosing a small NCHUNKS_ORDER.

>
> Please make this configurable by a kernel boot parameter at least. At
> a glance it looks like only problem would be that you have to kmalloc
> unbuddied[NCHUNKS] in the pool structure but that is hardly of earth
> shattering difficulty. Make the variables read_mostly to avoid cache-line
> bouncing problems.

I am hesitant to make this a tunable without understanding why anyone would
want to tune it. It's hard to convey to a user what this tunable would do and
what effect it might have. I'm not saying that isn't such a situation.
I just don't see one didn't understand your case above.

>
> Finally, because a review would never be complete without a bitching
> session about names -- I don't like the name zbud. Buddy allocators take
> a large block of memory and split it iteratively (by halves for binary
> buddy allocators but there are variations) until it's a best fit for the
> allocation request. A key advantage of such schemes is fast searching for
> free holes. That's not what this allocator does and as the page allocator
> is a binary buddy allocator in Linux, calling this this a buddy allocator
> is a bit misleading. Looks like the existing zbud.c also has this problem
> but hey. This thing is a first-fit segmented free list allocator with
> sub-allocator properties in that it takes fixed-sized blocks as inputs and
> splits them into pairs, not a buddy allocator. That characterisation does
> not lend itself to a snappy name but calling it zpair or something would
> be slightly less misleading than calling it a buddy allocator.

I agree that is it not a buddy allocator and the name is misleading.
zpair is fine with me.

>
> First Fit Segmented-list Allocator for in-Kernel comprEssion (FFSAKE)? :/

Well played :) For real though, I think that First Fit Segmented-List is
the most accurate description. ffsl.c? Just a thought. I'm fine with
zpair too.

<snip>
> > +struct zbud_pool {
> > + spinlock_t lock;
> > + struct list_head unbuddied[NCHUNKS];
> > + struct list_head buddied;
> > + struct list_head lru;
> > + atomic_t pages_nr;
>
> There is no need for pages_nr to be atomic. It's always manipulated
> under the lock. I see that the atomic is exported so someone can read it
> that is outside the lock but they are goign to have to deal with races
> anyway. atomic does not magically protect them

True. I'll change it.

>
> Also, pages_nr does not appear to be the number of zbud pages in the pool,
> it's the number of zpages. You may want to report both for debugging
> purposes as if nr_zpages != 2 * nr_zbud_pages then zswap is using more
> physical pages than it should be.

No, pages_nr is the number of pool pages, not zpages. The number of zpages (or
allocations from zbud's point of view) is easily trackable by the user as it
does each allocation. What the user can not know is how many pages are in the
pool. Hence why zbud tracks this stat and makes it accessible via
zbud_get_pool_size().

In the case of zswap, the debugfs attributes stored_pages/pool_pages will give
you the density metric, albeit in a non-atomic way.

<snip>
> > +/* Initializes a zbud page from a newly allocated page */
> > +static inline struct zbud_page *init_zbud_page(struct page *page)
> > +{
> > + struct zbud_page *zbpage = (struct zbud_page *)page;
> > + zbpage->first_chunks = 0;
> > + zbpage->last_chunks = 0;
> > + INIT_LIST_HEAD(&zbpage->buddy);
> > + INIT_LIST_HEAD(&zbpage->lru);
> > + return zbpage;
> > +}
>
> No need to inline. Only has one caller so the compiler will figure it
> out.

Ok.

>
> > +
> > +/* Resets a zbud page so that it can be properly freed */
> > +static inline struct page *reset_zbud_page(struct zbud_page *zbpage)
> > +{
> > + struct page *page = &zbpage->page;
> > + set_page_private(page, 0);
> > + page->mapping = NULL;
> > + page->index = 0;
> > + page_mapcount_reset(page);
> > + init_page_count(page);
> > + INIT_LIST_HEAD(&page->lru);
> > + return page;
> > +}
>
> This is only used for freeing so call it free_zbud_page and have it call
> __free_page for clarity. Also, this is a bit long for inlining.

Ah yes, much cleaner.

>
> > +
> > +/*
> > + * Encodes the handle of a particular buddy within a zbud page
> > + * Pool lock should be held as this function accesses first|last_chunks
> > + */
> > +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> > + enum buddy bud)
> > +{
> > + unsigned long handle;
> > +
> > + /*
> > + * For now, the encoded handle is actually just the pointer to the data
> > + * but this might not always be the case. A little information hiding.
> > + */
> > + handle = (unsigned long)page_address(&zbpage->page);
> > + if (bud == FIRST)
> > + return handle;
> > + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> > + return handle;
> > +}
>
> Your handles are unsigned long and are addresses. Consider making it an
> opaque type so someone deferencing it would take a special kind of
> stupid.

My argument for keeping the handles as unsigned longs is a forward-looking
to the implementation of the pluggable allocator interface in zswap.
Typing the handles prevents the creation of an allocator neutral function
signature.

Maybe I'm overlooking an easy solution here.

<snip>
> > + * zbud_create_pool() - create a new zbud pool
> > + * @gfp: gfp flags when allocating the zbud pool structure
> > + * @ops: user-defined operations for the zbud pool
> > + *
> > + * Return: pointer to the new zbud pool or NULL if the metadata allocation
> > + * failed.
> > + */
> > +struct zbud_pool *zbud_create_pool(gfp_t gfp, struct zbud_ops *ops)
> > +{
> > + struct zbud_pool *pool;
> > + int i;
> > +
> > + pool = kmalloc(sizeof(struct zbud_pool), gfp);
> > + if (!pool)
> > + return NULL;
> > + spin_lock_init(&pool->lock);
> > + for_each_unbuddied_list(i, 0)
> > + INIT_LIST_HEAD(&pool->unbuddied[i]);
> > + INIT_LIST_HEAD(&pool->buddied);
> > + INIT_LIST_HEAD(&pool->lru);
> > + atomic_set(&pool->pages_nr, 0);
> > + pool->ops = ops;
> > + return pool;
> > +}
> > +EXPORT_SYMBOL_GPL(zbud_create_pool);
> > +
>
> Why the export? It doesn't look like this thing is going to be consumed
> by modules.

This is true for now, I'll remove them. Can always add them back later
and save additions to the KABI in the meantime.

<snip>
> > +int zbud_alloc(struct zbud_pool *pool, int size, gfp_t gfp,
> > + unsigned long *handle)
> > +{
> > + int chunks, i, freechunks;
> > + struct zbud_page *zbpage = NULL;
> > + enum buddy bud;
> > + struct page *page;
> > +
> > + if (size <= 0 || size > PAGE_SIZE || gfp & __GFP_HIGHMEM)
> > + return -EINVAL;
> > + chunks = size_to_chunks(size);
> > + spin_lock(&pool->lock);
> > +
> > + /*
> > + * First, try to use the zbpage we last used (at the head of the
> > + * LRU) to increase LRU locality of the buddies. This is first fit.
> > + */
> > + if (!list_empty(&pool->lru)) {
> > + zbpage = list_first_entry(&pool->lru, struct zbud_page, lru);
> > + if (num_free_chunks(zbpage) >= chunks) {
> > + if (zbpage->first_chunks == 0) {
> > + list_del(&zbpage->buddy);
> > + bud = FIRST;
> > + goto found;
> > + }
> > + if (zbpage->last_chunks == 0) {
> > + list_del(&zbpage->buddy);
> > + bud = LAST;
> > + goto found;
> > + }
> > + }
> > + }
> > +
> > + /* Second, try to find an unbuddied zbpage. This is best fit. */
>
> No it isn't, it's also first fit.

Ok.

>
> Give for_each_unbuddied_list() additional smarts to always start with
> the last zbpage that was used and collapse these two block of code
> together and call it first-fit.

I've removed the try the "last page used" logic since I am, at this
time, not able to demonstrate that it improves anything.

Without the contrast, I'll just refrain from any comment about the
fit type.

> > + zbpage = NULL;
> > + for_each_unbuddied_list(i, chunks) {
> > + if (!list_empty(&pool->unbuddied[i])) {
> > + zbpage = list_first_entry(&pool->unbuddied[i],
> > + struct zbud_page, buddy);
> > + list_del(&zbpage->buddy);
> > + if (zbpage->first_chunks == 0)
> > + bud = FIRST;
> > + else
> > + bud = LAST;
> > + goto found;
> > + }
> > + }
> > +
> > + /* Lastly, couldn't find unbuddied zbpage, create new one */
> > + spin_unlock(&pool->lock);
> > + page = alloc_page(gfp);
> > + if (!page)
> > + return -ENOMEM;
> > + spin_lock(&pool->lock);
> > + atomic_inc(&pool->pages_nr);
> > + zbpage = init_zbud_page(page);
> > + bud = FIRST;
> > +
>
> What bounds the size of the pool? Maybe a higher layer does but should the
> higher layer set the maximum size and enforce it here instead? That way the
> higher layer does not need to know that the allocator is dealing with pages.

I see your point. The higher layer would have to set the limit in some
units, likely pages, so it would be aware that zbud is using pages.

However, zswap (or any user) would probably make an initial determination of
the limit in pages. Then would have to register a notifier for anything that
could change the memory size (i.e. memory add/remove) and adjust the zbud
limit.

I guess a different way would be to set the zbud limit as a percentage, then
zbud could automatically adjust when the amount of ram changes, doing a
per-allocation limit check.

Any thoughts about those options?

<snip>
> > + spin_lock(&pool->lock);
> > + zbpage = handle_to_zbud_page(handle);
> > +
> > + /* If first buddy, handle will be page aligned */
> > + if (handle & ~PAGE_MASK)
> > + zbpage->last_chunks = 0;
> > + else
> > + zbpage->first_chunks = 0;
> > +
> > + if (PageReclaim(&zbpage->page)) {
> > + /* zbpage is under reclaim, reclaim will free */
> > + spin_unlock(&pool->lock);
> > + return;
> > + }
> > +
>
> This implies that it is possible for a zpage to get freed twice. That
> sounds wrong. It sounds like a page being reclaimed should be isolated
> from other lists that makes it accessible similar to how normal pages are
> isolated from the LRU and then freed.

No, a zpage will not be freed twice.

The problem is that even if zbud isolates the page in it's structures,
which it does now removing from the buddied/unbuddied and lru list, there is no
way to isolate it in the _users_ data structures. Once we release the pool
lock, a free could still come in from the user.

However, the user should have protections in place in it's eviction handler that
prevent two cases:

1) If the user entry associated with the allocation being evicted has already
been freed, the eviction handler should just return 0 (already freed)

2) If the user entry lookup in the eviction handler is successful, some
lock/refcount must protect the entry and its associated zbud allocation from
being freed while it is being evicted.

<snip>
> > + for (i = 0; i < retries; i++) {
> > + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> > + list_del(&zbpage->lru);
> > + list_del(&zbpage->buddy);
> > + /* Protect zbpage against free */
> > + SetPageReclaim(&zbpage->page);
>
> Why not isolated it instead of using a page flag?

Same reason as above, can't isolate in the user structure.

Thanks for the review!

Seth

2013-05-19 23:33:33

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 3/4] zswap: add to mm/

On Fri, May 17, 2013 at 05:54:18PM +0100, Mel Gorman wrote:
> On Mon, May 13, 2013 at 07:40:02AM -0500, Seth Jennings wrote:
> > zswap is a thin compression backend for frontswap. It receives pages from
> > frontswap and attempts to store them in a compressed memory pool, resulting in
> > an effective partial memory reclaim and dramatically reduced swap device I/O.
> >
>
> potentially reduces IO. No guarantees.

Sorry, I was in marketing mode I guess.

> > Additionally, in most cases, pages can be retrieved from this compressed store
> > much more quickly than reading from tradition swap devices resulting in faster
> > performance for many workloads.
> >
>
> While this is likely, it's also not necessarily true if the swap device
> is particularly fast. Also, swap devices can be asynchronously written,
> is the same true for zswap? I doubt it as I would expect the compression
> operation to slow down pages being added to swap cache.

Same here.

The compression happens synchronously at pageout() time, more precisely the
frontswap_store() in swap_writepage(). The advantage here is that pages
synchronously stored in zswap can be immediately reclaimed in
shrink_page_list().

>
> > It also has support for evicting swap pages that are currently compressed in
> > zswap to the swap device on an LRU(ish) basis.
>
> I know I initially suggested an LRU but don't worry about this thing
> being an LRU too much. A FIFO list would be just fine as the pages are
> presumably idle if they ended up in zswap in the first place.

The LRU stuff is already in zbud and doesn't add much complexity. It is
cheap and understandable so may as well do it I figure. You'll have to
select a page one way or another. May as well be consistent with the
rest of the MM.

<snip>
> > +/*********************************
> > +* statistics
> > +**********************************/
> > +/* Number of memory pages used by the compressed pool */
> > +static atomic_t zswap_pool_pages = ATOMIC_INIT(0);
>
> They underlying allocator should be tracking the number of physical
> pages used, not this layer.

zbud does track the number of pool pages. This variable just mirrors the zbud
value when it has the potential to change so that it is accessible in the zswap
debugfs.

However, since the conversion to zbud, this atomic isn't inc/dec anymore,
just set, so no need to be atomic.

>
> > +/* The number of compressed pages currently stored in zswap */
> > +static atomic_t zswap_stored_pages = ATOMIC_INIT(0);
> > +
> > +/*
> > + * The statistics below are not protected from concurrent access for
> > + * performance reasons so they may not be a 100% accurate. However,
> > + * they do provide useful information on roughly how many times a
> > + * certain event is occurring.
> > +*/
> > +static u64 zswap_pool_limit_hit;
> > +static u64 zswap_written_back_pages;
> > +static u64 zswap_reject_reclaim_fail;
> > +static u64 zswap_reject_compress_poor;
> > +static u64 zswap_reject_alloc_fail;
> > +static u64 zswap_reject_kmemcache_fail;
> > +static u64 zswap_duplicate_entry;
> > +
>
> Document what these mean.

Will do.

>
> > +/*********************************
> > +* tunables
> > +**********************************/
> > +/* Enable/disable zswap (disabled by default, fixed at boot for now) */
> > +static bool zswap_enabled;
>
> read_mostly

Yep.

>
> > +module_param_named(enabled, zswap_enabled, bool, 0);
> > +
> > +/* Compressor to be used by zswap (fixed at boot for now) */
> > +#define ZSWAP_COMPRESSOR_DEFAULT "lzo"
> > +static char *zswap_compressor = ZSWAP_COMPRESSOR_DEFAULT;
> > +module_param_named(compressor, zswap_compressor, charp, 0);
> > +
> > +/* The maximum percentage of memory that the compressed pool can occupy */
> > +static unsigned int zswap_max_pool_percent = 20;
> > +module_param_named(max_pool_percent,
> > + zswap_max_pool_percent, uint, 0644);
> > +
>
> This will need additional love in the future. If you have an 8 node machine
> then zswap pool could completely exhaust a single NUMA node with this
> parameter. This is pretty much a big fat hammer that stops zswap getting
> compltely out of control and taking over the system but it'll need some
> sort of sensible automatic resizing based on system activity in the future.
> It's not an obstacle to merging because you have to start somewhere but
> the fixed-pool size thing is fugly and you should plan on putting it down
> in the future.

Agreed, it is a starting point and making this policy better and NUMA-aware
is at the top of my TODO list.

>
> > +/*
> > + * Maximum compression ratio, as as percentage, for an acceptable
> > + * compressed page. Any pages that do not compress by at least
> > + * this ratio will be rejected.
> > +*/
> > +static unsigned int zswap_max_compression_ratio = 80;
> > +module_param_named(max_compression_ratio,
> > + zswap_max_compression_ratio, uint, 0644);
> > +
>
> I would be very surprised if a user wanted to tune this. What is a sensible
> recommendation for it? I don't think you can give one because it depends
> entirely on the workload and the current system state. A good value for
> one day may be a bad choice the next day if a backup takes place or the
> workload changes pattern frequently. As there is no sensible recommendation
> for this value, just don't expose it to userspace at all.

Agreed, this mattered more for zsmalloc. Upon reexamination, this should
be done in the allocator. If the allocator can't (optimally) store the
compressed page, it can just return -E2BIG and zswap will increment
zswap_reject_compress_poor.

>
> I guess you could apply the same critism to the suggestion that NCHUNKS
> be tunable but that has only two settings really. The default and 2 if
> the pool is continually fragmented.

I think you might be misunderstanding NCHUNKS. NCHUNKS is the number of
chunks per zbud page. If you set NCHUNKS to 2, zbud basically won't be
able to pair and buddy that is larger than PAGE_SIZE/2.

>
> > +/*********************************
> > +* compression functions
> > +**********************************/
> > <SNIP>
>
> I'm glossed over a lot of this. It looks fairly similar to what was reviewed
> before and I'm assuming there are no major changes. Much of it is in the
> category of "it'll either work or fail spectacularly early in the lifetime
> of the system" and I'm assuming you tested this. Note that the comments
> are out of sync with the structures. Fix that.

Yes, you said this before and I forgot to pick it up. Sorry :-/
>
> > +/*********************************
> > +* helpers
> > +**********************************/
> > +static inline bool zswap_is_full(void)
> > +{
> > + int pool_pages = atomic_read(&zswap_pool_pages);
>
> Does this thing really have to be an atomic? Why not move it into the tree
> structure, protect it with the tree lock and then sum the individual counts
> when checking if zswap_is_full? It'll be a little race but not much more
> so than using atomics outside of a lock like this.

When zswap was doing the accounting with zsmalloc it did need to be atomic
but not anymore. I'll fix it up.

> > + * zswap_get_swap_cache_page
> > + *
> > + * This is an adaption of read_swap_cache_async()
> > + *
> > + * This function tries to find a page with the given swap entry
> > + * in the swapper_space address space (the swap cache). If the page
> > + * is found, it is returned in retpage. Otherwise, a page is allocated,
> > + * added to the swap cache, and returned in retpage.
> > + *
> > + * If success, the swap cache page is returned in retpage
> > + * Returns 0 if page was already in the swap cache, page is not locked
> > + * Returns 1 if the new page needs to be populated, page is locked
> > + * Returns <0 on error
> > + */
>
> Still not massively happy that this is duplicating code from
> read_swap_cache_async(). It's just begging for trouble. I do not have
> suggestions on how it can be done cleanly at this time because I haven't
> put the effort in.

Yes, how to reuse the code here isn't a trivial thing, but I can look
again how if and how it could be done cleanly.

<snip>
> > + };
> > +
> > + /* extract swpentry from data */
> > + zhdr = zbud_map(pool, handle);
> > + swpentry = zhdr->swpentry; /* here */
> > + zbud_unmap(pool, handle);
> > + tree = zswap_trees[swp_type(swpentry)];
>
> This is going to further solidify the use of PTEs to store the swap file
> and offset for swap pages that Hugh complained about at LSF/MM. It's
> unfortunate but it's not like there is queue of people waiting to fix
> that particular problem :(

Yes, but there are a lot of places that will have to be updated I imagine.
This will just be one more. I for one, wouldn't mind undertaking that
improvement (swap entry abstraction layer). But that's for another day.


> > + offset = swp_offset(swpentry);
> > + BUG_ON(pool != tree->pool);
> > +
> > + /* find and ref zswap entry */
> > + spin_lock(&tree->lock);
> > + entry = zswap_rb_search(&tree->rbroot, offset);
> > + if (!entry) {
> > + /* entry was invalidated */
> > + spin_unlock(&tree->lock);
> > + return 0;
> > + }
> > + zswap_entry_get(entry);
> > + spin_unlock(&tree->lock);
> > + BUG_ON(offset != entry->offset);
> > +
> > + /* try to allocate swap cache page */
> > + switch (zswap_get_swap_cache_page(swpentry, &page)) {
> > + case ZSWAP_SWAPCACHE_NOMEM: /* no memory */
> > + ret = -ENOMEM;
> > + goto fail;
> > +
>
> Yikes. So it's possible to fail a zpage writeback? Can this livelock? I
> expect you are protected by a combination of the 20% memory limitation
> and that it is likely that *some* file pages can be reclaimed but this
> is going to cause a bug report eventually. Consider using a mempool to
> guarantee that some writeback progress can always be made.

If the reclaim fails here, then the overall store operation just fails and the
page is written to swap as if zswap wasn't there But this happens VERY rarely
since we are using GFP_KERNEL.

If you are talking about the allocation in zswap_get_swap_cache_page()
resulting in a swap_writepage() that calls back down this path I've never seen
that but can't explain exactly why it isn't possible.

What if we used GFP_NOIO, that way shrink_page_list() wouldn't swap out
addition pages in response to an allocation for zswap writeback? If the zone
is congested with dirty pages then this might fail more often. I'd have to try
it out.

What do you think? Or have I completely misunderstood your concern?

> > + case ZSWAP_SWAPCACHE_EXIST: /* page is unlocked */
> > + /* page is already in the swap cache, ignore for now */
> > + page_cache_release(page);
> > + ret = -EEXIST;
> > + goto fail;
> > +
> > + case ZSWAP_SWAPCACHE_NEW: /* page is locked */
> > + /* decompress */
> > + dlen = PAGE_SIZE;
> > + src = (u8 *)zbud_map(tree->pool, entry->handle) +
> > + sizeof(struct zswap_header);
> > + dst = kmap_atomic(page);
> > + ret = zswap_comp_op(ZSWAP_COMPOP_DECOMPRESS, src,
> > + entry->length, dst, &dlen);
> > + kunmap_atomic(dst);
> > + zbud_unmap(tree->pool, entry->handle);
> > + BUG_ON(ret);
> > + BUG_ON(dlen != PAGE_SIZE);
> > +
> > + /* page is up to date */
> > + SetPageUptodate(page);
> > + }
> > +
> > + /* start writeback */
> > + SetPageReclaim(page);
> > + __swap_writepage(page, &wbc, end_swap_bio_write);
> > + page_cache_release(page);
> > + zswap_written_back_pages++;
> > +
>
> SetPageReclaim? Why?. If the page is under writeback then why do you not
> mark it as that? Do not free pages that are currently under writeback
> obviously.

You're right, no need to set Reclaim here. Not sure why I had that there.
__swap_writeback() sets the writeback flag.

> It's likely that it was PageWriteback you wanted in zbud.c too.

In zbud, the reclaim flag is just being repurposed for internal use.

<snip>
> > + /* reclaim space if needed */
> > + if (zswap_is_full()) {
> > + zswap_pool_limit_hit++;
> > + if (zbud_reclaim_page(tree->pool, 8)) {
> > + zswap_reject_reclaim_fail++;
> > + ret = -ENOMEM;
> > + goto reject;
> > + }
> > + }
> > +
>
> If the allocator layer handled the sizing limitations then you could defer
> the size checks until it calls alloc_page. From a layering perspective
> this would be a hell of a lot cleaner. As it is, this layer has excessive
> knowledge of the zbud layer which feels wrong.

Ok. I responded to this in the zbud patch thread. Short rehash, yes it could
work. Just how will the limit be expressed (and updated if needed)?

> > + /* allocate entry */
> > + entry = zswap_entry_cache_alloc(GFP_KERNEL);
> > + if (!entry) {
> > + zswap_reject_kmemcache_fail++;
> > + ret = -ENOMEM;
> > + goto reject;
> > + }
> > +
> > + /* compress */
> > + dst = get_cpu_var(zswap_dstmem);
> > + src = kmap_atomic(page);
> > + ret = zswap_comp_op(ZSWAP_COMPOP_COMPRESS, src, PAGE_SIZE, dst, &dlen);
> > + kunmap_atomic(src);
> > + if (ret) {
> > + ret = -EINVAL;
> > + goto freepage;
> > + }
> > + len = dlen + sizeof(struct zswap_header);
> > + if ((len * 100 / PAGE_SIZE) > zswap_max_compression_ratio) {
> > + zswap_reject_compress_poor++;
> > + ret = -E2BIG;
> > + goto freepage;
> > + }
> > +
> > + /* store */
> > + ret = zbud_alloc(tree->pool, len, __GFP_NORETRY | __GFP_NOWARN,
> > + &handle);
>
> You do all the compression work and then check if you can store it?
> It's harmless, but it's a little silly. Do the alloc work first and push
> the sizing checks down a layer to the time you call alloc_pages.

You don't know how large the zbud allocation needs to be until after you've
actually compressed the page.

<snip>
> > +MODULE_LICENSE("GPL");
> > +MODULE_AUTHOR("Seth Jennings <[email protected]>");
> > +MODULE_DESCRIPTION("Compressed cache for swap pages");
>
> I think there is still a lot of ugly in here so see what you can fix up
> quickly. It's not mandatory to me that you get all this fixed up prior
> to merging because it's long gone past the point where dealing with it
> out-of-tree or in staging is going to work. By the time you address all the
> concerns, it will have reached the point where it's too complex to review
> and back to square one. At least if it's in mm/ it can be incrementally
> developed but it should certainly start with a big fat warning that it's
> a WIP. I wouldn't slap "ready for production" sticker on this just yet :/

We are in agreement on all points. I'll send out the revised patchset
ASAP.

Thanks for the review!

Seth

2013-05-20 13:54:54

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Sun, May 19, 2013 at 03:52:19PM -0500, Seth Jennings wrote:
> <snip>
> > > 4 files changed, 597 insertions(+)
> > > + * zbud pages are divided into "chunks". The size of the chunks is fixed at
> > > + * compile time and determined by NCHUNKS_ORDER below. Dividing zbud pages
> > > + * into chunks allows organizing unbuddied zbud pages into a manageable number
> > > + * of unbuddied lists according to the number of free chunks available in the
> > > + * zbud page.
> > > + *
> >
> > Fixing the size of the chunks at compile time is a very strict
> > limitation! Distributions will have to make that decision for all workloads
> > that might conceivably use zswap. Having the allocator only deal with pairs
> > of pages limits the worst-case behaviour where reclaim can generate lots of
> > IO to free a single physical page. However, the chunk size directly affects
> > the fragmentation properties, both internal and external, of this thing.
>
> > Once NCHUNKS is > 2 it is possible to create a workload that externally
> > fragments this allocator such that each physical page only holds one
> > compressed page. If this is a problem for a user then their only option
> > is to rebuild the kernel which is not always possible.
>
> You lost me here. Do you mean NCHUNKS > 2 or NCHUNKS_ORDER > 2?
>

NCHUNKS

> My first guess is that the external fragmentation situation you are referring to
> is a workload in which all pages compress to greater than half a page. If so,
> then it doesn't matter what NCHUCNKS_ORDER is, there won't be any pages the
> compress enough to fit in the < PAGE_SIZE/2 free space that remains in the
> unbuddied zbud pages.
>

There are numerous aspects to this, too many to write them all down.
Modelling the external fragmentation one and how it affects swap IO
would be a complete pain in the ass so lets consider the following
example instead as it's a bit clearer.

Three processes. Process A compresses by 75%, Process B compresses to 15%,
Process C pages compress to 15%. They are all adding to zswap in lockstep.
Lets say that zswap can hold 100 physical pages.

NCHUNKS == 2
All Process A pages get rejected.
All Process B and C pages get added to zswap until zswap fills
Zswap fills 300 allocation requests
Swap IO is 33 physical pages (all process A rejects)
Process B and C see no swap IO

NCHUNKS == 6
Zswap fills after 100 allocation requests
zswap holds 200 compressed pages
All other requests go to swap.

If both configurations were to compare each other for 300 allocation
requests, NCHUNKS==6 results in more swap IO with a mix of page writes
from A, B and C than if NCHUNKS==2.

This obviously is overly simplistic because it's ignoring writeback and
how that will impact page avalability and I didn't model what it would
look like for arbitrarily long request strings. The point is that the
value of NCHUNKS affects the amount of swap IO and when it kicks in. A
higher NCHUNKS might defer when swap IO starts but when it starts, it may
be continual. A lower NCHUNKS might start writing to swap earlier but in
some cases will result in less swap IO overall.

> You might also be referring to the fact that if you set NCHUNKS_ORDER to 2
> (i.e. there are 4 chunks per zbud page) and you receive an allocation for size
> (3/4 * PAGE_SIZE) + 1, the allocator will use all 4 chunks for that allocation
> and the rest of the zbud page is lost to internal fragmentation.
>

I'm less ocncerned about internal fragmentation because there are only
ever 2 compressed page in a single physical page. The internal
fragmentation characteristics are going to suck no matter what.

> That is simply an argument for not choosing a small NCHUNKS_ORDER.
>
> >
> > Please make this configurable by a kernel boot parameter at least. At
> > a glance it looks like only problem would be that you have to kmalloc
> > unbuddied[NCHUNKS] in the pool structure but that is hardly of earth
> > shattering difficulty. Make the variables read_mostly to avoid cache-line
> > bouncing problems.
>
> I am hesitant to make this a tunable without understanding why anyone would
> want to tune it. It's hard to convey to a user what this tunable would do and
> what effect it might have. I'm not saying that isn't such a situation.
> I just don't see one didn't understand your case above.
>

I would only expect the tunable to be used by a developer supporting a user
of zswap that complains about zswap-related stalls. They would be checking
what the rate of swap IO is with default nchunks vs nchunks==2. They would
not necessarily be able to do anything useful with this information until
they were willing to develop zswap. There would be no recommended value
for this because it's completely workload dependant but data gathered from
the tunable could potentially be used to justify the full zsmalloc allocator.

> > > +struct zbud_pool {
> > > + spinlock_t lock;
> > > + struct list_head unbuddied[NCHUNKS];
> > > + struct list_head buddied;
> > > + struct list_head lru;
> > > + atomic_t pages_nr;
> >
> > There is no need for pages_nr to be atomic. It's always manipulated
> > under the lock. I see that the atomic is exported so someone can read it
> > that is outside the lock but they are goign to have to deal with races
> > anyway. atomic does not magically protect them
>
> True. I'll change it.
>
> >
> > Also, pages_nr does not appear to be the number of zbud pages in the pool,
> > it's the number of zpages. You may want to report both for debugging
> > purposes as if nr_zpages != 2 * nr_zbud_pages then zswap is using more
> > physical pages than it should be.
>
> No, pages_nr is the number of pool pages, not zpages. The number of zpages (or
> allocations from zbud's point of view) is easily trackable by the user as it
> does each allocation. What the user can not know is how many pages are in the
> pool. Hence why zbud tracks this stat and makes it accessible via
> zbud_get_pool_size().
>

Bah, I missed up my terminiology. When I wrote this, I thought zbud must
be a "buddy" page or a compressed page. Bit confusing but it'll settle
in eventually.

> > > +/*
> > > + * Encodes the handle of a particular buddy within a zbud page
> > > + * Pool lock should be held as this function accesses first|last_chunks
> > > + */
> > > +static inline unsigned long encode_handle(struct zbud_page *zbpage,
> > > + enum buddy bud)
> > > +{
> > > + unsigned long handle;
> > > +
> > > + /*
> > > + * For now, the encoded handle is actually just the pointer to the data
> > > + * but this might not always be the case. A little information hiding.
> > > + */
> > > + handle = (unsigned long)page_address(&zbpage->page);
> > > + if (bud == FIRST)
> > > + return handle;
> > > + handle += PAGE_SIZE - (zbpage->last_chunks << CHUNK_SHIFT);
> > > + return handle;
> > > +}
> >
> > Your handles are unsigned long and are addresses. Consider making it an
> > opaque type so someone deferencing it would take a special kind of
> > stupid.
>
> My argument for keeping the handles as unsigned longs is a forward-looking
> to the implementation of the pluggable allocator interface in zswap.
> Typing the handles prevents the creation of an allocator neutral function
> signature.
>

I don't see how but I don't know what your forward-looking
implementation looks like either so I'll take your word for it.

> > > + zbpage = NULL;
> > > + for_each_unbuddied_list(i, chunks) {
> > > + if (!list_empty(&pool->unbuddied[i])) {
> > > + zbpage = list_first_entry(&pool->unbuddied[i],
> > > + struct zbud_page, buddy);
> > > + list_del(&zbpage->buddy);
> > > + if (zbpage->first_chunks == 0)
> > > + bud = FIRST;
> > > + else
> > > + bud = LAST;
> > > + goto found;
> > > + }
> > > + }
> > > +
> > > + /* Lastly, couldn't find unbuddied zbpage, create new one */
> > > + spin_unlock(&pool->lock);
> > > + page = alloc_page(gfp);
> > > + if (!page)
> > > + return -ENOMEM;
> > > + spin_lock(&pool->lock);
> > > + atomic_inc(&pool->pages_nr);
> > > + zbpage = init_zbud_page(page);
> > > + bud = FIRST;
> > > +
> >
> > What bounds the size of the pool? Maybe a higher layer does but should the
> > higher layer set the maximum size and enforce it here instead? That way the
> > higher layer does not need to know that the allocator is dealing with pages.
>
> I see your point. The higher layer would have to set the limit in some
> units, likely pages, so it would be aware that zbud is using pages.
>

I don't think the higher layer necessarily has to inform zbud.

> However, zswap (or any user) would probably make an initial determination of
> the limit in pages. Then would have to register a notifier for anything that
> could change the memory size (i.e. memory add/remove) and adjust the zbud
> limit.
>

To be honest, I would have expected zbud to set the limit as it's the
allocator. I would also expect it to register the notifier for hotplug
events because it's the allocator that knows how many physical pages are
used. The higher layer should not necessarily care about how many pages
are in use.

> I guess a different way would be to set the zbud limit as a percentage, then
> zbud could automatically adjust when the amount of ram changes, doing a
> per-allocation limit check.
>
> Any thoughts about those options?
>

Nothing specific, just the general observation that the allocator is
what is responsible for the physical resource and deferring the sizing
of it to a higher layer will complicate the API.

> <snip>
> > > + spin_lock(&pool->lock);
> > > + zbpage = handle_to_zbud_page(handle);
> > > +
> > > + /* If first buddy, handle will be page aligned */
> > > + if (handle & ~PAGE_MASK)
> > > + zbpage->last_chunks = 0;
> > > + else
> > > + zbpage->first_chunks = 0;
> > > +
> > > + if (PageReclaim(&zbpage->page)) {
> > > + /* zbpage is under reclaim, reclaim will free */
> > > + spin_unlock(&pool->lock);
> > > + return;
> > > + }
> > > +
> >
> > This implies that it is possible for a zpage to get freed twice. That
> > sounds wrong. It sounds like a page being reclaimed should be isolated
> > from other lists that makes it accessible similar to how normal pages are
> > isolated from the LRU and then freed.
>
> No, a zpage will not be freed twice.
>
> The problem is that even if zbud isolates the page in it's structures,
> which it does now removing from the buddied/unbuddied and lru list, there is no
> way to isolate it in the _users_ data structures. Once we release the pool
> lock, a free could still come in from the user.
>

That sounds very fragile although I cannot point my finger on exactly
why. I worry that depending on the ordering of events that we might leak
pages although again I cannot point to exactly where this would happen.

> However, the user should have protections in place in it's eviction handler that
> prevent two cases:
>
> 1) If the user entry associated with the allocation being evicted has already
> been freed, the eviction handler should just return 0 (already freed)
>
> 2) If the user entry lookup in the eviction handler is successful, some
> lock/refcount must protect the entry and its associated zbud allocation from
> being freed while it is being evicted.
>
> <snip>
> > > + for (i = 0; i < retries; i++) {
> > > + zbpage = list_tail_entry(&pool->lru, struct zbud_page, lru);
> > > + list_del(&zbpage->lru);
> > > + list_del(&zbpage->buddy);
> > > + /* Protect zbpage against free */
> > > + SetPageReclaim(&zbpage->page);
> >
> > Why not isolated it instead of using a page flag?
>
> Same reason as above, can't isolate in the user structure.

Because I cannot point to exactly where this can go to hell I will not
object to this longer but it feels like zpages should be reference
counted and get freed (potentially freeing the zbud page) when it drops
to zero.

--
Mel Gorman
SUSE Labs

2013-05-20 15:42:38

by Seth Jennings

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Mon, May 20, 2013 at 02:54:39PM +0100, Mel Gorman wrote:
> On Sun, May 19, 2013 at 03:52:19PM -0500, Seth Jennings wrote:
> > My first guess is that the external fragmentation situation you are referring to
> > is a workload in which all pages compress to greater than half a page. If so,
> > then it doesn't matter what NCHUCNKS_ORDER is, there won't be any pages the
> > compress enough to fit in the < PAGE_SIZE/2 free space that remains in the
> > unbuddied zbud pages.
> >
>
> There are numerous aspects to this, too many to write them all down.
> Modelling the external fragmentation one and how it affects swap IO
> would be a complete pain in the ass so lets consider the following
> example instead as it's a bit clearer.
>
> Three processes. Process A compresses by 75%, Process B compresses to 15%,
> Process C pages compress to 15%. They are all adding to zswap in lockstep.
> Lets say that zswap can hold 100 physical pages.
>
> NCHUNKS == 2
> All Process A pages get rejected.

Ah, I think this is our disconnect. Process A pages will not be rejected.
They will be stored in a zbud page, and that zbud page will be added
to the 0th unbuddied list. This list maintains a list of zbud pages
that will never be buddied because there are no free chunks.

In other words, changing NCHUNKS has no effect on the acceptable size
of allocations.

Seth

2013-05-21 08:10:34

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Mon, May 20, 2013 at 10:42:25AM -0500, Seth Jennings wrote:
> On Mon, May 20, 2013 at 02:54:39PM +0100, Mel Gorman wrote:
> > On Sun, May 19, 2013 at 03:52:19PM -0500, Seth Jennings wrote:
> > > My first guess is that the external fragmentation situation you are referring to
> > > is a workload in which all pages compress to greater than half a page. If so,
> > > then it doesn't matter what NCHUCNKS_ORDER is, there won't be any pages the
> > > compress enough to fit in the < PAGE_SIZE/2 free space that remains in the
> > > unbuddied zbud pages.
> > >
> >
> > There are numerous aspects to this, too many to write them all down.
> > Modelling the external fragmentation one and how it affects swap IO
> > would be a complete pain in the ass so lets consider the following
> > example instead as it's a bit clearer.
> >
> > Three processes. Process A compresses by 75%, Process B compresses to 15%,
> > Process C pages compress to 15%. They are all adding to zswap in lockstep.
> > Lets say that zswap can hold 100 physical pages.
> >
> > NCHUNKS == 2
> > All Process A pages get rejected.
>
> Ah, I think this is our disconnect. Process A pages will not be rejected.
> They will be stored in a zbud page, and that zbud page will be added
> to the 0th unbuddied list. This list maintains a list of zbud pages
> that will never be buddied because there are no free chunks.
>

D'oh, good point. Unfortunately, the problem then still exists at the
writeback end which I didn't bring up in the previous mail. Take three
processes writing in lockstep. Process A pages compress to 15%, B compresses
to 15%, C compresses to 60%. Each physical page packing will look like this

nchunks=6 nchunks = 2

Page 0 A B A B
Page 1 C A C
Page 2 B C A B
Page 3 A B C
Pattern repeats..........

This continues until zswap is full. Now all three process stop and process
D starts writing to zswap, each of its pages compresses to 80%. These will
be freed in LRU order which is effectively FIFO.

With nchunks=6, to store 2 process D pages, it must write out 4 pages
to swap. With nchunks=2, to store two process D pages, it must write 3
pages so process D stalls for less time with nchunks==2.

This is a variation of the zsmalloc packing problem where greater packing
leads to worse performance when zswap is full. The user bug report will
look something like "performance goes to hell when zswap is full although
swap IO rates look normal". If it was a kernel parameter, setting nchunk=2
as a kernel boot parameter will at least be a workaround.

Of course, this is all hypothetical. It is certainly possible to create
a reference string where nchunks=2 generates more IO but it tends to
generate the IO sooner and more closely resemble existing swap behaviour
that users might be willing to accept as a workaround until their problem
can be resolved. This is why I think the parameter should be available at
boot-time as a debugging/workaround option for developers to recommend to
a user.

--
Mel Gorman
SUSE Labs

2013-05-23 02:01:20

by Bob Liu

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

Hi Mel & Seth,

On 05/21/2013 04:10 PM, Mel Gorman wrote:
> On Mon, May 20, 2013 at 10:42:25AM -0500, Seth Jennings wrote:
>> On Mon, May 20, 2013 at 02:54:39PM +0100, Mel Gorman wrote:
>>> On Sun, May 19, 2013 at 03:52:19PM -0500, Seth Jennings wrote:
>>>> My first guess is that the external fragmentation situation you are referring to
>>>> is a workload in which all pages compress to greater than half a page. If so,
>>>> then it doesn't matter what NCHUCNKS_ORDER is, there won't be any pages the
>>>> compress enough to fit in the < PAGE_SIZE/2 free space that remains in the
>>>> unbuddied zbud pages.
>>>>
>>>
>>> There are numerous aspects to this, too many to write them all down.
>>> Modelling the external fragmentation one and how it affects swap IO
>>> would be a complete pain in the ass so lets consider the following
>>> example instead as it's a bit clearer.
>>>
>>> Three processes. Process A compresses by 75%, Process B compresses to 15%,
>>> Process C pages compress to 15%. They are all adding to zswap in lockstep.
>>> Lets say that zswap can hold 100 physical pages.
>>>
>>> NCHUNKS == 2
>>> All Process A pages get rejected.
>>
>> Ah, I think this is our disconnect. Process A pages will not be rejected.
>> They will be stored in a zbud page, and that zbud page will be added
>> to the 0th unbuddied list. This list maintains a list of zbud pages
>> that will never be buddied because there are no free chunks.
>>
>
> D'oh, good point. Unfortunately, the problem then still exists at the
> writeback end which I didn't bring up in the previous mail.

What's your opinion if we write back the whole compressed page to swap disk?

--
Regards,
-Bob

2013-05-23 09:52:34

by Mel Gorman

[permalink] [raw]
Subject: Re: [PATCHv11 2/4] zbud: add to mm/

On Thu, May 23, 2013 at 10:00:07AM +0800, Bob Liu wrote:
> Hi Mel & Seth,
>
> On 05/21/2013 04:10 PM, Mel Gorman wrote:
> > On Mon, May 20, 2013 at 10:42:25AM -0500, Seth Jennings wrote:
> >> On Mon, May 20, 2013 at 02:54:39PM +0100, Mel Gorman wrote:
> >>> On Sun, May 19, 2013 at 03:52:19PM -0500, Seth Jennings wrote:
> >>>> My first guess is that the external fragmentation situation you are referring to
> >>>> is a workload in which all pages compress to greater than half a page. If so,
> >>>> then it doesn't matter what NCHUCNKS_ORDER is, there won't be any pages the
> >>>> compress enough to fit in the < PAGE_SIZE/2 free space that remains in the
> >>>> unbuddied zbud pages.
> >>>>
> >>>
> >>> There are numerous aspects to this, too many to write them all down.
> >>> Modelling the external fragmentation one and how it affects swap IO
> >>> would be a complete pain in the ass so lets consider the following
> >>> example instead as it's a bit clearer.
> >>>
> >>> Three processes. Process A compresses by 75%, Process B compresses to 15%,
> >>> Process C pages compress to 15%. They are all adding to zswap in lockstep.
> >>> Lets say that zswap can hold 100 physical pages.
> >>>
> >>> NCHUNKS == 2
> >>> All Process A pages get rejected.
> >>
> >> Ah, I think this is our disconnect. Process A pages will not be rejected.
> >> They will be stored in a zbud page, and that zbud page will be added
> >> to the 0th unbuddied list. This list maintains a list of zbud pages
> >> that will never be buddied because there are no free chunks.
> >>
> >
> > D'oh, good point. Unfortunately, the problem then still exists at the
> > writeback end which I didn't bring up in the previous mail.
>
> What's your opinion if we write back the whole compressed page to swap disk?
>

I'm not sure how to answer that sensibly. If the compressed page is
written to swap, in my opinion then there will be IO :/ . It will be a
maximum of two pages of IO with zbud (or zpair or whatever) as currently
implemented. With zsmalloc it potentially was more.

--
Mel Gorman
SUSE Labs