Skip to content

Commit e626ea2

Browse files
committed
Merge patch series "kernfs: remove kernfs_rwsem from dentry revalidation"
Shakeel Butt <shakeel.butt@linux.dev> says: kernfs: remove kernfs_rwsem from dentry revalidation At Meta, we are seeing important system daemons that poll cgroupfs and sysfs geth stuck in kernfs_dop_revalidate() for minutes. The two that hurt most are the ones we can least afford to lose: oomd, which decides what to kill when a machine runs out of memory, and below[1], which records the telemetry used to understand what happened afterwards. kernfs_dop_revalidate() takes kernfs_rwsem for read once per path component of every walk into a kernfs mount. Linux rwsems do not permit reader lock stealing once a writer is queued, so one writer -- a cgroup created or destroyed, a device renamed -- parks the entire incoming reader stream in uninterruptible sleep. Daemons polling cgroup files in a loop are exactly the workload that turns this into a convoy, and cgroup churn is exactly what a busy machine does. Nothing the callback reads needs the semaphore. kn->active is an atomic_t already tested lock-free elsewhere, kn->__parent and kn->name are RCU pointers, kn->ns can be compared rather than dereferenced, and parent->dir.rev is a plain counter. 1/4 uses the parent inode and name the VFS already passes to ->d_revalidate() rather than recovering them from mutable dentry fields, comparing the name by explicit length 2/4 annotates the directory revision counter for lockless access 3/4 compares namespace tags by pointer 4/4 removes kernfs_rwsem from the callback LOOKUP_RCU still returns -ECHILD. kernfs_iop_permission() forces every walk out of RCU-walk before children are revalidated, so lifting it here would have no effect until that path is fixed; left to a separate series. Readers walking cgroupfs and sysfs while another thread churns cgroups, renames netdevs and adds/removes devices, on an 8-CPU VM: kernfs_rwsem read contentions revalidate among acquisitions top call sites before 48,593,360 1,744,517 #1 and #2 after 1,280,280 429,846 absent Reader path-walk throughput improved 35-53% over the same workload. Tested against an unpatched control of the same tree, built and booted with KASAN, KCSAN (default and STRICT), PROVE_LOCKING, PROVE_RCU, DEBUG_ATOMIC_SLEEP and LOCK_STAT. The deactivated, renamed and namespace-moved reject paths and negative-dentry invalidation all behave as before. KCSAN_STRICT over 180s reports no data race involving kernfs_dop_revalidate() or any field it reads, and there are no KASAN, lockdep or might-sleep reports across millions of concurrent path walks. The only kernfs KCSAN reports are in kernfs_refresh_inode(), present identically on the control and addressed separately. Link: https://github.com/facebookincubator/below [1] * patches from https://patch.msgid.link/20260821050507.2161607-1-shakeel.butt@linux.dev: kernfs: Remove kernfs_rwsem from dentry revalidation kernfs: Avoid namespace dereference in d_revalidate() kernfs: Prepare directory revisions for lockless reads kernfs: Use VFS lookup context in d_revalidate() Link: https://patch.msgid.link/20260821050507.2161607-1-shakeel.butt@linux.dev Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2 parents cee9395 + 8c2a5e6 commit e626ea2

2 files changed

Lines changed: 35 additions & 50 deletions

File tree

