diff --git a/init/Kconfig b/init/Kconfig
index c02d897..6f94b5a 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -294,6 +294,8 @@ config USELIB
 	  earlier, you may need to enable this syscall.  Current systems
 	  running glibc can safely disable this.
 
+source mm/duet/Kconfig
+
 config AUDIT
 	bool "Auditing support"
 	depends on NET
diff --git a/mm/Makefile b/mm/Makefile
index 78c6f7d..074c15f 100644
--- a/mm/Makefile
+++ b/mm/Makefile
@@ -99,3 +99,4 @@ obj-$(CONFIG_USERFAULTFD) += userfaultfd.o
 obj-$(CONFIG_IDLE_PAGE_TRACKING) += page_idle.o
 obj-$(CONFIG_FRAME_VECTOR) += frame_vector.o
 obj-$(CONFIG_DEBUG_PAGE_REF) += debug_page_ref.o
+obj-$(CONFIG_DUET) += duet/
diff --git a/mm/duet/Kconfig b/mm/duet/Kconfig
new file mode 100644
index 0000000..2f3a0c5
--- /dev/null
+++ b/mm/duet/Kconfig
@@ -0,0 +1,31 @@
+config DUET
+	bool "Duet framework support"
+
+	help
+	  Duet is a framework aiming to reduce the IO footprint of analytics
+	  and maintenance work. By exposing page cache events to these tasks,
+	  it allows them to adapt their data processing order, in order to
+	  benefit from data available in the page cache. Duet's operation is
+	  based on hooks into the page cache.
+
+	  To compile support for Duet, say Y.
+
+config DUET_STATS
+	bool "Duet statistics collection"
+	depends on DUET
+	help
+	  This option enables support for the collection of statistics on the
+	  operation of Duet. It will print information about the data structures
+	  used internally, and profiling information about the framework.
+
+	  If unsure, say N.
+
+config DUET_DEBUG
+	bool "Duet debugging support"
+	depends on DUET
+	help
+	  Enable runtime debugging support for the Duet framework. This may
+	  enable additional and expensive checks with negative impact on
+	  performance.
+
+	  To compile debugging support for Duet, say Y. If unsure, say N.
diff --git a/mm/duet/Makefile b/mm/duet/Makefile
new file mode 100644
index 0000000..c0c9e11
--- /dev/null
+++ b/mm/duet/Makefile
@@ -0,0 +1,7 @@
+#
+# Makefile for the linux Duet framework.
+#
+
+obj-$(CONFIG_DUET) += duet.o
+
+duet-y := init.o hash.o hook.o task.o bittree.o path.o debug.o
diff --git a/mm/duet/bittree.c b/mm/duet/bittree.c
new file mode 100644
index 0000000..3b20c35
--- /dev/null
+++ b/mm/duet/bittree.c
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include "common.h"
+
+#define BMAP_READ	0x01	/* Read bmaps (overrides other flags) */
+#define BMAP_CHECK	0x02	/* Check given bmap value expression */
+				/* Sets bmaps to match expression if not set */
+
+/* Bmap expressions can be formed using the following flags: */
+#define BMAP_DONE_SET	0x04	/* Set done bmap values */
+#define BMAP_DONE_RST	0x08	/* Reset done bmap values */
+#define BMAP_RELV_SET	0x10	/* Set relevant bmap values */
+#define BMAP_RELV_RST	0x20	/* Reset relevant bmap values */
+#define BMAP_SEEN_SET	0x40	/* Set seen bmap values */
+#define BMAP_SEEN_RST	0x80	/* Reset seen bmap values */
+
+/* Some macros to make our life easier */
+#define BMAP_ALL_SET	(BMAP_SEEN_SET | BMAP_RELV_SET | BMAP_DONE_SET)
+#define BMAP_ALL_RST	(BMAP_SEEN_RST | BMAP_RELV_RST | BMAP_DONE_RST)
+
+#define BITTREE_RANGE	PAGE_SIZE	/* Bytes per bitmap bit */
+#define BITS_PER_NODE	(32768 * 8)	/* 32KB bitmaps */
+
+#define UUID_IDX(uuid)	(((unsigned long long) uuid.gen << 32) | \
+			  (unsigned long long) uuid.ino)
+/*
+ * The following functions are wrappers for the basic bitmap functions.
+ * A bitmap is characterized by a starting offset (bstart). The wrappers
+ * translate an arbitrary idx to the appropriate bit.
+ */
+
+/* Sets (or resets) a single bit */
+static int bmap_set(unsigned long *bmap, __u64 start, __u64 idx, __u8 do_set)
+{
+	__u64 bofft = idx - start;
+
+	if (bofft + 1 >= start + (BITS_PER_NODE * BITTREE_RANGE))
+		return -1;
+
+	/* Convert range to bitmap granularity */
+	do_div(bofft, BITTREE_RANGE);
+
+	if (do_set)
+		bitmap_set(bmap, (unsigned int)bofft, 1);
+	else
+		bitmap_clear(bmap, (unsigned int)bofft, 1);
+
+	return 0;
+}
+
+/* Returns value of bit at idx */
+static int bmap_read(unsigned long *bmap, __u64 start, __u64 idx)
+{
+	__u64 bofft64 = idx - start;
+	unsigned long *p, mask;
+	unsigned int bofft;
+
+	if (bofft64 + 1 >= start + (BITS_PER_NODE * BITTREE_RANGE))
+		return -1;
+
+	/* Convert offset to bitmap granularity */
+	do_div(bofft64, BITTREE_RANGE);
+	bofft = (unsigned int)bofft64;
+
+	/* Check the bits */
+	p = bmap + BIT_WORD(bofft);
+	mask = BITMAP_FIRST_WORD_MASK(bofft) & BITMAP_LAST_WORD_MASK(bofft + 1);
+
+	if ((*p) & mask)
+		return 1;
+
+	return 0;
+}
+
+/* Checks whether a bit is set */
+static int bmap_chk(unsigned long *bmap, __u64 start, __u64 idx, __u8 do_set)
+{
+	__u64 bofft64 = idx - start;
+	unsigned long *p, mask;
+	unsigned int bofft;
+
+	if (bofft64 + 1 >= start + (BITS_PER_NODE * BITTREE_RANGE))
+		return -1;
+
+	/* Convert range to bitmap granularity */
+	do_div(bofft64, BITTREE_RANGE);
+
+	/* Now it is safe to cast these variables */
+	bofft = (unsigned int)bofft64;
+
+	/* Check the bit */
+	p = bmap + BIT_WORD(bofft);
+	mask = BITMAP_FIRST_WORD_MASK(bofft) & BITMAP_LAST_WORD_MASK(bofft + 1);
+
+	if (do_set && !((*p) & mask))
+		return 0;
+	else if (!do_set && !(~(*p) & mask))
+		return 0;
+
+	return 1;
+}
+
+/* Initializes a bitmap tree node */
+static struct bmap_rbnode *bnode_init(struct duet_bittree *bt, __u64 idx)
+{
+	struct bmap_rbnode *bnode = NULL;
+
+#ifdef CONFIG_DUET_STATS
+	if (bt) {
+		(bt->statcur)++;
+		if (bt->statcur > bt->statmax) {
+			bt->statmax = bt->statcur;
+			pr_info("duet: %llu BitTree nodes (%llub)\n",
+				bt->statmax, bt->statmax * BITS_PER_NODE / 8);
+		}
+	}
+#endif /* CONFIG_DUET_STATS */
+
+	bnode = kmalloc(sizeof(*bnode), GFP_NOWAIT);
+	if (!bnode)
+		return NULL;
+
+	bnode->done = kzalloc(sizeof(unsigned long) *
+			BITS_TO_LONGS(BITS_PER_NODE), GFP_NOWAIT);
+	if (!bnode->done) {
+		kfree(bnode);
+		return NULL;
+	}
+
+	/* Allocate relevant bitmap, if needed */
+	bnode->relv = kzalloc(sizeof(unsigned long) *
+		BITS_TO_LONGS(BITS_PER_NODE), GFP_NOWAIT);
+	if (!bnode->relv) {
+		kfree(bnode->done);
+		kfree(bnode);
+		return NULL;
+	}
+
+	bnode->seen = kzalloc(sizeof(unsigned long) *
+		BITS_TO_LONGS(BITS_PER_NODE), GFP_NOWAIT);
+	if (!bnode->seen) {
+		kfree(bnode->relv);
+		kfree(bnode->done);
+		kfree(bnode);
+		return NULL;
+	}
+
+	RB_CLEAR_NODE(&bnode->node);
+	bnode->idx = idx;
+	return bnode;
+}
+
+static void bnode_dispose(struct bmap_rbnode *bnode, struct rb_node *rbnode,
+	struct duet_bittree *bt)
+{
+#ifdef CONFIG_DUET_STATS
+	if (bt)
+		(bt->statcur)--;
+#endif /* CONFIG_DUET_STATS */
+	rb_erase(rbnode, &bt->root);
+	kfree(bnode->relv);
+	kfree(bnode->seen);
+	kfree(bnode->done);
+	kfree(bnode);
+}
+
+/*
+ * Traverses bitmap nodes to read/set/unset/check a specific bit across bitmaps.
+ * May insert/remove bitmap nodes as needed.
+ *
+ * If DUET_BMAP_READ is set:
+ * - the bitmap value for idx are read for one or all bitmaps
+ * Otherwise, if DUET_BMAP_CHECK flag is set:
+ * - return value 1 means the idx matches the given flags
+ * - return value 0 means the idx doesn't match the given flags
+ * Otherwise, if neither flag is set:
+ * - return value 0 means the idx was updated to match given flags
+ *
+ * In all cases, a return value -1 denotes an error.
+ */
+static int __update_tree(struct duet_bittree *bt, __u64 idx, __u8 flags)
+{
+	int found, ret, res;
+	__u64 node_offt, div_rem;
+	struct rb_node **link, *parent;
+	struct bmap_rbnode *bnode = NULL;
+	unsigned long iflags;
+
+	local_irq_save(iflags);
+	spin_lock(&bt->lock);
+
+	div64_u64_rem(idx, BITTREE_RANGE * BITS_PER_NODE, &div_rem);
+	node_offt = idx - div_rem;
+
+	/* Look up BitTree node */
+	found = 0;
+	link = &(bt->root).rb_node;
+	parent = NULL;
+
+	while (*link) {
+		parent = *link;
+		bnode = rb_entry(parent, struct bmap_rbnode, node);
+
+		if (bnode->idx > node_offt) {
+			link = &(*link)->rb_left;
+		} else if (bnode->idx < node_offt) {
+			link = &(*link)->rb_right;
+		} else {
+			found = 1;
+			break;
+		}
+	}
+
+	/* If we're just reading bitmap values, return them now */
+	if (flags & BMAP_READ) {
+		ret = 0;
+
+		if (!found)
+			goto done;
+
+		/* First read seen bit */
+		res = bmap_read(bnode->seen, bnode->idx, idx);
+		if (res == -1) {
+			ret = -1;
+			goto done;
+		}
+		ret |= res << 2;
+
+		/* Then read relevant bit */
+		res = bmap_read(bnode->relv, bnode->idx, idx);
+		if (res == -1) {
+			ret = -1;
+			goto done;
+		}
+		ret |= res << 1;
+
+		/* Read done bit */
+		res = bmap_read(bnode->done, bnode->idx, idx);
+		if (res == -1) {
+			ret = -1;
+			goto done;
+		}
+
+		ret |= res;
+		goto done;
+	}
+
+	/*
+	 * Take appropriate action based on whether we found the node
+	 * and whether we plan to update (SET/RST), or only CHECK it.
+	 *
+	 *   NULL  |       Found            !Found      |
+	 *  -------+------------------------------------+
+	 *    SET  |     Set Bits     |  Init new node  |
+	 *         |------------------+-----------------|
+	 *    RST  | Clear (dispose?) |     Nothing     |
+	 *  -------+------------------------------------+
+	 *
+	 *  CHECK  |       Found            !Found      |
+	 *  -------+------------------------------------+
+	 *    SET  |    Check Bits    |  Return false   |
+	 *         |------------------+-----------------|
+	 *    RST  |    Check Bits    |    Continue     |
+	 *  -------+------------------------------------+
+	 */
+
+	/* First handle setting (or checking set) bits */
+	if (flags & BMAP_ALL_SET) {
+		if (!found && !(flags & BMAP_CHECK)) {
+			/* Insert the new node */
+			bnode = bnode_init(bt, node_offt);
+			if (!bnode) {
+				ret = -1;
+				goto done;
+			}
+
+			rb_link_node(&bnode->node, parent, link);
+			rb_insert_color(&bnode->node, &bt->root);
+
+		} else if (!found && (flags & BMAP_CHECK)) {
+			/* Looking for set bits, node didn't exist */
+			ret = 0;
+			goto done;
+		}
+
+		/* Set the bits. Return -1 if something goes wrong. */
+		if (!(flags & BMAP_CHECK)) {
+			if ((flags & BMAP_SEEN_SET) &&
+			    bmap_set(bnode->seen, bnode->idx, idx, 1)) {
+				ret = -1;
+				goto done;
+			}
+
+			if ((flags & BMAP_RELV_SET) &&
+			    bmap_set(bnode->relv, bnode->idx, idx, 1)) {
+				ret = -1;
+				goto done;
+			}
+
+			if ((flags & BMAP_DONE_SET) &&
+			    bmap_set(bnode->done, bnode->idx, idx, 1)) {
+				ret = -1;
+				goto done;
+			}
+
+		/* Check the bits. Return if any bits are off */
+		} else {
+			if (flags & BMAP_SEEN_SET) {
+				ret = bmap_chk(bnode->seen, bnode->idx, idx, 1);
+				if (ret != 1)
+					goto done;
+			}
+
+			if (flags & BMAP_RELV_SET) {
+				ret = bmap_chk(bnode->relv, bnode->idx, idx, 1);
+				if (ret != 1)
+					goto done;
+			}
+
+			ret = bmap_chk(bnode->done, bnode->idx, idx, 1);
+			if (ret != 1)
+				goto done;
+		}
+	}
+
+	/* Now handle unsetting any bits */
+	if (found && (flags & BMAP_ALL_RST)) {
+		/* Clear the bits. Return -1 if something goes wrong. */
+		if (!(flags & BMAP_CHECK)) {
+			if ((flags & BMAP_SEEN_RST) &&
+			    bmap_set(bnode->seen, bnode->idx, idx, 0)) {
+				ret = -1;
+				goto done;
+			}
+
+			if ((flags & BMAP_RELV_RST) &&
+			    bmap_set(bnode->relv, bnode->idx, idx, 0)) {
+				ret = -1;
+				goto done;
+			}
+
+			if ((flags & BMAP_DONE_RST) &&
+			    bmap_set(bnode->done, bnode->idx, idx, 0)) {
+				ret = -1;
+				goto done;
+			}
+
+		/* Check the bits. Return if any bits are off */
+		} else {
+			if (flags & BMAP_SEEN_RST) {
+				ret = bmap_chk(bnode->seen, bnode->idx, idx, 0);
+				if (ret != 1)
+					goto done;
+			}
+
+			if (flags & BMAP_RELV_RST) {
+				ret = bmap_chk(bnode->relv, bnode->idx, idx, 0);
+				if (ret != 1)
+					goto done;
+			}
+
+			ret = bmap_chk(bnode->done, bnode->idx, idx, 0);
+			if (ret != 1)
+				goto done;
+		}
+
+		/* Dispose of the node if empty */
+		if (!(flags & BMAP_CHECK) &&
+		    bitmap_empty(bnode->done, BITS_PER_NODE) &&
+		    bitmap_empty(bnode->seen, BITS_PER_NODE) &&
+		    bitmap_empty(bnode->relv, BITS_PER_NODE))
+			bnode_dispose(bnode, parent, bt);
+	}
+
+	if (!(flags & BMAP_CHECK))
+		ret = 0;
+	else
+		ret = 1;
+
+done:
+	if (ret == -1)
+		pr_err("duet: blocks were not %s\n",
+			(flags & BMAP_READ) ? "read" :
+			((flags & BMAP_CHECK) ? "checked" : "modified"));
+	spin_unlock(&bt->lock);
+	local_irq_restore(iflags);
+	return ret;
+}
+
+/*
+ * Check if we have seen this inode before. If not, check if it is relevant.
+ * Then, check whether it's done.
+ */
+static int do_bittree_check(struct duet_bittree *bt, struct duet_uuid uuid,
+			    struct duet_task *task, struct inode *inode)
+{
+	int ret, bits;
+	unsigned long long idx = UUID_IDX(uuid);
+
+	bits = __update_tree(bt, idx, BMAP_READ);
+
+	if (!(bits & 0x4)) {
+		/* We have not seen this inode before */
+		if (inode) {
+			ret = do_find_path(task, inode, 0, NULL, 0);
+		} else if (task) {
+			ret = duet_find_path(task, uuid, 0, NULL, 0);
+		} else {
+			pr_err("duet: check failed, no task/inode\n");
+			return -1;
+		}
+
+		if (!ret) {
+			/* Mark as relevant and return not done */
+			ret = __update_tree(bt, idx,
+					    BMAP_SEEN_SET | BMAP_RELV_SET);
+			if (ret != -1)
+				ret = 0;
+
+		} else if (ret == -ENOENT) {
+			/* Mark as irrelevant and return done */
+			ret = __update_tree(bt, idx, BMAP_SEEN_SET);
+			if (ret != -1)
+				ret = 1;
+
+		} else {
+			pr_err("duet: inode relevance undetermined\n");
+			return -1;
+		}
+
+	} else {
+		/* We know this inode, return 1 if done, or irrelevant */
+		ret = ((bits & 0x1) || !(bits & 0x2)) ? 1 : 0;
+	}
+
+	return ret;
+}
+
+/* Checks if a given inode is done. Skips inode lookup. */
+int bittree_check_inode(struct duet_bittree *bt, struct duet_task *task,
+	struct inode *inode)
+{
+	struct duet_uuid uuid;
+
+	uuid.ino = inode->i_ino;
+	uuid.gen = inode->i_generation;
+
+	return do_bittree_check(bt, uuid, task, inode);
+}
+
+/* Checks if the given entries are done */
+int bittree_check(struct duet_bittree *bt, struct duet_uuid uuid,
+		  struct duet_task *task)
+{
+	return do_bittree_check(bt, uuid, task, NULL);
+}
+
+/* Mark done bit for given entries */
+int bittree_set(struct duet_bittree *bt, struct duet_uuid uuid)
+{
+	return __update_tree(bt, UUID_IDX(uuid), BMAP_DONE_SET);
+}
+
+/* Unmark done bit for given entries */
+int bittree_reset(struct duet_bittree *bt, struct duet_uuid uuid)
+{
+	return __update_tree(bt, UUID_IDX(uuid), BMAP_DONE_RST);
+}
+
+int bittree_print(struct duet_task *task)
+{
+	struct bmap_rbnode *bnode = NULL;
+	struct rb_node *node;
+	unsigned long iflags;
+
+	local_irq_save(iflags);
+	spin_lock(&task->bittree.lock);
+	pr_info("duet: Printing task bittree\n");
+	node = rb_first(&task->bittree.root);
+	while (node) {
+		bnode = rb_entry(node, struct bmap_rbnode, node);
+
+		/* Print node information */
+		pr_info("duet: Node key = %llu\n", bnode->idx);
+		pr_info("duet:   Done bits set: %d out of %d\n",
+			bitmap_weight(bnode->done, BITS_PER_NODE),
+			BITS_PER_NODE);
+		pr_info("duet:   Relv bits set: %d out of %d\n",
+			bitmap_weight(bnode->relv, BITS_PER_NODE),
+			BITS_PER_NODE);
+		pr_info("duet:   Seen bits set: %d out of %d\n",
+			bitmap_weight(bnode->seen, BITS_PER_NODE),
+			BITS_PER_NODE);
+
+		node = rb_next(node);
+	}
+	spin_unlock(&task->bittree.lock);
+	local_irq_restore(iflags);
+
+	pr_info("duet: Task #%d bitmap has %d out of %lu bits set\n",
+		task->id, bitmap_weight(task->bucket_bmap,
+		duet_env.itm_hash_size), duet_env.itm_hash_size);
+
+	return 0;
+}
+
+void bittree_init(struct duet_bittree *bittree)
+{
+	spin_lock_init(&bittree->lock);
+	bittree->root = RB_ROOT;
+#ifdef CONFIG_DUET_STATS
+	bittree->statcur = bittree->statmax = 0;
+#endif /* CONFIG_DUET_STATS */
+}
+
+void bittree_destroy(struct duet_bittree *bittree)
+{
+	struct rb_node *rbnode;
+	struct bmap_rbnode *bnode;
+
+	while (!RB_EMPTY_ROOT(&bittree->root)) {
+		rbnode = rb_first(&bittree->root);
+		bnode = rb_entry(rbnode, struct bmap_rbnode, node);
+		bnode_dispose(bnode, rbnode, bittree);
+	}
+}
diff --git a/mm/duet/common.h b/mm/duet/common.h
new file mode 100644
index 0000000..1dac66b
--- /dev/null
+++ b/mm/duet/common.h
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+#ifndef _COMMON_H
+#define _COMMON_H
+
+#include <linux/fs.h>
+#include <linux/mount.h>
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+#include <linux/bitmap.h>
+#include <linux/rculist.h>
+#include <linux/syscalls.h>
+#include <linux/duet.h>
+
+#ifdef DUET_DEBUG
+#define duet_dbg(...)	pr_info(__VA_ARGS__)
+#else
+#define duet_dbg(...)
+#endif
+
+/*
+ * Duet can be state-based, and/or event-based.
+ *
+ * Event-based Duet monitors events that occurred on a page, during its
+ * time in the page cache: ADDED, REMOVED, DIRTY, and FLUSHED.
+ *
+ * State-based Duet monitors changes in the page cache since the last time
+ * a notification was sent to the interested application. Registering for
+ * EXIST informs the application of page additions or removals from the cache
+ * (i.e. ADDED and REMOVED events cancel each other out if the application
+ * hasn't been told in the meantime). Registering for MODIFIED events is a
+ * similar model, where unreported DIRTY and FLUSHED events cancel each other.
+ */
+#define DUET_PAGE_ADDED		0x0001
+#define DUET_PAGE_REMOVED	0x0002
+#define DUET_PAGE_DIRTY		0x0004
+#define DUET_PAGE_FLUSHED	0x0008
+#define DUET_PAGE_MODIFIED	0x0010
+#define DUET_PAGE_EXISTS	0x0020
+#define DUET_FD_NONBLOCK	0x0040
+
+/* Used only for page state */
+#define DUET_MASK_VALID		0x8000
+
+/* Some useful flags for clearing bitmaps */
+#define BMAP_SEEN	0x1
+#define BMAP_RELV	0x2
+#define BMAP_DONE	0x4
+
+#define DUET_DEF_NUMTASKS	8
+#define DUET_INODE_FREEING	(I_WILL_FREE | I_FREEING | I_CLEAR)
+
+enum {
+	DUET_STATUS_OFF = 0,
+	DUET_STATUS_ON,
+	DUET_STATUS_INIT,
+	DUET_STATUS_CLEAN,
+};
+
+/*
+ * Item struct returned for processing.
+ * The UUID currently consists of the inode number, the inode generation
+ * (to help us identify cases of inode reuse), and the task id.
+ * For state-based duet, we mark a page if it EXISTS or is MODIFIED.
+ * For event-based duet, we mark a page added, removed, dirtied, and/or flushed.
+ * Acceptable event combinations will differ based on the task's subscription.
+ */
+struct duet_uuid {
+	unsigned long	ino;
+	__u32		gen;
+	__u8		tid;
+};
+
+struct duet_item {
+	struct duet_uuid	uuid;
+	unsigned long		idx;
+	__u16			state;
+};
+
+/*
+ * Red-black bitmap tree node.
+ * Represents the range starting from idx. For block tasks, only the done
+ * bitmap is used. For file tasks, the seen and relv (relevant) bitmaps are
+ * also used. The semantics of different states are listed below, where an
+ * item can be in the unknown state due to a bitmap reset, or because it hasn't
+ * been encountered yet.
+ * - !SEEN && !RELV && !DONE: Item in unknown state
+ * - !SEEN && !RELV &&  DONE: Item processed, but in unknown state
+ * -  SEEN && !RELV && !DONE: Item not relevant to the task
+ * -  SEEN &&  RELV && !DONE: Item is relevant, but not processed
+ * -  SEEN &&  RELV &&  DONE: Item is relevant, and has already been processed
+ */
+struct bmap_rbnode {
+	__u64		idx;
+	struct rb_node	node;
+	unsigned long	*seen;
+	unsigned long	*relv;
+	unsigned long	*done;
+};
+
+struct item_hnode {
+	struct hlist_bl_node	node;
+	struct duet_item	item;
+	__u8			refcount;
+	__u16			*state;		/* One entry per task */
+};
+
+struct duet_bittree {
+	spinlock_t		lock;
+	struct rb_root		root;
+#ifdef CONFIG_DUET_STATS
+	__u64			statcur;	/* Cur # of BitTree nodes */
+	__u64			statmax;	/* Max # of BitTree nodes */
+#endif /* CONFIG_DUET_STATS */
+};
+
+struct duet_task {
+	__u8			id;		/* Internal task ID */
+
+	int			fd;
+	struct filename		*name;
+	__u32			evtmask;	/* Mask of subscribed events */
+	struct path		*regpath;	/* Registered path */
+	char			*regpathname;	/* Registered path name */
+	__u16			regpathlen;	/* Length of path name */
+
+	/* Data structures linking task to framework */
+	struct list_head	task_list;
+	wait_queue_head_t	cleaner_queue;
+	atomic_t		refcount;
+	char			*pathbuf;	/* Buffer for getpath */
+	struct duet_bittree	bittree;	/* Progress bitmap */
+	wait_queue_head_t	event_queue;	/* for read() and poll() */
+
+	/* Hash table bucket bitmap */
+	spinlock_t		bbmap_lock;
+	unsigned long		*bucket_bmap;
+	unsigned long		bmap_cursor;
+};
+
+struct duet_info {
+	atomic_t		status;
+	__u8			numtasks;	/* Number of concurrent tasks */
+
+	/*
+	 * Access to the task list is synchronized via a mutex. However, any
+	 * operations that are on-going for a task (e.g. fetch) will increase
+	 * its refcount. This refcount is consulted when disposing of the task.
+	 */
+	struct mutex		task_list_mutex;
+	struct list_head	tasks;
+
+	/* ItemTable -- Global page state hash table */
+	struct hlist_bl_head	*itm_hash_table;
+	unsigned long		itm_hash_size;
+	unsigned long		itm_hash_shift;
+	unsigned long		itm_hash_mask;
+};
+
+extern struct duet_info duet_env;
+
+/* hook.c */
+void duet_hook(__u16 evtcode, void *data);
+
+/* hash.c */
+int hash_init(void);
+int hash_add(struct duet_task *task, struct duet_uuid uuid,
+	     unsigned long idx, __u16 evtmask, short in_scan);
+int hash_fetch(struct duet_task *task, struct duet_item *itm);
+void hash_print(struct duet_task *task);
+
+/* task.c -- not in linux/duet.h */
+struct duet_task *duet_find_task(__u8 id);
+void duet_task_dispose(struct duet_task *task);
+
+/* path.c */
+int do_find_path(struct duet_task *task, struct inode *inode,
+		 int getpath, char *buf, int bufsize);
+int duet_find_path(struct duet_task *task, struct duet_uuid uuid,
+		   int getpath, char *buf, int bufsize);
+
+/* bittree.c */
+int bittree_check_inode(struct duet_bittree *bt, struct duet_task *task,
+			struct inode *inode);
+int bittree_check(struct duet_bittree *bt, struct duet_uuid uuid,
+		  struct duet_task *task);
+int bittree_set(struct duet_bittree *bt, struct duet_uuid uuid);
+int bittree_reset(struct duet_bittree *bt, struct duet_uuid uuid);
+int bittree_print(struct duet_task *task);
+void bittree_init(struct duet_bittree *bittree);
+void bittree_destroy(struct duet_bittree *bittree);
+
+/* init.c */
+int duet_online(void);
+
+/* debug.c */
+int duet_print_bmap(__u8 id);
+int duet_print_item(__u8 id);
+int duet_print_list(struct duet_status_args __user *arg);
+
+#endif /* _COMMON_H */
diff --git a/mm/duet/debug.c b/mm/duet/debug.c
new file mode 100644
index 0000000..77a47b6
--- /dev/null
+++ b/mm/duet/debug.c
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include "common.h"
+#include "syscall.h"
+
+/* Do a preorder print of the BitTree */
+int duet_print_bmap(__u8 id)
+{
+	struct duet_task *task;
+
+	task = duet_find_task(id);
+	if (!task)
+		return -ENOENT;
+
+	if (bittree_print(task)) {
+		pr_err("duet: failed to print BitTree for task #%d\n",
+			task->id);
+		return -1;
+	}
+
+	/* decref and wake up cleaner if needed */
+	if (atomic_dec_and_test(&task->refcount))
+		wake_up(&task->cleaner_queue);
+
+	return 0;
+}
+
+/* Do a preorder print of the global hash table */
+int duet_print_item(__u8 id)
+{
+	struct duet_task *task;
+
+	task = duet_find_task(id);
+	if (!task)
+		return -ENOENT;
+
+	hash_print(task);
+
+	/* decref and wake up cleaner if needed */
+	if (atomic_dec_and_test(&task->refcount))
+		wake_up(&task->cleaner_queue);
+
+	return 0;
+}
+
+int duet_print_list(struct duet_status_args __user *arg)
+{
+	int i = 0;
+	struct duet_task *cur;
+	struct duet_status_args argh, *argp;
+
+	/* Copy in task list header (again) */
+	if (copy_from_user(&argh, arg, sizeof(argh)))
+		return -EFAULT;
+
+	/* Copy in entire task list */
+	argp = memdup_user(arg, sizeof(*argp) + (argh.numtasks *
+				sizeof(struct duet_task_attrs)));
+	if (IS_ERR(argp))
+		return PTR_ERR(argp);
+
+	/* Copy the info for the first numtasks */
+	mutex_lock(&duet_env.task_list_mutex);
+	list_for_each_entry(cur, &duet_env.tasks, task_list) {
+		argp->tasks[i].id = cur->id;
+		argp->tasks[i].fd = cur->fd;
+		memcpy(argp->tasks[i].name, cur->name->name, NAME_MAX);
+		argp->tasks[i].regmask = cur->evtmask;
+		memcpy(argp->tasks[i].path, cur->regpathname, cur->regpathlen);
+		i++;
+		if (i == argp->numtasks)
+			break;
+	}
+	mutex_unlock(&duet_env.task_list_mutex);
+
+	/* Copy out entire task list */
+	if (copy_to_user(arg, argp, sizeof(*argp) + (argp->numtasks *
+			 sizeof(struct duet_task_attrs)))) {
+		pr_err("duet_print_list: failed to copy out list\n");
+		kfree(argp);
+		return -EINVAL;
+	}
+
+	duet_dbg("duet_print_list: success sending task list\n");
+	kfree(argp);
+	return 0;
+}
diff --git a/mm/duet/hash.c b/mm/duet/hash.c
new file mode 100644
index 0000000..c2644d6
--- /dev/null
+++ b/mm/duet/hash.c
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include <linux/hash.h>
+#include "common.h"
+
+#define DUET_NEGATE_EXISTS	(DUET_PAGE_ADDED | DUET_PAGE_REMOVED)
+#define DUET_NEGATE_MODIFIED	(DUET_PAGE_DIRTY | DUET_PAGE_FLUSHED)
+
+/*
+ * Page state is retained in a global hash table shared by all tasks.
+ * Indexing is based on the page's inode number and offset.
+ */
+
+static unsigned long hash(unsigned long ino, unsigned long idx)
+{
+	unsigned long h;
+
+	h = (idx * ino ^ (GOLDEN_RATIO_PRIME + idx)) / L1_CACHE_BYTES;
+	h = h ^ ((h ^ GOLDEN_RATIO_PRIME) >> duet_env.itm_hash_shift);
+	return h & duet_env.itm_hash_mask;
+}
+
+int hash_init(void)
+{
+	/* Allocate power-of-2 number of buckets */
+	duet_env.itm_hash_shift = ilog2(totalram_pages);
+	duet_env.itm_hash_size = 1 << duet_env.itm_hash_shift;
+	duet_env.itm_hash_mask = duet_env.itm_hash_size - 1;
+
+	pr_debug("duet: allocated global hash table (%lu buckets)\n",
+		duet_env.itm_hash_size);
+	duet_env.itm_hash_table = vmalloc(sizeof(struct hlist_bl_head) *
+						duet_env.itm_hash_size);
+	if (!duet_env.itm_hash_table)
+		return 1;
+
+	memset(duet_env.itm_hash_table, 0, sizeof(struct hlist_bl_head) *
+						duet_env.itm_hash_size);
+	return 0;
+}
+
+/* Deallocate a hash table node */
+static void hnode_destroy(struct item_hnode *itnode)
+{
+	kfree(itnode->state);
+	kfree(itnode);
+}
+
+/* Allocate and initialize a new hash table node */
+static struct item_hnode *hnode_init(struct duet_uuid uuid, unsigned long idx)
+{
+	struct item_hnode *itnode = NULL;
+
+	itnode = kzalloc(sizeof(struct item_hnode), GFP_NOWAIT);
+	if (!itnode)
+		return NULL;
+
+	itnode->state = kcalloc(duet_env.numtasks, sizeof(*(itnode->state)),
+				GFP_NOWAIT);
+	if (!(itnode->state)) {
+		pr_err("duet: failed to allocate hash node state\n");
+		kfree(itnode);
+		return NULL;
+	}
+
+	(itnode->item).uuid = uuid;
+	(itnode->item).idx = idx;
+	itnode->refcount++;
+
+	return itnode;
+}
+
+/* Add one event into the hash table */
+int hash_add(struct duet_task *task, struct duet_uuid uuid, unsigned long idx,
+	__u16 evtmask, short in_scan)
+{
+	__u16 curmask = 0;
+	short found = 0;
+	unsigned long bnum, flags;
+	struct hlist_bl_head *b;
+	struct hlist_bl_node *n;
+	struct item_hnode *itnode;
+
+	evtmask &= task->evtmask;
+
+	/* Get the bucket */
+	bnum = hash(uuid.ino, idx);
+	b = duet_env.itm_hash_table + bnum;
+	local_irq_save(flags);
+	hlist_bl_lock(b);
+
+	/* Lookup the item in the bucket */
+	hlist_bl_for_each_entry(itnode, n, b, node) {
+		if ((itnode->item).uuid.ino == uuid.ino &&
+		    (itnode->item).uuid.gen == uuid.gen &&
+		    (itnode->item).idx == idx) {
+			found = 1;
+			break;
+		}
+	}
+
+	duet_dbg("duet: %s hash node (tid %d, ino%lu, gen%lu, idx%lu)\n",
+		found ? (in_scan ? "replacing" : "updating") : "inserting",
+		uuid.tid, uuid.ino, uuid.gen, idx);
+
+	if (found) {
+		curmask = itnode->state[task->id];
+
+		/* Only up the refcount if we are adding a new mask */
+		if (!(curmask & DUET_MASK_VALID) || in_scan) {
+			if (!in_scan)
+				itnode->refcount++;
+			curmask = evtmask | DUET_MASK_VALID;
+			goto check_dispose;
+		}
+
+		curmask |= evtmask | DUET_MASK_VALID;
+
+		/* Negate previous events and remove if needed */
+		if ((task->evtmask & DUET_PAGE_EXISTS) &&
+		   ((curmask & DUET_NEGATE_EXISTS) == DUET_NEGATE_EXISTS))
+			curmask &= ~DUET_NEGATE_EXISTS;
+
+		if ((task->evtmask & DUET_PAGE_MODIFIED) &&
+		   ((curmask & DUET_NEGATE_MODIFIED) == DUET_NEGATE_MODIFIED))
+			curmask &= ~DUET_NEGATE_MODIFIED;
+
+check_dispose:
+		if ((curmask == DUET_MASK_VALID) && (itnode->refcount == 1)) {
+			if (itnode->refcount != 1) {
+				itnode->state[task->id] = 0;
+			} else {
+				hlist_bl_del(&itnode->node);
+				hnode_destroy(itnode);
+			}
+
+			/* Are we still interested in this bucket? */
+			hlist_bl_for_each_entry(itnode, n, b, node) {
+				if (itnode->state[task->id] & DUET_MASK_VALID) {
+					found = 1;
+					break;
+				}
+			}
+
+			if (!found)
+				clear_bit(bnum, task->bucket_bmap);
+		} else {
+			itnode->state[task->id] = curmask;
+
+			/* Update bitmap */
+			set_bit(bnum, task->bucket_bmap);
+		}
+	} else if (!found) {
+		if (!evtmask)
+			goto done;
+
+		itnode = hnode_init(uuid, idx);
+		if (!itnode)
+			return 1;
+
+		itnode->state[task->id] = evtmask | DUET_MASK_VALID;
+		hlist_bl_add_head(&itnode->node, b);
+
+		/* Update bitmap */
+		set_bit(bnum, task->bucket_bmap);
+	}
+
+done:
+	hlist_bl_unlock(b);
+	local_irq_restore(flags);
+	return 0;
+}
+
+/* Fetch one item for a given task. Return found (1), empty (0), error (-1) */
+int hash_fetch(struct duet_task *task, struct duet_item *itm)
+{
+	int found;
+	unsigned long bnum, flags;
+	struct hlist_bl_head *b;
+	struct hlist_bl_node *n;
+	struct item_hnode *itnode;
+
+	local_irq_save(flags);
+again:
+	spin_lock(&task->bbmap_lock);
+	bnum = find_next_bit(task->bucket_bmap, duet_env.itm_hash_size,
+			     task->bmap_cursor);
+
+	if (bnum == duet_env.itm_hash_size) {
+		/* Reached end of bitmap */
+		found = 0;
+
+		if (task->bmap_cursor != 0) {
+			/* Started part way, try again */
+			bnum = find_next_bit(task->bucket_bmap,
+					     task->bmap_cursor, 0);
+
+			if (bnum != task->bmap_cursor)
+				found = 1;
+		}
+
+		if (!found) {
+			spin_unlock(&task->bbmap_lock);
+			local_irq_restore(flags);
+			return 1;
+		}
+	}
+
+	task->bmap_cursor = bnum;
+	clear_bit(bnum, task->bucket_bmap);
+	spin_unlock(&task->bbmap_lock);
+	b = duet_env.itm_hash_table + bnum;
+
+	/* Grab first item from bucket */
+	hlist_bl_lock(b);
+	if (!b->first) {
+		pr_err("duet: empty hash bucket marked in bitmap\n");
+		hlist_bl_unlock(b);
+		goto again;
+	}
+
+	found = 0;
+	hlist_bl_for_each_entry(itnode, n, b, node) {
+		if (itnode->state[task->id] & DUET_MASK_VALID) {
+			*itm = itnode->item;
+			itm->uuid.tid = task->id;
+			itm->state = itnode->state[task->id] &
+				     (~DUET_MASK_VALID);
+
+			itnode->refcount--;
+			/* Free or update node */
+			if (!itnode->refcount) {
+				hlist_bl_del(n);
+				hnode_destroy(itnode);
+			} else {
+				itnode->state[task->id] = 0;
+			}
+
+			found = 1;
+			break;
+		}
+	}
+
+	if (!found) {
+		hlist_bl_unlock(b);
+		goto again;
+	}
+
+	/* Are we still interested in this bucket? */
+	found = 0;
+	hlist_bl_for_each_entry(itnode, n, b, node) {
+		if (itnode->state[task->id] & DUET_MASK_VALID) {
+			found = 1;
+			break;
+		}
+	}
+
+	if (found)
+		set_bit(bnum, task->bucket_bmap);
+
+	hlist_bl_unlock(b);
+	local_irq_restore(flags);
+	return 0;
+}
+
+/* Warning: expensive printing function. Use with care. */
+void hash_print(struct duet_task *task)
+{
+	unsigned long loop, count, start, end, buckets, flags;
+	unsigned long long nodes, tnodes;
+	struct hlist_bl_head *b;
+	struct hlist_bl_node *n;
+	struct item_hnode *itnode;
+
+	count = duet_env.itm_hash_size / 100;
+	tnodes = nodes = buckets = start = end = 0;
+	pr_info("duet: Printing hash table\n");
+	for (loop = 0; loop < duet_env.itm_hash_size; loop++) {
+		if (loop - start >= count) {
+			pr_info("duet:   Buckets %lu - %lu: %llu nodes (task: %llu)\n",
+				start, end, nodes, tnodes);
+			start = end = loop;
+			nodes = tnodes = 0;
+		}
+
+		/* Count bucket nodes */
+		b = duet_env.itm_hash_table + loop;
+		local_irq_save(flags);
+		hlist_bl_lock(b);
+		hlist_bl_for_each_entry(itnode, n, b, node) {
+			nodes++;
+			if (itnode->state[task->id] & DUET_MASK_VALID)
+				tnodes++;
+		}
+		hlist_bl_unlock(b);
+		local_irq_restore(flags);
+
+		end = loop;
+	}
+
+	if (start != loop - 1)
+		pr_info("duet:   Buckets %lu - %lu: %llu nodes (task: %llu)\n",
+			start, end, nodes, tnodes);
+}
diff --git a/mm/duet/hook.c b/mm/duet/hook.c
new file mode 100644
index 0000000..3ac89f5
--- /dev/null
+++ b/mm/duet/hook.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include "common.h"
+
+/* Handle an event. We're in RCU context so whatever happens, stay awake! */
+void duet_hook(__u16 evtcode, void *data)
+{
+	struct page *page = NULL;
+	struct inode *inode = NULL;
+	struct duet_task *cur;
+	unsigned long page_idx = 0;
+	struct duet_uuid uuid;
+
+	/* Duet must be online */
+	if (!duet_online())
+		return;
+
+	/* Handle page event */
+	page = (struct page *)data;
+
+	/* Duet must be online, and the page must belong to a valid mapping */
+	if (!page || !page_mapping(page)) {
+		duet_dbg("duet: dropped event %x due to NULL mapping\n",
+			evtcode);
+		return;
+	}
+
+	inode = page_mapping(page)->host;
+	page_idx = page->index;
+
+	/* Check that we're referring to an actual inode and get its UUID */
+	if (!inode)
+		return;
+
+	uuid.ino = inode->i_ino;
+	uuid.gen = inode->i_generation;
+
+	/* Verify that the inode does not belong to a special file */
+	if (!S_ISREG(inode->i_mode) && !S_ISDIR(inode->i_mode))
+		return;
+
+	if (!inode->i_ino) {
+		pr_err("duet: inode not initialized\n");
+		return;
+	}
+
+	/* Look for tasks interested in this event type and invoke callbacks */
+	rcu_read_lock();
+	list_for_each_entry_rcu(cur, &duet_env.tasks, task_list) {
+		struct super_block *sb = cur->regpath->mnt->mnt_sb;
+
+		/* Verify that the event refers to the fs we're interested in */
+		if (sb && sb != inode->i_sb)
+			continue;
+
+		duet_dbg("duet: rcvd event %x on (ino %lu, gen %lu, idx %lu)\n",
+			evtcode, uuid.ino, uuid.gen, page_idx);
+
+		/* Use the inode bitmap to filter out event if applicable */
+		if (bittree_check_inode(&cur->bittree, cur, inode) == 1)
+			continue;
+
+		/* Update the hash table */
+		if (hash_add(cur, uuid, page_idx, evtcode, 0))
+			pr_err("duet: hash table add failed\n");
+
+		wake_up(&cur->event_queue);
+	}
+	rcu_read_unlock();
+}
diff --git a/mm/duet/init.c b/mm/duet/init.c
new file mode 100644
index 0000000..a9e5ea1
--- /dev/null
+++ b/mm/duet/init.c
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include "common.h"
+#include "syscall.h"
+
+struct duet_info duet_env;
+duet_hook_t *duet_hook_fp;
+EXPORT_SYMBOL(duet_hook_fp);
+
+int duet_online(void)
+{
+	return (atomic_read(&duet_env.status) == DUET_STATUS_ON);
+}
+
+int duet_bootstrap(__u16 numtasks)
+{
+	if (atomic_cmpxchg(&duet_env.status, DUET_STATUS_OFF, DUET_STATUS_INIT)
+	    != DUET_STATUS_OFF) {
+		pr_err("duet: framework on, bootstrap aborted\n");
+		return 1;
+	}
+
+	duet_env.numtasks = (numtasks ? numtasks : DUET_DEF_NUMTASKS);
+
+	/* Initialize global hash table */
+	if (hash_init()) {
+		pr_err("duet: failed to initialize hash table\n");
+		return 1;
+	}
+
+	/* Initialize task list */
+	INIT_LIST_HEAD(&duet_env.tasks);
+	mutex_init(&duet_env.task_list_mutex);
+	atomic_set(&duet_env.status, DUET_STATUS_ON);
+
+	rcu_assign_pointer(duet_hook_fp, duet_hook);
+	synchronize_rcu();
+	return 0;
+}
+
+int duet_shutdown(void)
+{
+	struct duet_task *task;
+
+	if (atomic_cmpxchg(&duet_env.status, DUET_STATUS_ON, DUET_STATUS_CLEAN)
+	    != DUET_STATUS_ON) {
+		pr_err("duet: framework off, shutdown aborted\n");
+		return 1;
+	}
+
+	rcu_assign_pointer(duet_hook_fp, NULL);
+	synchronize_rcu();
+
+	/* Remove all tasks */
+	mutex_lock(&duet_env.task_list_mutex);
+	while (!list_empty(&duet_env.tasks)) {
+		task = list_entry_rcu(duet_env.tasks.next, struct duet_task,
+				task_list);
+		list_del_rcu(&task->task_list);
+		mutex_unlock(&duet_env.task_list_mutex);
+
+		/* Make sure everyone's let go before we free it */
+		synchronize_rcu();
+		wait_event(task->cleaner_queue,
+			atomic_read(&task->refcount) == 0);
+		duet_task_dispose(task);
+
+		mutex_lock(&duet_env.task_list_mutex);
+	}
+	mutex_unlock(&duet_env.task_list_mutex);
+
+	/* Destroy global hash table */
+	vfree((void *)duet_env.itm_hash_table);
+
+	INIT_LIST_HEAD(&duet_env.tasks);
+	mutex_destroy(&duet_env.task_list_mutex);
+	atomic_set(&duet_env.status, DUET_STATUS_OFF);
+	return 0;
+}
+
+SYSCALL_DEFINE2(duet_status, u16, flags, struct duet_status_args __user *, arg)
+{
+	int ret = 0;
+	struct duet_status_args *sa;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
+	sa = memdup_user(arg, sizeof(*sa));
+	if (IS_ERR(sa))
+		return PTR_ERR(sa);
+
+	/* For now, we only support one struct size */
+	if (sa->size != sizeof(*sa)) {
+		pr_err("duet_status: invalid args struct size (%u)\n",
+			sa->size);
+		ret = -EINVAL;
+		goto done;
+	}
+
+	/* If we're cleaning up, only allow ops that affect Duet status */
+	if (atomic_read(&duet_env.status) != DUET_STATUS_ON && !(flags &
+	    (DUET_STATUS_START | DUET_STATUS_STOP | DUET_STATUS_REPORT))) {
+		pr_err("duet_status: ops rejected during shutdown\n");
+		ret = -EINVAL;
+		goto done;
+	}
+
+	switch (flags) {
+	case DUET_STATUS_START:
+		ret = duet_bootstrap(sa->maxtasks);
+
+		if (ret)
+			pr_err("duet: failed to enable framework\n");
+		else
+			pr_info("duet: framework enabled\n");
+
+		break;
+
+	case DUET_STATUS_STOP:
+		ret = duet_shutdown();
+
+		if (ret)
+			pr_err("duet: failed to disable framework\n");
+		else
+			pr_info("duet: framework disabled\n");
+
+		break;
+
+	case DUET_STATUS_REPORT:
+		ret = duet_online();
+		break;
+
+	case DUET_STATUS_PRINT_BMAP:
+		ret = duet_print_bmap(sa->id);
+		break;
+
+	case DUET_STATUS_PRINT_ITEM:
+		ret = duet_print_item(sa->id);
+		break;
+
+	case DUET_STATUS_PRINT_LIST:
+		ret = duet_print_list(arg);
+		goto done;
+
+	default:
+		pr_info("duet_status: invalid flags\n");
+		ret = -EINVAL;
+		goto done;
+	}
+
+	if (copy_to_user(arg, sa, sizeof(*sa))) {
+		pr_err("duet_status: copy_to_user failed\n");
+		ret = -EINVAL;
+		goto done;
+	}
+
+done:
+	kfree(sa);
+	return ret;
+}
diff --git a/mm/duet/path.c b/mm/duet/path.c
new file mode 100644
index 0000000..103ab42
--- /dev/null
+++ b/mm/duet/path.c
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include "common.h"
+#include "syscall.h"
+
+/* Scan through the page cache for a given inode */
+static int find_get_inode(struct super_block *sb, struct duet_uuid c_uuid,
+	struct inode **c_inode)
+{
+	struct inode *inode = NULL;
+
+	*c_inode = NULL;
+	spin_lock(&sb->s_inode_list_lock);
+	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
+		spin_lock(&inode->i_lock);
+		if (!*c_inode && inode->i_ino == c_uuid.ino &&
+		    inode->i_generation == c_uuid.gen &&
+		    !(inode->i_state & DUET_INODE_FREEING)) {
+			atomic_inc(&inode->i_count);
+			*c_inode = inode;
+			spin_unlock(&inode->i_lock);
+			spin_unlock(&sb->s_inode_list_lock);
+			return 0;
+		}
+		spin_unlock(&inode->i_lock);
+	}
+	spin_unlock(&sb->s_inode_list_lock);
+
+	/* We shouldn't get here unless we failed */
+	return 1;
+}
+
+int do_find_path(struct duet_task *task, struct inode *inode, int getpath,
+	char *buf, int bufsize)
+{
+	int len;
+	char *p;
+	struct path path;
+	struct dentry *alias, *i_dentry = NULL;
+
+	if (!task) {
+		pr_err("do_find_path: invalid task\n");
+		return -EINVAL;
+	}
+
+	if (getpath)
+		buf[0] = '\0';
+
+	/* Get the path for at least one alias of the inode */
+	if (hlist_empty(&inode->i_dentry))
+		return -ENOENT;
+
+	hlist_for_each_entry(alias, &inode->i_dentry, d_u.d_alias) {
+		if (IS_ROOT(alias) && (alias->d_flags & DCACHE_DISCONNECTED))
+			continue;
+
+		i_dentry = alias;
+
+		/* Now get the path */
+		len = PATH_MAX;
+		memset(task->pathbuf, 0, len);
+		path.mnt = task->regpath->mnt;
+		path.dentry = i_dentry;
+
+		p = d_path(&path, task->pathbuf, len);
+		if (IS_ERR(p)) {
+			pr_err("do_find_path: d_path failed\n");
+			continue;
+		} else if (!p) {
+			duet_dbg("do_find_path: dentry not found\n");
+			continue;
+		}
+
+		/* Is this path of interest? */
+		if (memcmp(task->regpathname, p, task->regpathlen - 1)) {
+			duet_dbg("do_find_path: no common ancestor\n");
+			continue;
+		}
+
+		/* Got one. If it fits, return it */
+		if (getpath && (bufsize < len - (p - task->pathbuf)))
+			return -ENOMEM;
+
+		duet_dbg("do_find_path: got %s\n", p);
+		if (getpath)
+			memcpy(buf, p, len - (p - task->pathbuf));
+
+		return 0;
+	}
+
+	/* We only get here if we got nothing */
+	return -ENOENT;
+}
+
+int duet_find_path(struct duet_task *task, struct duet_uuid uuid, int getpath,
+	char *buf, int bufsize)
+{
+	int ret = 0;
+	struct inode *ino;
+
+	if (!task) {
+		pr_err("duet_find_path: invalid task\n");
+		return -EINVAL;
+	}
+
+	/* First, we need to find struct inode for child and parent */
+	if (find_get_inode(task->regpath->mnt->mnt_sb, uuid, &ino)) {
+		duet_dbg("duet_find_path: child inode not found\n");
+		return -ENOENT;
+	}
+
+	ret = do_find_path(task, ino, getpath, buf, bufsize);
+
+	iput(ino);
+	return ret;
+}
+
+SYSCALL_DEFINE3(duet_get_path, struct duet_uuid_arg __user *, uuid,
+		char __user *, pathbuf, int, pathbufsize)
+{
+	int ret = 0;
+	struct duet_uuid_arg *ua;
+	struct duet_task *task;
+	char *buf;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
+	if (!duet_online())
+		return -ESRCH;
+
+	/* Do some basic sanity checking */
+	if (!uuid || pathbufsize <= 0)
+		return -EINVAL;
+
+	buf = kcalloc(pathbufsize, sizeof(char), GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	ua = memdup_user(uuid, sizeof(*uuid));
+	if (IS_ERR(ua)) {
+		kfree(buf);
+		return PTR_ERR(ua);
+	}
+
+	/* For now, we only support one struct size */
+	if (ua->size != sizeof(*ua)) {
+		pr_err("duet_get_path: invalid args struct size (%u)\n",
+			ua->size);
+		ret = -EINVAL;
+		goto done;
+	}
+
+	task = duet_find_task(ua->uuid.tid);
+	if (!task) {
+		ret = -ENOENT;
+		goto done;
+	}
+
+	ret = duet_find_path(task, ua->uuid, 1, buf, pathbufsize);
+
+	if (!ret && copy_to_user(pathbuf, buf, pathbufsize))
+		ret = -EFAULT;
+
+	/* decref and wake up cleaner if needed */
+	if (atomic_dec_and_test(&task->refcount))
+		wake_up(&task->cleaner_queue);
+
+done:
+	kfree(ua);
+	kfree(buf);
+	return ret;
+}
diff --git a/mm/duet/syscall.h b/mm/duet/syscall.h
new file mode 100644
index 0000000..1bc6830
--- /dev/null
+++ b/mm/duet/syscall.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include "common.h"
+
+/* Status syscall flags */
+#define DUET_STATUS_START	0x0001
+#define DUET_STATUS_STOP	0x0002
+#define DUET_STATUS_REPORT	0x0004
+#define DUET_STATUS_PRINT_BMAP	0x0008
+#define DUET_STATUS_PRINT_ITEM	0x0010
+#define DUET_STATUS_PRINT_LIST	0x0020
+
+struct duet_task_attrs {
+	__u8	id;
+	int	fd;
+	char	name[NAME_MAX];
+	__u32	regmask;
+	char	path[PATH_MAX];
+};
+
+struct duet_status_args {
+	__u32 size;						/* in */
+	union {
+		/* DUET_START args */
+		struct {
+			__u16 maxtasks;				/* in */
+		};
+
+		/* DUET_PRINT_BIT, DUET_PRINT_ITEM args */
+		struct {
+			__u8 id;				/* in */
+		};
+
+		/* DUET_PRINT_LIST args */
+		struct {
+			__u16 numtasks;				/* out */
+			struct duet_task_attrs tasks[0];	/* out */
+		};
+	};
+};
+
+/* Bmap syscall flags */
+#define DUET_BMAP_SET		0x0001
+#define DUET_BMAP_RESET		0x0002
+#define DUET_BMAP_CHECK		0x0004
+
+struct duet_uuid_arg {
+	__u32			size;
+	struct duet_uuid	uuid;
+};
diff --git a/mm/duet/task.c b/mm/duet/task.c
new file mode 100644
index 0000000..c91fad3
--- /dev/null
+++ b/mm/duet/task.c
@@ -0,0 +1,584 @@
+/*
+ * Copyright (C) 2016 George Amvrosiadis.  All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License v2 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.
+ */
+
+#include <linux/namei.h>
+#include <linux/anon_inodes.h>
+#include "common.h"
+#include "syscall.h"
+
+/*
+ * To synchronize access to the task list and structures without compromising
+ * scalability, a two-level approach is used. At the task list level, which is
+ * rarely updated, RCU is used. For the task structures, we use traditional
+ * reference counting. The two techniques are interweaved to achieve overall
+ * consistency.
+ */
+
+static unsigned int duet_poll(struct file *file, poll_table *wait)
+{
+	__u8 *tid = file->private_data;
+	struct duet_task *task;
+	int ret = 0;
+
+	task = duet_find_task(*tid);
+	if (!task) {
+		pr_err("duet_poll: task not found\n");
+		return ret;
+	}
+
+	poll_wait(file, &task->event_queue, wait);
+	if (bitmap_weight(task->bucket_bmap, duet_env.itm_hash_size))
+		ret = POLLIN | POLLRDNORM;
+
+	return ret;
+}
+
+/*
+ * Copy an item to user space, returning how much we copied.
+ *
+ * We already checked that the event size is smaller than the
+ * buffer we had in duet_read() below.
+ */
+static ssize_t copy_item_to_user(struct duet_task *task,
+				 struct duet_item *item,
+				 char __user *buf)
+{
+	size_t item_size = sizeof(struct duet_item);
+
+	/* Send the item */
+	if (copy_to_user(buf, item, item_size))
+		return -EFAULT;
+
+	buf += item_size;
+
+	duet_dbg("duet_read: sending (ino%lu, gen%u, idx%lu, %x)\n",
+		 item->uuid.ino, item->uuid.gen, item->idx, item->state);
+
+	return item_size;
+}
+
+/*
+ * Sends out duet items. The number of bytes returned corresponds to the number
+ * of sizeof(struct duet_item) items fetched. Items are checked against the
+ * bitmap, and discarded if they have been marked; this can happen because an
+ * insertion can occur between the last read and the last bitmap set operation.
+ */
+static ssize_t duet_read(struct file *file, char __user *buf,
+			 size_t count, loff_t *pos)
+{
+	struct duet_task *task;
+	struct duet_item item;
+	char __user *start;
+	int ret, err;
+	__u8 *tid;
+	DEFINE_WAIT_FUNC(wait, woken_wake_function);
+
+	start = buf;
+	tid = file->private_data;
+
+	task = duet_find_task(*tid);
+	if (!task)
+		return -ENOENT;
+
+	add_wait_queue(&task->event_queue, &wait);
+	while (1) {
+		/* Fetch an item only if there's space to store it */
+		if (sizeof(struct duet_item) > count)
+			err = -EINVAL;
+		else
+			err = hash_fetch(task, &item);
+
+		if (!err) {
+			ret = copy_item_to_user(task, &item, buf);
+			if (ret < 0)
+				break;
+			buf += ret;
+			count -= ret;
+			continue;
+		}
+
+		ret = -EAGAIN;
+		if (file->f_flags & O_NONBLOCK)
+			break;
+		ret = -ERESTARTSYS;
+		if (signal_pending(current))
+			break;
+
+		if (start != buf)
+			break;
+
+		wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
+	}
+	remove_wait_queue(&task->event_queue, &wait);
+
+	if (start != buf && ret != -EFAULT)
+		ret = buf - start;
+
+	/* Decref and wake up cleaner if needed */
+	if (atomic_dec_and_test(&task->refcount))
+		wake_up(&task->cleaner_queue);
+
+	return ret;
+}
+
+/*
+ * Properly dismantle and dispose of a task struct.
+ * At this point we've guaranteed that noone else is accessing the
+ * task struct, so we don't need any locks
+ */
+void duet_task_dispose(struct duet_task *task)
+{
+	int ret = 0;
+	struct duet_item itm;
+
+	/* Dispose of the bitmap tree */
+	bittree_destroy(&task->bittree);
+
+	/* Dispose of hash table entries, bucket bitmap */
+	while (!ret)
+		ret = hash_fetch(task, &itm);
+	kfree(task->bucket_bmap);
+
+	putname(task->name);
+	path_put(task->regpath);
+	kfree(task->regpath);
+	kfree(task->regpathname);
+	kfree(task->pathbuf);
+	kfree(task);
+}
+
+static int duet_release(struct inode *ignored, struct file *file)
+{
+	__u8 *tid = file->private_data;
+	struct duet_task *cur;
+
+	/* Find the task in the list, then dispose of it */
+	mutex_lock(&duet_env.task_list_mutex);
+	list_for_each_entry_rcu(cur, &duet_env.tasks, task_list) {
+		if (cur->id == *tid) {
+#ifdef CONFIG_DUET_STATS
+			hash_print(cur);
+			bittree_print(cur);
+#endif /* CONFIG_DUET_STATS */
+			list_del_rcu(&cur->task_list);
+			mutex_unlock(&duet_env.task_list_mutex);
+
+			/* Wait until everyone's done with it */
+			synchronize_rcu();
+			wait_event(cur->cleaner_queue,
+				atomic_read(&cur->refcount) == 0);
+
+			pr_info("duet: deregistered task %d\n",	cur->id);
+
+			duet_task_dispose(cur);
+			kfree(tid);
+			return 0;
+		}
+	}
+	mutex_unlock(&duet_env.task_list_mutex);
+
+	return -ENOENT;
+}
+
+static const struct file_operations duet_fops = {
+	.show_fdinfo	= NULL,
+	.poll		= duet_poll,
+	.read		= duet_read,
+	.fasync		= NULL,
+	.release	= duet_release,
+	.unlocked_ioctl	= NULL,
+	.compat_ioctl	= NULL,
+	.llseek		= noop_llseek,
+};
+
+static int process_inode(struct duet_task *task, struct inode *inode)
+{
+	struct radix_tree_iter iter;
+	struct duet_uuid uuid;
+	void **slot;
+	__u16 state;
+
+	/* Use the inode bitmap to decide whether to skip inode */
+	if (bittree_check_inode(&task->bittree, task, inode) == 1)
+		return 0;
+
+	/* Go through all pages of this inode */
+	rcu_read_lock();
+	radix_tree_for_each_slot(slot, &inode->i_mapping->page_tree, &iter, 0) {
+		struct page *page;
+
+		page = radix_tree_deref_slot(slot);
+		if (unlikely(!page))
+			continue;
+
+		if (radix_tree_exception(page)) {
+			if (radix_tree_deref_retry(page)) {
+				slot = radix_tree_iter_retry(&iter);
+				continue;
+			}
+			/*
+			 * Shadow entry of recently evicted page, or swap entry
+			 * from shmem/tmpfs. Skip over it.
+			 */
+			continue;
+		}
+
+		state = DUET_PAGE_ADDED;
+		if (PageDirty(page))
+			state |= DUET_PAGE_DIRTY;
+		uuid.ino = inode->i_ino;
+		uuid.gen = inode->i_generation;
+		hash_add(task, uuid, page->index, state, 1);
+	}
+	rcu_read_unlock();
+
+	return 0;
+}
+
+/* Scan through the page cache for events of interest to the task */
+static int scan_page_cache(struct duet_task *task)
+{
+	struct inode *inode, *prev = NULL;
+	struct super_block *sb = task->regpath->mnt->mnt_sb;
+
+	pr_info("duet: page cache scan started\n");
+
+	spin_lock(&sb->s_inode_list_lock);
+	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
+		struct address_space *mapping = inode->i_mapping;
+
+		spin_lock(&inode->i_lock);
+		if (inode->i_state & DUET_INODE_FREEING ||
+		    mapping->nrpages == 0) {
+			spin_unlock(&inode->i_lock);
+			continue;
+		}
+		atomic_inc(&inode->i_count);
+		spin_unlock(&inode->i_lock);
+		spin_unlock(&sb->s_inode_list_lock);
+
+		/*
+		 * We are holding a reference to inode so it won't be removed
+		 * from s_inodes list while we don't hold the s_inode_list_lock.
+		 * We cannot iput the inode now, though, as we may be holding
+		 * the last reference. We will iput it after the iteration is
+		 * done.
+		 */
+
+		iput(prev);
+		prev = inode;
+
+		process_inode(task, inode);
+
+		spin_lock(&sb->s_inode_list_lock);
+	}
+	spin_unlock(&sb->s_inode_list_lock);
+	iput(prev);
+
+	pr_info("duet: page cache scan finished\n");
+
+	return 0;
+}
+
+/* Find task and increment its refcount */
+struct duet_task *duet_find_task(__u8 id)
+{
+	struct duet_task *cur, *task = NULL;
+
+	rcu_read_lock();
+	list_for_each_entry_rcu(cur, &duet_env.tasks, task_list) {
+		if (cur->id == id) {
+			task = cur;
+			atomic_inc(&task->refcount);
+			break;
+		}
+	}
+	rcu_read_unlock();
+
+	return task;
+}
+
+/* Allocate and initialize a task struct */
+static int duet_task_init(struct duet_task **task, struct filename *name,
+	__u32 regmask, struct path *path)
+{
+	int len;
+	char *p;
+	u32 evtmask = regmask;
+
+	/* Do some sanity checking on event mask. */
+	if (evtmask & DUET_PAGE_EXISTS) {
+		if (evtmask & (DUET_PAGE_ADDED | DUET_PAGE_REMOVED)) {
+			pr_debug("duet_task_init: invalid regmask\n");
+			return -EINVAL;
+		}
+		evtmask |= (DUET_PAGE_ADDED | DUET_PAGE_REMOVED);
+	}
+
+	if (evtmask & DUET_PAGE_MODIFIED) {
+		if (evtmask & (DUET_PAGE_DIRTY | DUET_PAGE_FLUSHED)) {
+			pr_debug("duet_task_init: invalid regmask\n");
+			goto err;
+		}
+		evtmask |= (DUET_PAGE_DIRTY | DUET_PAGE_FLUSHED);
+	}
+
+	/* Allocate task info struct */
+	*task = kzalloc(sizeof(**task), GFP_KERNEL);
+	if (!(*task))
+		return -ENOMEM;
+
+	/* Allocate temporary space for getpath file paths */
+	(*task)->pathbuf = kzalloc(PATH_MAX, GFP_KERNEL);
+	if (!(*task)->pathbuf) {
+		pr_err("duet_task_init: buffer allocation failed\n");
+		kfree(*task);
+		return -ENOMEM;
+	}
+
+	/* Find and store registered dir path */
+	(*task)->regpathname = kzalloc(PATH_MAX, GFP_KERNEL);
+	if (!(*task)->regpathname) {
+		pr_err("duet_task_init: path allocation failed\n");
+		kfree((*task)->pathbuf);
+		kfree(*task);
+		return -ENOMEM;
+	}
+
+	/* Populate registered dir path buffer */
+	len = PATH_MAX;
+	p = d_path(path, (*task)->pathbuf, len);
+	if (IS_ERR(p)) {
+		pr_err("duet_task_init: path registration failed\n");
+		goto err;
+	} else if (!p) {
+		pr_err("duet_task_init: (null) registered path\n");
+		goto err;
+	}
+
+	(*task)->regpathlen = len - (p - (*task)->pathbuf);
+	memcpy((*task)->regpathname, p, (*task)->regpathlen);
+
+	(*task)->id = 1;
+	(*task)->name = name;
+	(*task)->regpath = path;
+	(*task)->evtmask = (regmask & 0xffff);
+	atomic_set(&(*task)->refcount, 0);
+	INIT_LIST_HEAD(&(*task)->task_list);
+	init_waitqueue_head(&(*task)->cleaner_queue);
+	init_waitqueue_head(&(*task)->event_queue);
+	bittree_init(&(*task)->bittree);
+
+	/* Initialize hash table bitmap */
+	(*task)->bmap_cursor = 0;
+	spin_lock_init(&(*task)->bbmap_lock);
+	(*task)->bucket_bmap = kzalloc(sizeof(unsigned long) *
+		BITS_TO_LONGS(duet_env.itm_hash_size), GFP_KERNEL);
+	if (!(*task)->bucket_bmap) {
+		pr_err("duet_task_init: hash bitmap alloc failed\n");
+		kfree((*task)->regpathname);
+		kfree((*task)->pathbuf);
+		kfree(*task);
+		return -ENOMEM;
+	}
+
+	return 0;
+err:
+	pr_err("duet_task_init: error registering task\n");
+	kfree((*task)->regpathname);
+	kfree((*task)->pathbuf);
+	kfree(*task);
+	return -EINVAL;
+}
+
+/* Register the task with Duet */
+int duet_register_task(struct filename *name, __u32 regmask, struct path *path)
+{
+	int ret;
+	__u8 *tid;
+	struct duet_task *cur, *task = NULL;
+	struct list_head *last;
+
+	/* Do some sanity checking on the parameters passed */
+	if (!path || !regmask)
+		return -EINVAL;
+
+	if (!path->dentry || !path->dentry->d_inode) {
+		pr_err("duet_register_task: invalid path\n");
+		return -EINVAL;
+	}
+
+	if (!S_ISDIR(path->dentry->d_inode->i_mode)) {
+		pr_err("duet_register_task: path is not a dir\n");
+		return -EINVAL;
+	}
+
+	ret = duet_task_init(&task, name, regmask, path);
+	if (ret) {
+		pr_err("duet_register_task: initialization failed\n");
+		return ret;
+	}
+
+	/* Now get an anonymous inode to use for communication with Duet */
+	tid = kzalloc(sizeof(__u8), GFP_KERNEL);
+	if (!tid) {
+		duet_task_dispose(task);
+		return -ENOMEM;
+	}
+
+	ret = anon_inode_getfd("duet", &duet_fops, tid,
+		O_RDONLY | ((regmask & DUET_FD_NONBLOCK) ? O_NONBLOCK : 0));
+	if (ret < 0) {
+		duet_task_dispose(task);
+		kfree(tid);
+		return ret;
+	}
+
+	task->fd = ret;
+
+	/* Find a free task id for the new task. Tasks are sorted by id. */
+	mutex_lock(&duet_env.task_list_mutex);
+	last = &duet_env.tasks;
+	list_for_each_entry_rcu(cur, &duet_env.tasks, task_list) {
+		if (cur->id == task->id)
+			(task->id)++;
+		else if (cur->id > task->id)
+			break;
+
+		last = &cur->task_list;
+	}
+	list_add_rcu(&task->task_list, last);
+	mutex_unlock(&duet_env.task_list_mutex);
+
+	*tid = task->id;
+
+	/* Before we return, scan the page cache for pages of interest */
+	scan_page_cache(task);
+
+	pr_info("duet: task %d (fd %d) registered %s(%d) with mask %x\n",
+		task->id, task->fd, task->regpathname, task->regpathlen,
+		task->evtmask);
+
+	return ret;
+}
+
+SYSCALL_DEFINE3(duet_init, const char __user *, taskname, u32, regmask,
+		const char __user *, pathname)
+{
+	int ret;
+	unsigned int lookup_flags = LOOKUP_DIRECTORY;
+	struct filename *name = NULL;
+	struct path *path = NULL;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
+	if (!duet_online())
+		return -ESRCH;
+
+	/* Do some basic sanity checking */
+	if (!pathname || !regmask)
+		return -EINVAL;
+
+	if (taskname) {
+		name = getname(taskname);
+		if (IS_ERR(name))
+			return PTR_ERR(name);
+	}
+
+	path = kzalloc(sizeof(struct path), GFP_KERNEL);
+	if (!path) {
+		putname(name);
+		return -ENOMEM;
+	}
+
+	ret = user_path_at(AT_FDCWD, pathname, lookup_flags, path);
+	if (ret) {
+		pr_err("duet_init: user_path_at failed\n");
+		goto err;
+	}
+
+	/* Register the task with the framework */
+	ret = duet_register_task(name, regmask, path);
+	if (ret < 0) {
+		pr_err("duet_init: task registration failed\n");
+		goto err;
+	}
+
+	return ret;
+
+err:
+	putname(name);
+	path_put(path);
+	kfree(path);
+	return ret;
+}
+
+SYSCALL_DEFINE2(duet_bmap, u16, flags, struct duet_uuid_arg __user *, arg)
+{
+	int ret = 0;
+	struct duet_uuid_arg *ua;
+	struct duet_task *task;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
+	if (!duet_online())
+		return -ESRCH;
+
+	/* Do some basic sanity checking */
+	if (!arg)
+		return -EINVAL;
+
+	ua = memdup_user(arg, sizeof(*arg));
+	if (IS_ERR(ua))
+		return PTR_ERR(ua);
+
+	/* For now, we only support one struct size */
+	if (ua->size != sizeof(*ua)) {
+		pr_err("duet_bmap: invalid args struct size (%u)\n", ua->size);
+		ret = -EINVAL;
+		goto done;
+	}
+
+	task = duet_find_task(ua->uuid.tid);
+	if (!task)
+		return -ENOENT;
+
+	switch (flags) {
+	case DUET_BMAP_SET:
+		ret = bittree_set(&task->bittree, ua->uuid);
+		break;
+
+	case DUET_BMAP_RESET:
+		ret = bittree_reset(&task->bittree, ua->uuid);
+		break;
+
+	case DUET_BMAP_CHECK:
+		ret = bittree_check(&task->bittree, ua->uuid, task);
+		break;
+
+	default:
+		pr_err("duet_bmap: invalid flags\n");
+		ret = -EINVAL;
+		break;
+	}
+
+	/* decreg and wake up cleaner if needed */
+	if (atomic_dec_and_test(&task->refcount))
+		wake_up(&task->cleaner_queue);
+
+done:
+	kfree(ua);
+	return ret;
+}
