[PATCH v4 4/6] fs/dcache: Enable automatic pruning of negative dentries

From: Waiman Long
Date: Mon Sep 18 2017 - 14:22:37 EST


Having a limit for the number of negative dentries may have an
undesirable side effect that no new negative dentries will be allowed
when the limit is reached. This may have a performance impact on some
workloads.

To prevent this from happening, we need a way to prune the negative
dentries so that new ones can be created before it is too late. This
is done by using the workqueue API to do the pruning gradually when a
threshold is reached to minimize performance impact on other running
tasks.

The current threshold is 1/4 of the initial value of the free pool
count. Once the threshold is reached, the automatic pruning process
will be kicked in to replenish the free pool. Each pruning run will
scan 64 dentries per LRU list and can remove up to 256 negative
dentries to minimize the LRU locks hold time. The pruning rate will
be 50 Hz if the free pool count is less than 1/8 of the original and
10 Hz otherwise.

A short artificial delay loop is added to wait for changes in the
negative dentry count before killing the negative dentry. Sleeping
in this case may be problematic as the callers of dput() may not
be in a state that is sleepable.

Allowing tasks needing negative dentries to potentially go to do
the pruning synchronously themselves can cause lock and cacheline
contention. The end result may not be better than that of killing
recently created negative dentries.

The dentry pruning operation will also free some least recently used
positive dentries as well.

In the unlikely event that a superblock is being umount'ed while in
negative dentry pruning mode, the umount may face an additional delay
of up to 0.1s.

With automatic pruning in place, forced negative dentry killing should
not happen unless there are pathological tasks present that generate
a large number of negative dentries in a very short time.

Signed-off-by: Waiman Long <longman@xxxxxxxxxx>
---
fs/dcache.c | 190 +++++++++++++++++++++++++++++++++++++++++++++--
include/linux/list_lru.h | 1 +
mm/list_lru.c | 4 +-
3 files changed, 189 insertions(+), 6 deletions(-)

diff --git a/fs/dcache.c b/fs/dcache.c
index 6fb65b8..dd1adda 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -146,14 +146,27 @@ struct dentry_stat_t dentry_stat = {
*/
#define NEG_DENTRY_PC 2
#define NEG_DENTRY_BATCH (1 << 8)
+#define NEG_PRUNING_SIZE (1 << 6)
+#define NEG_PRUNING_SLOW_RATE (HZ/10)
+#define NEG_PRUNING_FAST_RATE (HZ/50)
+#define NEG_IS_SB_UMOUNTING(sb) \
+ unlikely(!(sb)->s_root || !((sb)->s_flags & MS_ACTIVE))

static long neg_dentry_percpu_limit __read_mostly;
static long neg_dentry_nfree_init __read_mostly; /* Free pool initial value */
static struct {
raw_spinlock_t nfree_lock;
+ int niter; /* Pruning iteration count */
+ int lru_count; /* Per-LRU pruning count */
+ long n_neg; /* # of negative dentries pruned */
+ long n_pos; /* # of positive dentries pruned */
long nfree; /* Negative dentry free pool */
+ struct super_block *prune_sb; /* Super_block for pruning */
} ndblk ____cacheline_aligned_in_smp;

+static void prune_negative_dentry(struct work_struct *work);
+static DECLARE_DELAYED_WORK(prune_neg_dentry_work, prune_negative_dentry);
+
static DEFINE_PER_CPU(long, nr_dentry);
static DEFINE_PER_CPU(long, nr_dentry_unused);
static DEFINE_PER_CPU(long, nr_dentry_neg);
@@ -326,6 +339,25 @@ static void __neg_dentry_inc(struct dentry *dentry)
*/
if (!cnt)
dentry->d_flags |= DCACHE_KILL_NEGATIVE;
+
+ /*
+ * Initiate negative dentry pruning if free pool has less than
+ * 1/4 of its initial value.
+ */
+ if ((READ_ONCE(ndblk.nfree) < READ_ONCE(neg_dentry_nfree_init)/4) &&
+ !READ_ONCE(ndblk.prune_sb) &&
+ !cmpxchg(&ndblk.prune_sb, NULL, dentry->d_sb)) {
+ /*
+ * Abort if umounting is in progress, otherwise take a
+ * reference and move on.
+ */
+ if (NEG_IS_SB_UMOUNTING(ndblk.prune_sb)) {
+ WRITE_ONCE(ndblk.prune_sb, NULL);
+ } else {
+ atomic_inc(&ndblk.prune_sb->s_active);
+ schedule_delayed_work(&prune_neg_dentry_work, 1);
+ }
+ }
}