fs/kernfs/dir.c

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,74 +1171,60 @@ struct kernfs_node *kernfs_create_empty_dir(struct kernfs_node *parent,
11711171
static int kernfs_dop_revalidate(struct inode *dir, const struct qstr *name,
11721172
struct dentry *dentry, unsigned int flags)
11731173
{
1174-
struct kernfs_node *kn, *parent;
1175-
struct kernfs_root *root;
1174+
struct kernfs_node *parent = dir->i_private;
1175+
struct kernfs_node *kn;
1176+
const char *kn_name;
11761177

11771178
if (flags & LOOKUP_RCU)
11781179
return -ECHILD;
11791180

11801181
/* Negative hashed dentry? */
11811182
if (d_really_is_negative(dentry)) {
1182-
/* If the kernfs parent node has changed discard and
1183-
* proceed to ->lookup.
1184-
*
1185-
* There's nothing special needed here when getting the
1186-
* dentry parent, even if a concurrent rename is in
1187-
* progress. That's because the dentry is negative so
1188-
* it can only be the target of the rename and it will
1189-
* be doing a d_move() not a replace. Consequently the
1190-
* dentry d_parent won't change over the d_move().
1183+
/*
1184+
* If the kernfs parent node has changed discard and proceed to
1185+
* ->lookup.
11911186
*
11921187
* Also kernfs negative dentries transitioning from
11931188
* negative to positive during revalidate won't happen
11941189
* because they are invalidated on containing directory
11951190
* changes and the lookup re-done so that a new positive
11961191
* dentry can be properly created.
11971192
*/
1198-
root = kernfs_root_from_sb(dentry->d_sb);
1199-
down_read(&root->kernfs_rwsem);
1200-
parent = kernfs_dentry_node(dentry->d_parent);
1201-
if (parent) {
1202-
if (kernfs_dir_changed(parent, dentry)) {
1203-
up_read(&root->kernfs_rwsem);
1204-
return 0;
1205-
}
1206-
}
1207-
up_read(&root->kernfs_rwsem);
1208-
1209-
/* The kernfs parent node hasn't changed, leave the
1210-
* dentry negative and return success.
1211-
*/
1212-
return 1;
1193+
return !kernfs_dir_changed(parent, dentry);
12131194
}
12141195

12151196
kn = kernfs_dentry_node(dentry);
1216-
root = kernfs_root(kn);
1217-
down_read(&root->kernfs_rwsem);
1197+
1198+
guard(rcu)();
12181199

12191200
/* The kernfs node has been deactivated */
1220-
if (!kernfs_active(kn))
1221-
goto out_bad;
1201+
if (!__kernfs_active(kn))
1202+
return 0;
12221203

1223-
parent = kernfs_parent(kn);
12241204
/* The kernfs node has been moved? */
1225-
if (kernfs_dentry_node(dentry->d_parent) != parent)
1226-
goto out_bad;
1205+
if (kernfs_parent(kn) != parent)
1206+
return 0;
12271207

12281208
/* The kernfs node has been renamed */
1229-
if (strcmp(dentry->d_name.name, kernfs_rcu_name(kn)) != 0)
1230-
goto out_bad;
1209+
kn_name = kernfs_rcu_name(kn);
1210+
if (name->len != strlen(kn_name) ||
1211+
memcmp(name->name, kn_name, name->len))
1212+
return 0;
12311213

1232-
/* The kernfs node has been moved to a different namespace */
1233-
if (parent && kernfs_ns_enabled(parent) &&
1234-
kernfs_ns_id(kernfs_info(dentry->d_sb)->ns) != kernfs_ns_id(kn->ns))
1235-
goto out_bad;
1214+
/*
1215+
* The kernfs node has been moved to a different namespace.
1216+
*
1217+
* KERNFS_NS is set by kernfs_enable_ns() while @parent still has no
1218+
* children, so it cannot change while a child of @parent is being
1219+
* revalidated. The other bits in that word, KERNFS_ACTIVATED and
1220+
* KERNFS_REMOVING, are updated under kernfs_rwsem and are not read
1221+
* here, so racing with them is intentional and harmless.
1222+
*/
1223+
if (data_race(kernfs_ns_enabled(parent)) &&
1224+
kernfs_info(dir->i_sb)->ns != READ_ONCE(kn->ns))
1225+
return 0;
12361226

1237-
up_read(&root->kernfs_rwsem);
12381227
return 1;
1239-
out_bad:
1240-
up_read(&root->kernfs_rwsem);
1241-
return 0;
12421228
}
12431229

12441230
const struct dentry_operations kernfs_dops = {
@@ -1878,15 +1864,15 @@ int kernfs_rename_ns(struct kernfs_node *kn, struct kernfs_node *new_parent,
18781864

18791865
rcu_assign_pointer(kn->__parent, new_parent);
18801866

1881-
kn->ns = new_ns;
1867+
WRITE_ONCE(kn->ns, new_ns);
18821868
if (new_name)
18831869
rcu_assign_pointer(kn->name, new_name);
18841870

18851871
write_unlock_irq(&root->kernfs_rename_lock);
18861872
kernfs_put(old_parent);
18871873
} else {
18881874
/* name assignment is RCU protected, parent is the same */
1889-
kn->ns = new_ns;
1875+
WRITE_ONCE(kn->ns, new_ns);
18901876
if (new_name)
18911877
rcu_assign_pointer(kn->name, new_name);
18921878
}

fs/kernfs/kernfs-internal.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,19 @@ static inline struct kernfs_node *kernfs_dentry_node(struct dentry *dentry)
147147
static inline void kernfs_set_rev(struct kernfs_node *parent,
148148
struct dentry *dentry)
149149
{
150-
dentry->d_time = parent->dir.rev;
150+
WRITE_ONCE(dentry->d_time, READ_ONCE(parent->dir.rev));
151151
}
152152

153153
static inline void kernfs_inc_rev(struct kernfs_node *parent)
154154
{
155-
parent->dir.rev++;
155+
lockdep_assert_held_write(&parent->dir.root->kernfs_rwsem);
156+
WRITE_ONCE(parent->dir.rev, parent->dir.rev + 1);
156157
}
157158

158159
static inline bool kernfs_dir_changed(struct kernfs_node *parent,
159160
struct dentry *dentry)
160161
{
161-
if (parent->dir.rev != dentry->d_time)
162-
return true;
163-
return false;
162+
return READ_ONCE(parent->dir.rev) != READ_ONCE(dentry->d_time);
164163
}
165164

166165
extern const struct super_operations kernfs_sops;

0 commit comments

Comments
 (0)