[PATCH 9/9] mtd: Add swaponflash block driver

Richard Purdie rpurdie at openedhand.com
Fri Mar 2 10:55:15 EST 2007


Add a driver for allowing an mtd device to be used as a swap block device.

Signed-off-by: Richard Purdie <rpurdie at openedhand.com>

 drivers/mtd/Kconfig   |    7 
 drivers/mtd/Makefile  |    1 
 drivers/mtd/mtdswap.c | 1187 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1195 insertions(+)

Index: linux/drivers/mtd/mtdswap.c
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ linux/drivers/mtd/mtdswap.c	2007-02-28 18:12:48.000000000 +0000
@@ -0,0 +1,1187 @@
+/*
+ * Swap block device support for MTDs
+ * Turns an MTD device into a swap device with block wear leveling
+ *
+ * Copyright (C) 2007 Nokia Corporation. All rights reserved.
+ *
+ * Author: Richard Purdie <rpurdie at openedhand.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 as published by the Free Software Foundation.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * Features:
+ *  - The mtd partitions to turn into swap devices are listed in the module
+ *    parameters in the form 'partitions="1,3,5"'.
+ *  - A dummy swap header is added to the start of the device so mkswap isn't
+ *    needed.
+ *  - Since block erase counts are kept approximately equal, there is no need
+ *    to keep track of erase counts over driver restarts.
+ *  - Two threads are used, one for the bio queue, the other to handle garbage
+ *    collection such as erase block leveling, block erasing and
+ *    defragmentation. The bio thread can need to wait on the gc thread for
+ *    free blocks.
+ *  - For wear leveling there are two possible approaches, an in memory buffer
+ *    or keeping at least one erase block reserved. The latter approach is
+ *    taken. The driver becomes faster but less space efficent if given more
+ *    reserved blocks.
+ *  - The driver tracks erase blocks by placing them in one of five trees,
+ *    clean, used, low_frag, high_frag and dirty. A block will move between the
+ *    trees roughly in that order as its used. For convience, each tree is
+ *    sorted by erase count.
+ */
+
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/vmalloc.h>
+#include <linux/swap.h>
+#include <linux/rbtree.h>
+#include <linux/mm.h>
+#include <linux/genhd.h>
+#include <linux/mutex.h>
+#include <linux/kthread.h>
+#include <linux/crc32.h>
+#include <linux/mtd/mtd.h>
+#include <linux/mtd/blktrans.h>
+
+
+#define MAX_PAGES_PER_EB 64
+/*
+ * The maximum difference in erase counts before we move blocks
+ */
+#define MAX_ERASECOUNT_DIFFERENCE 15
+/*
+ * How many blocks to reserve for garbage collection
+ * Must be >= 1
+ */
+#define NUMBER_SPARE_BLOCKS 1
+/*
+ * When changes are made to the number of active blocks in erase blocks,
+ * how long to wait before triggering garbage collection (in msec).
+ */
+#define GC_THREAD_DELAY 5000
+/*
+ * How many clean blocks should be available aabove which
+ * the garbage collection stops processing blocks.
+ */
+#define CLEAN_BLOCK_THRESHOLD 20
+/*
+ * Double check data from mtd with an extra crc?
+ */
+#define USE_CRC 1
+
+#undef DEBUG
+#ifdef DEBUG
+#define TRACE(fmt,a...) printk(KERN_DEBUG fmt, ##a)
+#else
+#define TRACE(fmt,a...)
+#endif
+
+struct swp_blk {
+	int mapno;
+#ifdef USE_CRC
+	unsigned long crc;
+#endif
+};
+
+struct swp_eblk {
+	struct rb_node rb;
+	struct rb_root *root;
+
+	int erase_count;
+	int bad_count;
+
+	DECLARE_BITMAP(active, MAX_PAGES_PER_EB);
+};
+
+#define EBLKADDR_TO_NUM(swpdev, addr) (addr - &swpdev->eblk_data[0])
+#define ERASE_COUNT_MIN(rbroot) (rb_entry(rb_first(rbroot), struct swp_eblk, rb)->erase_count)
+#define ERASE_COUNT_MAX(rbroot) (rb_entry(rb_last(rbroot), struct swp_eblk, rb)->erase_count)
+
+struct mtdswp_dev {
+	struct mtd_blktrans_dev mbd_dev;
+	struct mtd_info *mtd;
+	int fsdata_pos;
+
+	int pages;
+	struct swp_blk *blk_data;
+	int eblks;
+	struct swp_eblk *eblk_data;
+	int pages_per_blk;
+	int max_erase_count;
+
+        /* spinlock protects the fields in this block */
+	spinlock_t trees_lock;
+	struct rb_root clean;
+	struct rb_root used;
+	struct rb_root low_frag;
+	struct rb_root high_frag;
+	struct rb_root dirty;
+	int clean_count;
+	int used_count;
+	int low_frag_count;
+	int high_frag_count;
+	int dirty_count;
+
+        /* mutex protects the fields in this block */
+	struct mutex write_mutex;
+	int curr_write_pos;
+	struct swp_eblk *curr_write;
+
+	struct page *page;
+	char *page_buf;
+
+	struct task_struct* thread;
+	wait_queue_head_t thread_wq;
+	struct timer_list thread_timer;
+
+	wait_queue_head_t clean_wq;
+};
+
+struct swpdev_oobdata {
+	u16 magic;
+	u16 type;
+} __attribute__((packed));
+
+#define SWPDEV_MAGIC 		0x2095
+#define SWPDEV_TYPE_CLEAN 	0x0001
+#define SWPDEV_TYPE_DIRTY 	0x0002
+#define SWPDEV_OOBSIZE		sizeof(struct swpdev_oobdata)
+#define MAX_ERASE_FAILURES	2
+
+static int mtdswap_write_block(struct mtdswp_dev *swpdev, char *buf, int gc_context);
+
+/* return: 0 - clean, 1 - dirty, 2 - bad */
+static int swpdev_check_markers(struct mtdswp_dev *swpdev, struct swp_eblk *eblk)
+{
+	struct swpdev_oobdata n;
+	unsigned char buf[SWPDEV_OOBSIZE];
+	unsigned char *p;
+	int ret, i, retval = 0;
+	size_t offset;
+	struct mtd_oob_ops ops;
+
+	ops.len = SWPDEV_OOBSIZE;
+	ops.ooblen = SWPDEV_OOBSIZE;
+	ops.oobbuf = buf;
+	ops.ooboffs = swpdev->fsdata_pos;
+	ops.datbuf = NULL;
+	ops.mode = MTD_OOB_PLACE;
+
+	offset = EBLKADDR_TO_NUM(swpdev, eblk) * swpdev->mtd->erasesize;
+
+	/* Check first if the block is bad. */
+	if (swpdev->mtd->block_isbad
+			&& swpdev->mtd->block_isbad(swpdev->mtd, offset)) {
+		printk(KERN_WARNING "swpdev_check_markers(): Bad block at %08x\n", offset);
+		return 2;
+	}
+
+	if (swpdev->fsdata_pos == -1)
+		return 1;
+
+	offset = EBLKADDR_TO_NUM(swpdev, eblk) * swpdev->mtd->erasesize;
+
+	ret = swpdev->mtd->read_oob(swpdev->mtd, offset, &ops);
+
+	if (ret) {
+		printk(KERN_WARNING "swpdev_check_markers(): Read OOB failed %d for block at %08x\n",
+				ret, offset);
+		return ret;
+	}
+	if (ops.retlen < ops.len) {
+		printk(KERN_WARNING "swpdev_check_markers(): Read OOB return short read (%zd bytes not %d) for block at %08x\n",
+				ops.retlen, SWPDEV_OOBSIZE, offset);
+		return -EIO;
+	}
+
+	n.magic = SWPDEV_MAGIC;
+	n.type = SWPDEV_TYPE_CLEAN;
+	p = (unsigned char *) &n;
+	for (i = 0; i < SWPDEV_OOBSIZE; i++)
+		if (buf[i] != p[i])
+			retval = 1;
+
+	return retval;
+}
+
+static int swpdev_write_marker(struct mtdswp_dev *swpdev,
+		struct swp_eblk *eblk, u16 marker)
+{
+	struct swpdev_oobdata n;
+	int ret;
+	int offset;
+	struct mtd_oob_ops ops;
+
+	ops.len = SWPDEV_OOBSIZE;
+	ops.ooblen = SWPDEV_OOBSIZE;
+	ops.oobbuf = (uint8_t *)&n;
+	ops.ooboffs = swpdev->fsdata_pos;
+	ops.datbuf = NULL;
+	ops.mode = MTD_OOB_PLACE;
+
+	if (swpdev->fsdata_pos == -1)
+		return 0;
+
+	n.magic = SWPDEV_MAGIC;
+	n.type = marker;
+
+	offset = EBLKADDR_TO_NUM(swpdev, eblk) * swpdev->mtd->erasesize;
+
+	TRACE("Writing marker for %d\n", offset);
+
+	ret = swpdev->mtd->write_oob(swpdev->mtd, offset , &ops);
+
+	if (ret) {
+		printk(KERN_WARNING "swpdev_write_cleanmarker(): Write failed for block at %08x: error %d\n",
+				offset, ret);
+		return ret;
+	}
+	if (ops.retlen != ops.len) {
+		printk(KERN_WARNING "swpdev_write_cleanmarker(): Short write for block at %08x: %zd not %d\n",
+				offset, ops.retlen, SWPDEV_OOBSIZE);
+		return ret;
+	}
+	return 0;
+}
+
+static int swpdev_write_badblock(struct mtdswp_dev *swpdev, struct swp_eblk *eblk)
+{
+	int ret, offset;
+
+	offset = EBLKADDR_TO_NUM(swpdev, eblk) * swpdev->mtd->erasesize;
+
+	/* if the count is < max, we try to write the counter to
+	   the 2nd page oob area */
+	if( ++eblk->bad_count < MAX_ERASE_FAILURES)
+		return 0;
+
+	/* badblocks not supported */
+	if (!swpdev->mtd->block_markbad)
+		return 1;
+
+	printk(KERN_WARNING "swpdev_write_badblock(): Marking bad block at %08x\n",
+			offset);
+	ret = swpdev->mtd->block_markbad(swpdev->mtd, offset);
+
+	if (ret) {
+		printk(KERN_WARNING "swpdev_write_badblock(): Write failed for block at %08x: error %d\n",
+				offset, ret);
+		return ret;
+	}
+	return 1;
+}
+
+/* Assumes trees_lock held */
+static void counter_decrease(struct mtdswp_dev *swpdev, struct rb_root *root)
+{
+	if (!root)
+		return;
+	if (root == &swpdev->clean)
+		swpdev->clean_count--;
+	else if (root == &swpdev->used)
+		swpdev->used_count--;
+	else if (root == &swpdev->low_frag)
+		swpdev->low_frag_count--;
+	else if (root == &swpdev->high_frag)
+		swpdev->high_frag_count--;
+	else if (root == &swpdev->dirty)
+		swpdev->dirty_count--;
+}
+
+/* Assumes trees_lock held */
+static void counter_increase(struct mtdswp_dev *swpdev, struct rb_root *root)
+{
+	if (root == &swpdev->clean)
+		swpdev->clean_count++;
+	else if (root == &swpdev->used)
+		swpdev->used_count++;
+	else if (root == &swpdev->low_frag)
+		swpdev->low_frag_count++;
+	else if (root == &swpdev->high_frag)
+		swpdev->high_frag_count++;
+	else if (root == &swpdev->dirty)
+		swpdev->dirty_count++;
+}
+
+/* Assumes trees_lock held */
+static void mtdswap_rb_add(struct mtdswp_dev *swpdev, struct swp_eblk *eblk, struct rb_root *root)
+{
+	struct rb_node **p, *parent = NULL;
+
+	counter_decrease(swpdev, eblk->root);
+	if (eblk->root)
+		rb_erase(&eblk->rb, eblk->root);
+
+	p = &root->rb_node;
+	while (*p) {
+		struct swp_eblk *eblk1;
+
+		parent = *p;
+		eblk1 = rb_entry(parent, struct swp_eblk, rb);
+
+		if (eblk->erase_count > eblk1->erase_count)
+			p = &(*p)->rb_right;
+		else
+			p = &(*p)->rb_left;
+	}
+
+	rb_link_node(&eblk->rb, parent, p);
+	rb_insert_color(&eblk->rb, root);
+	eblk->root = root;
+	counter_increase(swpdev, root);
+}
+
+static struct swp_eblk * mtdswap_next_clean(struct mtdswp_dev *swpdev, int gc_context)
+{
+	struct swp_eblk *eblk;
+	unsigned long flags;
+
+	spin_lock_irqsave(&swpdev->trees_lock, flags);
+	/* Is the tree empty and can we sleep? */
+	if ((swpdev->clean.rb_node == NULL) && !gc_context) {
+		DECLARE_WAITQUEUE(wait, current);
+
+		spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+
+		TRACE("Sleeping until a clean block found\n");
+
+		set_current_state(TASK_INTERRUPTIBLE);
+		add_wait_queue(&swpdev->clean_wq, &wait);
+		wake_up(&swpdev->thread_wq);
+		schedule();
+		remove_wait_queue(&swpdev->clean_wq, &wait);
+
+		TRACE("Clean block wake up\n");
+
+		spin_lock_irqsave(&swpdev->trees_lock, flags);
+	}
+
+	/* Is the tree still empty? */
+	if (swpdev->clean.rb_node == NULL) {
+		spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+		return NULL;
+	}
+
+	eblk = rb_entry(rb_first(&swpdev->clean), struct swp_eblk, rb);
+	rb_erase(&eblk->rb, &swpdev->clean);
+	eblk->root = NULL;
+	swpdev->clean_count--;
+	spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+
+	swpdev_write_marker(swpdev, eblk, SWPDEV_TYPE_DIRTY);
+
+	return eblk;
+}
+
+static void wake_swap_thread_delayed(struct mtdswp_dev *swpdev)
+{
+	mod_timer(&swpdev->thread_timer, jiffies + msecs_to_jiffies(GC_THREAD_DELAY));
+}
+
+/*
+ * Place eblk into a tree corresponding to its number of active blocks
+ * it contains:
+ *   100% active -> used tree
+ *   >50% active -> low_frag tree
+ *   <50% active -> high_frag tree
+ *   0% active   -> dirty tree
+ */
+static void mtdswap_store_block(struct mtdswp_dev *swpdev, struct swp_eblk *eblk)
+{
+	int weight = bitmap_weight(eblk->active, MAX_PAGES_PER_EB);
+	int maxweight = swpdev->pages_per_blk;
+	unsigned long flags;
+
+	if (eblk == swpdev->curr_write)
+		return;
+
+	spin_lock_irqsave(&swpdev->trees_lock, flags);
+	if (weight == maxweight) {
+		TRACE("Placing block in used tree\n");
+		if (eblk->root != &swpdev->used)
+			mtdswap_rb_add(swpdev, eblk, &swpdev->used);
+	} else if (weight == 0) {
+		TRACE("Placing block in dirty tree\n");
+		if (eblk->root != &swpdev->dirty)
+			mtdswap_rb_add(swpdev, eblk, &swpdev->dirty);
+	} else if (weight > (maxweight/2)) {
+		TRACE("Placing block in low frag tree\n");
+		if (eblk->root != &swpdev->low_frag)
+			mtdswap_rb_add(swpdev, eblk, &swpdev->low_frag);
+	} else {
+		TRACE("Placing block in high frag tree\n");
+		if (eblk->root != &swpdev->high_frag)
+			mtdswap_rb_add(swpdev, eblk, &swpdev->high_frag);
+	}
+	spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+	wake_swap_thread_delayed(swpdev);
+}
+
+static void erase_callback(struct erase_info *done)
+{
+	wait_queue_head_t *wait_q = (wait_queue_head_t *)done->priv;
+	wake_up(wait_q);
+}
+
+static int erase_block(struct mtdswp_dev *swpdev, struct swp_eblk *eblk)
+{
+	struct mtd_info *mtd = swpdev->mtd;
+	struct erase_info erase;
+	DECLARE_WAITQUEUE(wait, current);
+	wait_queue_head_t wait_q;
+	int ret;
+
+	eblk->erase_count++;
+	if (eblk->erase_count > swpdev->max_erase_count)
+		swpdev->max_erase_count = eblk->erase_count;
+
+	init_waitqueue_head(&wait_q);
+	erase.mtd = mtd;
+	erase.callback = erase_callback;
+	erase.addr = EBLKADDR_TO_NUM(swpdev, eblk) * mtd->erasesize;
+	erase.len = mtd->erasesize;
+	erase.priv = (u_long)&wait_q;
+
+	set_current_state(TASK_INTERRUPTIBLE);
+	add_wait_queue(&wait_q, &wait);
+
+	ret = mtd->erase(mtd, &erase);
+	if (ret) {
+		set_current_state(TASK_RUNNING);
+		remove_wait_queue(&wait_q, &wait);
+		printk (KERN_WARNING "mtdswap: erase of region [0x%x, 0x%x] "
+				     "on \"%s\" failed\n",
+			erase.addr, erase.len, mtd->name);
+		if (!swpdev_write_badblock(swpdev, eblk))
+			return -EAGAIN;
+		return ret;
+	}
+
+	schedule();  /* Wait for erase to finish. */
+	remove_wait_queue(&wait_q, &wait);
+
+	swpdev_write_marker(swpdev, eblk, SWPDEV_TYPE_CLEAN);
+
+	return 0;
+}
+
+
+static int move_block(struct mtdswp_dev *swpdev, unsigned long block)
+{
+	struct mtd_info *mtd = swpdev->mtd;
+	unsigned long realblock;
+	int newblock;
+	size_t retlen;
+	int ret;
+
+	realblock = swpdev->blk_data[block].mapno;
+
+	ret = mtd->read(mtd, realblock << PAGE_SHIFT, PAGE_SIZE, &retlen, swpdev->page_buf);
+	if (ret < 0) {
+		printk(KERN_ERR "Read Error: %d (block %ld)\n", ret, block);
+		return ret;
+	}
+	if (retlen != PAGE_SIZE) {
+		printk(KERN_ERR "Short read: %d\n", retlen);
+		return -EAGAIN;
+	}
+
+	newblock = mtdswap_write_block(swpdev, swpdev->page_buf, 1);
+	if (newblock < 0) {
+		printk(KERN_ERR "Write error: %d\n", newblock);
+		return newblock;
+	}
+
+	TRACE("Moving block %ld (from %d to %d)\n", block, realblock, newblock);
+
+	swpdev->blk_data[block].mapno = newblock;
+
+	return 0;
+}
+
+static int empty_block(struct mtdswp_dev *swpdev, struct swp_eblk *eblk)
+{
+	int i, active;
+
+	active = bitmap_weight(eblk->active, MAX_PAGES_PER_EB);
+
+	TRACE("moving %d entires for block %d\n", active, EBLKADDR_TO_NUM(swpdev, eblk));
+	for (i = 0; i < active; i++) {
+		int ret, j, bnum, offset;
+
+		offset = find_first_bit(eblk->active, MAX_PAGES_PER_EB);
+		bnum = (EBLKADDR_TO_NUM(swpdev, eblk) * swpdev->pages_per_blk) + offset;
+
+		for (j = 0; j < swpdev->pages; j++)
+			if (swpdev->blk_data[j].mapno == bnum)
+				break;
+
+		if (j == swpdev->pages) {
+			/* block must have been unused since we made the active count */
+			TRACE("mapping for this block not found\n");
+			clear_bit(offset, eblk->active);
+			continue;
+		}
+
+		ret = move_block(swpdev, j);
+		if (ret < 0)
+			return ret;
+		else
+			clear_bit(offset, eblk->active);
+	}
+	return 0;
+}
+
+static void swpdev_timer_wakeup(unsigned long data)
+{
+	struct mtdswp_dev *swpdev = (struct mtdswp_dev *) data;
+	wake_up(&swpdev->thread_wq);
+}
+
+static int mtdswap_thread(void *arg)
+{
+	struct mtdswp_dev *swpdev = arg;
+
+	/* we're involved when memory gets low, so use PF_MEMALLOC */
+	current->flags |= PF_MEMALLOC | PF_NOFREEZE;
+
+	/* This isn't done by default since some kernel threads actually want
+           to deal with signals. We can't just call exit_sighand() since
+           that'll cause an oops when we finally do exit. */
+	spin_lock_irq(&current->sighand->siglock);
+	sigfillset(&current->blocked);
+	recalc_sigpending();
+	spin_unlock_irq(&current->sighand->siglock);
+
+	set_user_nice(current, -5);
+	set_current_state(TASK_INTERRUPTIBLE);
+
+	while (!kthread_should_stop()) {
+		int ret = 0, empty = 0;
+		struct swp_eblk *eblk = NULL;
+		DECLARE_WAITQUEUE(wait, current);
+		unsigned long flags;
+
+		TRACE("Thread looping\n");
+
+		spin_lock_irqsave(&swpdev->trees_lock, flags);
+		if (swpdev->clean_count > CLEAN_BLOCK_THRESHOLD) {
+			/* We have enough clean blocks */
+		} else if (swpdev->dirty.rb_node != NULL) {
+			eblk = rb_entry(rb_first(&swpdev->dirty), struct swp_eblk, rb);
+			rb_erase(&eblk->rb, &swpdev->dirty);
+			eblk->root = NULL;
+			swpdev->dirty_count--;
+		} else if ((swpdev->used.rb_node != NULL) &&
+				((ERASE_COUNT_MIN(&swpdev->used) + MAX_ERASECOUNT_DIFFERENCE)
+				< swpdev->max_erase_count)) {
+			eblk = rb_entry(rb_first(&swpdev->used), struct swp_eblk, rb);
+			rb_erase(&eblk->rb, &swpdev->used);
+			eblk->root = NULL;
+			swpdev->used_count--;
+			empty = 1;
+		} else if (swpdev->high_frag.rb_node != NULL) {
+			eblk = rb_entry(rb_first(&swpdev->high_frag), struct swp_eblk, rb);
+			rb_erase(&eblk->rb, &swpdev->high_frag);
+			eblk->root = NULL;
+			swpdev->high_frag_count--;
+			empty = 1;
+		} else if ((swpdev->low_frag.rb_node != NULL) && ((swpdev->clean_count < 5) ||
+				((ERASE_COUNT_MIN(&swpdev->low_frag) + MAX_ERASECOUNT_DIFFERENCE)
+				< swpdev->max_erase_count))) {
+			eblk = rb_entry(rb_first(&swpdev->low_frag), struct swp_eblk, rb);
+			rb_erase(&eblk->rb, &swpdev->low_frag);
+			eblk->root = NULL;
+			swpdev->low_frag_count--;
+			empty = 1;
+		}
+		spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+
+		if (empty)
+			ret = empty_block(swpdev, eblk);
+
+		if (ret < 0) {
+			printk(KERN_ERR "empty block returned error: %d, aborting.\n", ret);
+			mtdswap_store_block(swpdev, eblk);
+			eblk = NULL;
+		}
+
+		if (!eblk) {
+			TRACE("Thread waiting\n");
+			add_wait_queue(&swpdev->thread_wq, &wait);
+			wake_up(&swpdev->clean_wq);
+			schedule();
+			set_current_state(TASK_INTERRUPTIBLE);
+			remove_wait_queue(&swpdev->thread_wq, &wait);
+			continue;
+		}
+
+		TRACE("Erasing block %d (active = %d)\n",
+				EBLKADDR_TO_NUM(swpdev, eblk),
+				bitmap_weight(eblk->active, MAX_PAGES_PER_EB));
+
+		BUG_ON(bitmap_weight(eblk->active, MAX_PAGES_PER_EB) != 0);
+
+		ret = erase_block(swpdev, eblk);
+
+		spin_lock_irqsave(&swpdev->trees_lock, flags);
+		if (ret == -EAGAIN)
+			mtdswap_rb_add(swpdev, eblk, &swpdev->dirty);
+		else if (ret >= 0)
+			mtdswap_rb_add(swpdev, eblk, &swpdev->clean);
+		spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+	}
+
+	set_current_state(TASK_RUNNING);
+	return 0;
+}
+
+/*
+ * gc_context parameter tells us whether we're garbage collecting or responding
+ * to a swap read/write request. gc_context must have priority over swap
+ * requests as swap requests can wait on gc for free space but gc_context needs
+ * to use its reserved eraseblock(s).
+ */
+static int mtdswap_get_clean(struct mtdswp_dev *swpdev, int gc_context)
+{
+	int blockaddr;
+
+	/* Protect the last remaining block for gc only */
+	if ((swpdev->clean_count == 0) && !gc_context) {
+		DECLARE_WAITQUEUE(wait, current);
+
+		TRACE("Get clean sleeping until more blocks free\n");
+
+		set_current_state(TASK_INTERRUPTIBLE);
+		add_wait_queue(&swpdev->clean_wq, &wait);
+		wake_up(&swpdev->thread_wq);
+		schedule();
+		remove_wait_queue(&swpdev->clean_wq, &wait);
+
+		TRACE("get clean wake up\n");
+
+		if (swpdev->clean_count == 0) {
+			printk(KERN_ERR "too few free blocks!\n");
+			return -ENOSPC;
+		}
+
+	}
+
+	mutex_lock(&swpdev->write_mutex);
+
+	if ((swpdev->curr_write_pos == swpdev->pages_per_blk)
+			|| !swpdev->curr_write) {
+		struct swp_eblk *oldeblk = swpdev->curr_write;
+
+		swpdev->curr_write_pos = 0;
+		swpdev->curr_write = mtdswap_next_clean(swpdev, gc_context);
+
+		if (oldeblk)
+			mtdswap_store_block(swpdev, oldeblk);
+	}
+
+	if (swpdev->curr_write == NULL) {
+		mutex_unlock(&swpdev->write_mutex);
+		printk(KERN_ERR "No space!\n");
+		return -ENOSPC;
+	}
+
+	blockaddr = ((EBLKADDR_TO_NUM(swpdev, swpdev->curr_write)
+			* swpdev->pages_per_blk)
+			+ swpdev->curr_write_pos);
+
+	TRACE("Providing block %d (offset %d)\n", blockaddr, swpdev->curr_write_pos);
+
+	set_bit(swpdev->curr_write_pos, swpdev->curr_write->active);
+	swpdev->curr_write_pos++;
+
+	mutex_unlock(&swpdev->write_mutex);
+
+	return blockaddr;
+}
+
+static int mtdswap_mark_inactive(struct mtdswp_dev *swpdev, unsigned long blocknum)
+{
+	unsigned long offset = blocknum % swpdev->pages_per_blk;
+	struct swp_eblk *eblk = &swpdev->eblk_data[blocknum / swpdev->pages_per_blk];
+	int ret;
+
+	TRACE("Marking %ld as unused\n", blocknum);
+
+	ret = !test_and_clear_bit(offset, eblk->active);
+	if (ret)
+		TRACE("Marking block unused but hasn't been written to!\n", blocknum);
+
+	mtdswap_store_block(swpdev, eblk);
+
+	return ret;
+}
+
+static int mtdswap_write_block(struct mtdswp_dev *swpdev, char *buf, int gc_context)
+{
+	struct mtd_info *mtd = swpdev->mtd;
+	int blockaddr;
+	size_t retlen;
+	int ret;
+
+	blockaddr = mtdswap_get_clean(swpdev, gc_context);
+	if (blockaddr < 0)
+		return blockaddr;
+
+	ret =  mtd->write(mtd, blockaddr << PAGE_SHIFT, PAGE_SIZE, &retlen, buf);
+
+	if (retlen != PAGE_SIZE) {
+		mtdswap_mark_inactive(swpdev, blockaddr);
+		printk(KERN_ERR "Short write to MTD device: %d written\n",
+				retlen);
+		return -EAGAIN;
+	}
+
+	if (ret < 0) {
+		mtdswap_mark_inactive(swpdev, blockaddr);
+		printk(KERN_ERR "Write to MTD device failed: %d (%d written)\n",
+				ret, retlen);
+		return ret;
+	}
+
+	return blockaddr;
+}
+
+
+static int mtdswap_writesect(struct mtd_blktrans_dev *dev,
+			      unsigned long block, char *buf)
+{
+	struct mtdswp_dev *swpdev = container_of(dev, struct mtdswp_dev, mbd_dev);
+	int mtdblock;
+
+	/* Ignore writes to the header page */
+	if (unlikely(block == 0))
+		return 0;
+
+	block--;
+
+	if (swpdev->blk_data[block].mapno != -1) {
+		mtdswap_mark_inactive(swpdev, swpdev->blk_data[block].mapno);
+		swpdev->blk_data[block].mapno = -1;
+	}
+
+	mtdblock = mtdswap_write_block(swpdev, buf, 0);
+
+	if (mtdblock < 0)
+		return mtdblock;
+
+	TRACE("Mapping block %ld to %d\n", block, mtdblock);
+
+	swpdev->blk_data[block].mapno = mtdblock;
+#ifdef USE_CRC
+	swpdev->blk_data[block].crc = crc32(~0, buf, PAGE_SIZE);
+#endif
+	return 0;
+}
+
+/* Provide a dummy swap header for the kernel */
+static int read_swap_header(struct mtdswp_dev *swpdev, char *buf)
+{
+	union swap_header *header = (union swap_header *)(buf);
+
+	memset(buf, 0, PAGE_SIZE - 10);
+
+	header->info.version = 1;
+	header->info.last_page = (swpdev->mtd->size -
+		(swpdev->mtd->erasesize * NUMBER_SPARE_BLOCKS)) >> PAGE_SHIFT;
+	header->info.nr_badpages = 0;
+	header->info.flags = SWAPFLAG_UNUSED_IOCTL;
+
+	memcpy(buf + PAGE_SIZE - 10, "SWAPSPACE2", 10);
+
+	return 0;
+}
+
+static int mtdswap_readsect(struct mtd_blktrans_dev *dev,
+			      unsigned long block, char *buf)
+{
+	struct mtdswp_dev *swpdev = container_of(dev, struct mtdswp_dev, mbd_dev);
+	struct mtd_info *mtd = swpdev->mtd;
+	unsigned long realblock, offset;
+	struct swp_eblk *eblk;
+	size_t retlen;
+	int ret;
+
+	if (unlikely(block == 0))
+		return read_swap_header(swpdev, buf);
+
+	block--;
+	realblock = swpdev->blk_data[block].mapno;
+	eblk = &swpdev->eblk_data[realblock / swpdev->pages_per_blk];
+	offset = realblock % swpdev->pages_per_blk;
+
+	if (!test_bit(offset, eblk->active)) {
+		printk(KERN_ERR "Page %ld (block %ld) accessed but not written to!\n",
+			block, realblock);
+		return -EIO;
+	}
+
+	ret = mtd->read(mtd, realblock << PAGE_SHIFT, PAGE_SIZE, &retlen, buf);
+
+	TRACE("Reading block %ld (%ld)\n", block, realblock);
+
+	if (ret < 0) {
+		printk(KERN_ERR "Read error %d\n", ret);
+		return ret;
+	}
+
+	if (retlen != PAGE_SIZE) {
+		printk(KERN_ERR "Short read %d\n", retlen);
+		return -EAGAIN;
+	}
+
+#ifdef USE_CRC
+	if (crc32(~0, buf, PAGE_SIZE) != swpdev->blk_data[block].crc) {
+		printk(KERN_ERR "CRC mismatch for block %ld\n", block);
+		return -EIO;
+	}
+#endif
+
+	return ret;
+}
+
+static int mtdswap_ioctl(struct mtd_blktrans_dev *dev, unsigned int cmd,
+				unsigned long arg)
+{
+	struct mtdswp_dev *swpdev = container_of(dev, struct mtdswp_dev, mbd_dev);
+
+	switch (cmd) {
+	case BLKSWAPMARKUNUSED:
+                {
+		unsigned long eblknum, offset,realblock, page = arg - 1;
+		struct swp_eblk *eblk;
+
+		realblock = swpdev->blk_data[page].mapno;
+
+		if (realblock == -1) {
+			TRACE("Marking %ld (block %ld) unused but hasn't been written to!\n",
+					page, realblock);
+			return 0;
+		}
+
+		offset = realblock % swpdev->pages_per_blk;
+		eblknum = realblock / swpdev->pages_per_blk;
+		eblk = &swpdev->eblk_data[eblknum];
+
+		TRACE("Marking %ld as unused\n", page);
+
+		if (page > swpdev->pages) {
+			printk(KERN_ERR "Page outside of page range accessed (%ld, %d)\n",
+					page, swpdev->pages);
+			return 0;
+		}
+
+		if (!test_and_clear_bit(offset, eblk->active))
+			TRACE("Marking %ld (block %ld) unused but hasn't been written to!\n",
+					page, realblock);
+
+		mtdswap_store_block(swpdev, eblk);
+		swpdev->blk_data[page].mapno = -1;
+
+		return 0;
+		}
+	default:
+		return -ENOTTY;
+	}
+}
+
+static ssize_t mtdswap_show_stats_item(char *name, int count,
+		struct rb_root *root, char *buf, int buflen)
+{
+	int min = -1, max = -1;
+
+	if (root->rb_node != NULL) {
+		min = rb_entry(rb_first(root), struct swp_eblk, rb)->erase_count;
+		max = rb_entry(rb_last(root), struct swp_eblk, rb)->erase_count;
+	}
+
+	return snprintf(buf, buflen, "%s: %d (%d,%d) ", name, count, min, max);
+}
+
+static ssize_t mtdswap_show_stats(struct gendisk *disk, char *page)
+{
+	struct mtd_blktrans_dev *blkdev = disk->private_data;
+	struct mtdswp_dev *swpdev = container_of(blkdev, struct mtdswp_dev, mbd_dev);
+	ssize_t len = 0;
+	unsigned long flags;
+
+	spin_lock_irqsave(&swpdev->trees_lock, flags);
+	len += mtdswap_show_stats_item("clean", swpdev->clean_count,
+			&swpdev->clean, page, PAGE_SIZE);
+	len += mtdswap_show_stats_item("used", swpdev->used_count,
+			&swpdev->used, page+len, PAGE_SIZE-len);
+	len += mtdswap_show_stats_item("low", swpdev->low_frag_count,
+			&swpdev->low_frag, page+len, PAGE_SIZE-len);
+	len += mtdswap_show_stats_item("high", swpdev->high_frag_count,
+			&swpdev->high_frag, page+len, PAGE_SIZE-len);
+	len += mtdswap_show_stats_item("dirty", swpdev->dirty_count,
+			&swpdev->dirty, page+len, PAGE_SIZE-len);
+	spin_unlock_irqrestore(&swpdev->trees_lock, flags);
+
+	len += snprintf(page+len, PAGE_SIZE-len, "\n");
+	return len;
+}
+
+static struct disk_attribute disk_attr_stats = {
+	.attr = {.name = "stat-mtdswap", .mode = S_IRUGO },
+	.show = mtdswap_show_stats
+};
+
+static int mtdswap_add_sysfs(struct mtd_blktrans_dev *blkdev)
+{
+	struct gendisk *gd = get_mtd_blktrans_gendisk(blkdev);
+
+	return sysfs_create_file(&gd->kobj, &disk_attr_stats.attr);
+}
+
+static void mtdswap_rm_sysfs(struct mtd_blktrans_dev *blkdev)
+{
+	struct gendisk *gd = get_mtd_blktrans_gendisk(blkdev);
+
+	sysfs_remove_link(&gd->kobj, "stat-mtdswap");
+}
+
+static int mtdswap_open(struct mtd_blktrans_dev *mbd)
+{
+	return 0;
+}
+
+static int mtdswap_release(struct mtd_blktrans_dev *mbd)
+{
+	return 0;
+}
+
+static int mtdswap_init(struct mtdswp_dev *swpdev)
+{
+	struct nand_ecclayout *oinfo;
+	struct mtd_info *mtd = swpdev->mbd_dev.mtd;
+	int i, ret = -ENOMEM;
+
+	swpdev->mtd = mtd;
+	swpdev->pages = mtd->size >> PAGE_SHIFT;
+	swpdev->eblks = mtd->size >> (ffs(mtd->erasesize) - 1);
+	swpdev->pages_per_blk = swpdev->pages / swpdev->eblks;
+
+	spin_lock_init(&swpdev->trees_lock);
+	mutex_init(&swpdev->write_mutex);
+	swpdev->clean = swpdev->used = swpdev->low_frag = RB_ROOT;
+	swpdev->high_frag = swpdev->dirty = RB_ROOT;
+
+	init_waitqueue_head(&swpdev->thread_wq);
+	init_waitqueue_head(&swpdev->clean_wq);
+	init_timer(&swpdev->thread_timer);
+	swpdev->thread_timer.function = swpdev_timer_wakeup;
+	swpdev->thread_timer.data = (unsigned long) swpdev;
+
+	if (swpdev->pages_per_blk > MAX_PAGES_PER_EB) {
+		printk(KERN_ERR "Error: Maximum of %d pages per eraseblock exceeded.\n",
+				MAX_PAGES_PER_EB);
+		ret = -EINVAL;
+		goto blk_fail;
+	}
+	if (mtd->writesize > PAGE_SIZE) {
+		printk(KERN_ERR "Error: mtd->writesize (%d) > PAGE_SIZE (%ld) unsupported\n",
+				mtd->writesize, PAGE_SIZE);
+		ret = -EINVAL;
+		goto blk_fail;
+	}
+
+	oinfo = swpdev->mtd->ecclayout;
+	if (!swpdev->mtd->oobsize || !oinfo
+			|| (oinfo->oobfree[0].length < SWPDEV_OOBSIZE))
+		swpdev->fsdata_pos = -1;
+	else
+		swpdev->fsdata_pos = oinfo->oobfree[0].offset;
+
+	swpdev->blk_data = vmalloc(sizeof(struct swp_blk) * swpdev->pages);
+	if (!swpdev->blk_data)
+		goto blk_fail;
+
+	swpdev->eblk_data = kzalloc(sizeof(struct swp_eblk) * swpdev->eblks, GFP_KERNEL);
+	if (!swpdev->eblk_data)
+		goto eblk_fail;
+
+	for (i = 0; i < swpdev->pages; i++)
+		swpdev->blk_data[i].mapno = -1;
+
+	for (i = 0; i < swpdev->eblks; i++) {
+		int status = swpdev_check_markers(swpdev, &swpdev->eblk_data[i]);
+
+		if (status == 2 || status < 0)
+			continue;
+		if (status == 0) {
+			mtdswap_rb_add(swpdev, &swpdev->eblk_data[i], &swpdev->clean);
+			swpdev->eblk_data[i].root = &swpdev->clean;
+		} else {
+			mtdswap_rb_add(swpdev, &swpdev->eblk_data[i], &swpdev->dirty);
+			swpdev->eblk_data[i].root = &swpdev->dirty;
+		}
+	}
+
+	swpdev->page = alloc_page(GFP_KERNEL);
+	if (!swpdev->page)
+		goto page_fail;
+
+	swpdev->page_buf = page_address(swpdev->page);
+
+	ret = mtdswap_add_sysfs(&swpdev->mbd_dev);
+	if (ret < 0)
+		goto sysfs_fail;
+
+	swpdev->thread = kthread_run(mtdswap_thread, swpdev, "mtdswapd_%d", swpdev->mtd->index);
+	if (IS_ERR(swpdev->thread)) {
+		ret = PTR_ERR(swpdev->thread);
+		goto thread_fail;
+	}
+
+	return 0;
+
+thread_fail:
+	mtdswap_rm_sysfs(&swpdev->mbd_dev);
+sysfs_fail:
+	__free_page(swpdev->page);
+page_fail:
+	kfree(swpdev->eblk_data);
+eblk_fail:
+	vfree(swpdev->blk_data);
+blk_fail:
+	kfree(swpdev);
+	printk(KERN_ERR "mtdswap init failed (%d)\n", ret);
+	return ret;
+}
+
+static void mtdswap_cleanup(struct mtdswp_dev *swpdev)
+{
+   	TRACE("mtdswap_release\n");
+
+	mtdswap_rm_sysfs(&swpdev->mbd_dev);
+
+	/* Clean up the kernel thread */
+	del_timer_sync(&swpdev->thread_timer);
+	kthread_stop(swpdev->thread);
+
+	kfree(swpdev->eblk_data);
+	vfree(swpdev->blk_data);
+	__free_page(swpdev->page);
+}
+
+static int mtdswap_flush(struct mtd_blktrans_dev *dev)
+{
+	struct mtdswp_dev *swpdev = container_of(dev, struct mtdswp_dev, mbd_dev);
+
+	if (swpdev->mtd->sync)
+		swpdev->mtd->sync(swpdev->mtd);
+	return 0;
+}
+
+static char partitions[128] = "";
+module_param_string(partitions, partitions, sizeof(partitions), 0);
+MODULE_PARM_DESC(partitions, "MTD partitions numbers to use as swap: partitions=\"1,3,5\"");
+
+static void mtdswap_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)
+{
+	struct mtdswp_dev *dev;
+	struct mtd_blktrans_dev *mbd_dev;
+	char *parts = &partitions[0];
+	char *this_opt;
+	char *opt_end;
+	int part = -1;
+
+	if (!parts || !*parts)
+		return;
+
+	while ((this_opt = strsep(&parts, ",")) != NULL) {
+		part = simple_strtoul(this_opt, &opt_end, 0);
+		if (this_opt == opt_end)
+			continue;
+		if (mtd->index == part)
+			break;
+	}
+
+	if ((part == -1) || (mtd->index != part) || (this_opt == opt_end))
+		return;
+
+	printk(KERN_INFO "Enabling MTD swap on device %d\n", part);
+
+	dev = kzalloc(sizeof(struct mtdswp_dev), GFP_KERNEL);
+	if (!dev)
+		return;
+
+	mbd_dev = &dev->mbd_dev;
+	mbd_dev->mtd = mtd;
+	mbd_dev->devnum = mtd->index;
+	mbd_dev->size = ((mtd->size - (mtd->erasesize * NUMBER_SPARE_BLOCKS))
+			>> PAGE_SHIFT) + 1;
+	mbd_dev->tr = tr;
+
+	if (!(mtd->flags & MTD_WRITEABLE))
+		mbd_dev->readonly = 1;
+
+	if (add_mtd_blktrans_dev(mbd_dev) < 0)
+		return;
+	mtdswap_init(dev);
+}
+
+static void mtdswap_remove_dev(struct mtd_blktrans_dev *dev)
+{
+	struct mtdswp_dev *swpdev = container_of(dev, struct mtdswp_dev, mbd_dev);
+
+	mtdswap_cleanup(swpdev);
+	del_mtd_blktrans_dev(dev);
+	kfree(swpdev);
+}
+
+static struct mtd_blktrans_ops mtdswap_ops = {
+	.name		= "mtdswap",
+	.major		= 0,
+	.part_bits	= 0,
+	.blksize 	= PAGE_SIZE,
+	.open		= mtdswap_open,
+	.flush		= mtdswap_flush,
+	.release	= mtdswap_release,
+	.readsect	= mtdswap_readsect,
+	.writesect	= mtdswap_writesect,
+	.ioctl		= mtdswap_ioctl,
+	.add_mtd	= mtdswap_add_mtd,
+	.remove_dev	= mtdswap_remove_dev,
+	.owner		= THIS_MODULE,
+};
+
+static int __init mtdswap_modinit(void)
+{
+	return register_mtd_blktrans(&mtdswap_ops);
+}
+
+static void __exit mtdswap_modexit(void)
+{
+	deregister_mtd_blktrans(&mtdswap_ops);
+}
+
+module_init(mtdswap_modinit);
+module_exit(mtdswap_modexit);
+
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Richard Purdie <richard at openedhand.com>");
+MODULE_DESCRIPTION("Block device access to an MTD suitable for using as swap space");
Index: linux/drivers/mtd/Kconfig
===================================================================
--- linux.orig/drivers/mtd/Kconfig	2007-02-28 18:12:16.000000000 +0000
+++ linux/drivers/mtd/Kconfig	2007-02-28 18:12:48.000000000 +0000
@@ -282,6 +282,13 @@ config SSFDC
 	  This enables read only access to SmartMedia formatted NAND
 	  flash. You can mount it with FAT file system.
 
+config MTD_SWAP
+        tristate "Swap on MTD device support"
+	depends on MTD && SWAP
+	select MTD_BLKDEVS
+	---help---
+	  This provides support for swap space on an mtd device.
+
 source "drivers/mtd/chips/Kconfig"
 
 source "drivers/mtd/maps/Kconfig"
Index: linux/drivers/mtd/Makefile
===================================================================
--- linux.orig/drivers/mtd/Makefile	2007-02-28 18:12:16.000000000 +0000
+++ linux/drivers/mtd/Makefile	2007-02-28 18:12:48.000000000 +0000
@@ -23,6 +23,7 @@ obj-$(CONFIG_NFTL)		+= nftl.o
 obj-$(CONFIG_INFTL)		+= inftl.o
 obj-$(CONFIG_RFD_FTL)		+= rfd_ftl.o
 obj-$(CONFIG_SSFDC)		+= ssfdc.o
+obj-$(CONFIG_MTD_SWAP)		+= mtdswap.o
 
 nftl-objs		:= nftlcore.o nftlmount.o
 inftl-objs		:= inftlcore.o inftlmount.o






More information about the linux-mtd mailing list