static inline void neg_dentry_inc(struct dentry *dentry)
@@ -767,10 +799,8 @@ static struct dentry *dentry_kill(struct dentry *dentry)
* disappear under the hood even if the dentry
* lock is temporarily released.
*/
- unsigned int dflags;
+ unsigned int dflags = dentry->d_flags;

- dentry->d_flags &= ~DCACHE_KILL_NEGATIVE;
- dflags = dentry->d_flags;
parent = lock_parent(dentry);
/*
* Abort the killing if the reference count or
@@ -961,8 +991,35 @@ void dput(struct dentry *dentry)

dentry_lru_add(dentry);

- if (unlikely(dentry->d_flags & DCACHE_KILL_NEGATIVE))
- goto kill_it;
+ if (unlikely(dentry->d_flags & DCACHE_KILL_NEGATIVE)) {
+ /*
+ * Kill the dentry if it is really negative and the per-cpu
+ * negative dentry count has still exceeded the limit even
+ * after a short artificial delay.
+ */
+ if (d_is_negative(dentry) &&
+ (this_cpu_read(nr_dentry_neg) > neg_dentry_percpu_limit)) {
+ int loop = 256;
+
+ /*
+ * Waiting to transfer free negative dentries from the
+ * free pool to the percpu count.
+ */
+ while (--loop) {
+ if (READ_ONCE(ndblk.nfree)) {
+ long cnt = __neg_dentry_nfree_dec();
+
+ this_cpu_sub(nr_dentry_neg, cnt);
+ if (cnt)
+ break;
+ }
+ cpu_relax();
+ }
+ if (!loop)
+ goto kill_it;
+ }
+ dentry->d_flags &= ~DCACHE_KILL_NEGATIVE;
+ }

dentry->d_lockref.count--;
spin_unlock(&dentry->d_lock);
@@ -1323,6 +1380,129 @@ void shrink_dcache_sb(struct super_block *sb)
}
EXPORT_SYMBOL(shrink_dcache_sb);

+/*
+ * A modified version that attempts to remove a limited number of negative
+ * dentries as well as some other non-negative dentries at the front.
+ */
+static enum lru_status dentry_negative_lru_isolate(struct list_head *item,
+ struct list_lru_one *lru, spinlock_t *lru_lock, void *arg)
+{
+ struct list_head *freeable = arg;
+ struct dentry *dentry = container_of(item, struct dentry, d_lru);
+ enum lru_status status = LRU_SKIP;
+
+ /*
+ * Limit amount of dentry walking in each LRU list.
+ */
+ if (ndblk.lru_count >= NEG_PRUNING_SIZE) {
+ ndblk.lru_count = 0;
+ return LRU_STOP;
+ }
+ ndblk.lru_count++;
+
+ /*
+ * we are inverting the lru lock/dentry->d_lock here,
+ * so use a trylock. If we fail to get the lock, just skip
+ * it
+ */
+ if (!spin_trylock(&dentry->d_lock))
+ return LRU_SKIP;
+
+ /*
+ * Referenced dentries are still in use. If they have active
+ * counts, just remove them from the LRU. Otherwise give them
+ * another pass through the LRU.
+ */
+ if (dentry->d_lockref.count) {
+ d_lru_isolate(lru, dentry);
+ status = LRU_REMOVED;
+ goto out;
+ }
+
+ /*
+ * Dentries with reference bit on are moved back to the tail.
+ */
+ if (dentry->d_flags & DCACHE_REFERENCED) {
+ dentry->d_flags &= ~DCACHE_REFERENCED;
+ status = LRU_ROTATE;
+ goto out;
+ }
+
+ status = LRU_REMOVED;
+ d_lru_shrink_move(lru, dentry, freeable);
+ if (d_is_negative(dentry))
+ ndblk.n_neg++;
+out:
+ spin_unlock(&dentry->d_lock);
+ return status;
+}
+
+/*
+ * A workqueue function to prune negative dentry.
+ *
+ * The pruning is done gradually over time so as to have as little
+ * performance impact as possible.
+ */
+static void prune_negative_dentry(struct work_struct *work)
+{
+ int freed, last_n_neg;
+ long nfree;
+ struct super_block *sb = READ_ONCE(ndblk.prune_sb);
+ LIST_HEAD(dispose);
+
+ if (!sb)
+ return;
+ if (NEG_IS_SB_UMOUNTING(sb))
+ goto stop_pruning;
+
+ ndblk.niter++;
+ ndblk.lru_count = 0;
+ last_n_neg = ndblk.n_neg;
+ freed = list_lru_walk(&sb->s_dentry_lru, dentry_negative_lru_isolate,
+ &dispose, NEG_DENTRY_BATCH);
+
+ if (freed)
+ shrink_dentry_list(&dispose);
+ ndblk.n_pos += freed - (ndblk.n_neg - last_n_neg);
+
+ /*
+ * Continue delayed pruning until negative dentry free pool is at
+ * least 1/2 of the initial value, the super_block has no more
+ * negative dentries left at the front, or unmounting is in
+ * progress.
+ *
+ * The pruning rate depends on the size of the free pool. The
+ * faster rate is used when there is less than 1/8 left.
+ * Otherwise, the slower rate will be used.
+ */
+ nfree = READ_ONCE(ndblk.nfree);
+ if ((ndblk.n_neg == last_n_neg) ||
+ (nfree >= neg_dentry_nfree_init/2) || NEG_IS_SB_UMOUNTING(sb))
+ goto stop_pruning;
+
+ schedule_delayed_work(&prune_neg_dentry_work,
+ (nfree < neg_dentry_nfree_init/8)
+ ? NEG_PRUNING_FAST_RATE : NEG_PRUNING_SLOW_RATE);
+ return;
+
+stop_pruning:
+#ifdef CONFIG_DEBUG_KERNEL
+ /*
+ * Report large negative dentry pruning event.
+ */
+ if (ndblk.n_neg > NEG_PRUNING_SIZE) {
+ pr_info("Negative dentry pruning (SB=%s):\n\t"
+ "%d iterations, %ld/%ld neg/pos dentries freed.\n",
+ ndblk.prune_sb->s_id, ndblk.niter, ndblk.n_neg,
+ ndblk.n_pos);
+ }
+#endif
+ ndblk.niter = 0;
+ ndblk.n_neg = ndblk.n_pos = 0;
+ deactivate_super(sb);
+ WRITE_ONCE(ndblk.prune_sb, NULL);
+}
+
/**
* enum d_walk_ret - action to talke during tree walk
* @D_WALK_CONTINUE: contrinue walk
diff --git a/include/linux/list_lru.h b/include/linux/list_lru.h
index fa7fd03..06c9d15 100644
--- a/include/linux/list_lru.h
+++ b/include/linux/list_lru.h
@@ -22,6 +22,7 @@ enum lru_status {
LRU_SKIP, /* item cannot be locked, skip */
LRU_RETRY, /* item not freeable. May drop the lock
internally, but has to return locked. */
+ LRU_STOP, /* stop walking the list */
};

struct list_lru_one {
diff --git a/mm/list_lru.c b/mm/list_lru.c
index 7a40fa2..f6e7796 100644
--- a/mm/list_lru.c
+++ b/mm/list_lru.c
@@ -244,11 +244,13 @@ unsigned long list_lru_count_node(struct list_lru *lru, int nid)
*/
assert_spin_locked(&nlru->lock);
goto restart;
+ case LRU_STOP:
+ goto out;
default:
BUG();
}
}
-
+out:
spin_unlock(&nlru->lock);
return isolated;
}
--
1.8.3.1