Linux driver-core infrastructure
 help / color / mirror / Atom feed
* [PATCH 0/3] kernfs, driver core: Cut per-node lock traffic in bulk registration
@ 2026-09-11 17:16 Pavol Sakac
  2026-09-11 17:16 ` [PATCH 1/3] kernfs: activate nodes while linking them Pavol Sakac
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:16 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
	Danilo Krummrich
  Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
	Bartosz Golaszewski, nh-open-source

Registering VFs in parallel, introduced in [1], runs into three lock
costs in kernfs and the driver core. Two scale with the number of
sysfs nodes created: the kernfs root rwsem is write-taken twice per
node, and every inode ID is allocated under kernfs_idr_lock. The third
scales with the number of devices: get_device_parent()'s glue
directory lookup walks a flat list under the global gdp_mutex.

The glue directory is how the driver core groups class devices under a
parent that is not itself a class device: when vfio-pci binds, each VF
gets a "vfio-dev" glue directory holding its vfio-dev class device.
get_device_parent() finds or creates it by walking the class's flat
glue_dirs list under gdp_mutex, so with thousands of VFs each
registration walks thousands of entries - quadratic work, serialized
on one global mutex.

Patch 1 (kernfs) activates a new node inside the same kernfs_rwsem
write hold that links it: one write acquisition per node instead of
two. Patch 2 (driver core) indexes glue directories by parent kobject
in an rbtree, replacing the linear walk; gdp_mutex still serializes as
before. Patch 3 (kernfs) pre-allocates inode IDs in batches of 16,
caches them per CPU, and installs the node pointer with an RCU store
via idr_replace(), cutting kernfs_idr_lock acquisitions 16x.

Lock statistics and SR-IOV init time for 4x PF (NVMe, 255 VFs each), on
the reproducer from the parallel VF initialization cover letter [1]:

  lock_stat:
  Lock                     wait: Before     After   contentions: Before   After
  iommu_probe_device_lock      10471 ms   9154 ms                   841     783
  &vfio.group_lock              3614 ms   3823 ms                   736     730
  &root->kernfs_rwsem           1834 ms   1285 ms                116316   55459
  &root->kernfs_idr_lock           5 ms      0 ms                  4580      77

  iommu_probe_device_lock and vfio.group_lock are shown for scale;
  neither is touched by this series - they are addressed by the
  IOMMU [2] and VFIO [3] series posted separately.

  kernfs_rwsem write acquisitions  168302 (2/node) -> 84661 (1/node)
  kernfs_idr_lock acquisitions     83641 -> 5229 (batch 16)
  gdp_mutex avg hold               18 -> 7 us (2040 acq. both arms)

  Stage                 SR-IOV init time:
  S0 (baseline)         3027 ms
  S1                     999 ms
  S2                     995 ms
  S3 (this series)       991 ms

Reproducer disclaimer:
I lean primarily on lock_stat numbers to defend the improvements. In
the reproducer, the residual iommu_probe_device_lock dominates the
window and masks the later series' wall-time gains; reducing that lock
further is out of scope for this set. On real hardware the five series
together cut SR-IOV initialization by 65%, more in [1].

The staged sysfs RFC [4] builds on top of these changes (mainly the
IDR batching).

This series adds a KUnit suite. The lock_stat and timing figures come
from the public reproducer. The full series has also been tested on
current datacenter server hardware with thousands of VFs.


[1] https://lore.kernel.org/r/20260911-vfopt-s1-v1-0-693271dc0226@amazon.de
[2] https://lore.kernel.org/r/20260911-vfopt-s2-v1-0-fff3db7e01c2@amazon.de
[3] https://lore.kernel.org/r/20260911-vfopt-s4-v1-0-98ba1d2ef7ab@amazon.de
[4] https://lore.kernel.org/r/20260911-vfopt-s5-v1-0-fa4cacdb6ca8@amazon.de

Pavol Sakac (3):
  kernfs: activate nodes while linking them
  driver core: Index class glue directories by parent kobject
  kernfs: batch inode ID allocation per CPU

 drivers/base/base.h                  |   4 +
 drivers/base/core.c                  | 112 +++++--
 drivers/base/test/.kunitconfig       |   1 +
 drivers/base/test/Kconfig            |  12 +
 drivers/base/test/Makefile           |   2 +
 drivers/base/test/glue-dir-test.c    | 466 +++++++++++++++++++++++++++
 drivers/base/test/root-device-test.c | 215 ++++++++++++
 fs/kernfs/dir.c                      | 139 +++++++-
 fs/kernfs/kernfs-internal.h          |   3 +
 fs/sysfs/mount.c                     |   3 +-
 include/linux/kernfs.h               |  13 +
 11 files changed, 930 insertions(+), 40 deletions(-)
 create mode 100644 drivers/base/test/glue-dir-test.c


base-commit: cee9395acd8043be0644b25c34bfa86623f2b935
-- 
2.47.3


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/3] kernfs: activate nodes while linking them
  2026-09-11 17:16 [PATCH 0/3] kernfs, driver core: Cut per-node lock traffic in bulk registration Pavol Sakac
@ 2026-09-11 17:16 ` Pavol Sakac
  2026-09-11 18:12   ` Andy Shevchenko
  2026-09-11 17:16 ` [PATCH 2/3] driver core: Index class glue directories by parent kobject Pavol Sakac
  2026-09-11 17:16 ` [PATCH 3/3] kernfs: batch inode ID allocation per CPU Pavol Sakac
  2 siblings, 1 reply; 6+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:16 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
	Danilo Krummrich
  Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
	Bartosz Golaszewski, nh-open-source

kernfs_add_one() links a new node, releases kernfs_rwsem, and takes it
again through kernfs_activate(). A new node has no descendants, so the
second hold only activates that node.

Activate ordinary nodes before releasing the linking hold, removing a
second write-side acquisition and the linked-but-inactive interval.
KERNFS_ROOT_CREATE_DEACTIVATED roots retain explicit subtree activation.

Removing the unlock-to-lock pair also removes its publication ordering.
The in-tree lockless ID lookups use CREATE_DEACTIVATED roots and retain
kernfs_activate(); unsynchronized callers cannot rely on observing a
node.

Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 fs/kernfs/dir.c | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
index 82bbaeb326aa..d68bce0b0b41 100644
--- a/fs/kernfs/dir.c
+++ b/fs/kernfs/dir.c
@@ -41,6 +41,8 @@ static bool kernfs_active(struct kernfs_node *kn)
 	return __kernfs_active(kn);
 }
 
+static void kernfs_activate_one(struct kernfs_node *kn);
+
 static bool kernfs_lockdep(struct kernfs_node *kn)
 {
 #ifdef CONFIG_DEBUG_LOCK_ALLOC
@@ -855,17 +857,16 @@ int kernfs_add_one(struct kernfs_node *kn)
 	}
 
 	up_write(&root->kernfs_iattr_rwsem);
-	up_write(&root->kernfs_rwsem);
 
 	/*
-	 * Activate the new node unless CREATE_DEACTIVATED is requested.
-	 * If not activated here, the kernfs user is responsible for
-	 * activating the node with kernfs_activate().  A node which hasn't
-	 * been activated is not visible to userland and its removal won't
-	 * trigger deactivation.
+	 * A freshly linked node has no descendants, so activating just @kn
+	 * under the linking hold is equivalent to the kernfs_activate()
+	 * walk; CREATE_DEACTIVATED roots keep deferred activation.
 	 */
-	if (!(kernfs_root(kn)->flags & KERNFS_ROOT_CREATE_DEACTIVATED))
-		kernfs_activate(kn);
+	if (!(root->flags & KERNFS_ROOT_CREATE_DEACTIVATED))
+		kernfs_activate_one(kn);
+
+	up_write(&root->kernfs_rwsem);
 	return 0;
 
 out_unlock:
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] driver core: Index class glue directories by parent kobject
  2026-09-11 17:16 [PATCH 0/3] kernfs, driver core: Cut per-node lock traffic in bulk registration Pavol Sakac
  2026-09-11 17:16 ` [PATCH 1/3] kernfs: activate nodes while linking them Pavol Sakac
@ 2026-09-11 17:16 ` Pavol Sakac
  2026-09-11 18:11   ` Andy Shevchenko
  2026-09-11 17:16 ` [PATCH 3/3] kernfs: batch inode ID allocation per CPU Pavol Sakac
  2 siblings, 1 reply; 6+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:16 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
	Danilo Krummrich
  Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
	Bartosz Golaszewski, nh-open-source

get_device_parent() finds a parent's glue directory by walking the
class's glue_dirs kset list under gdp_mutex, so one per parent, as
vfio-dev needs per SR-IOV VF, is quadratic.

Index them by parent kobject in an rbtree embedded in the class's
subsys_private, which dies with the kset list it indexes, so no
per-entry class check is needed: two classes below one parent are told
apart by tree selection. The key is a kobject because a parentless
class device hangs off the shared "virtual" kobject, referenced by the
glue dir while indexed. The rb_node lives in struct class_dir, adding
no allocation and no failure mode.

gdp_mutex serializes the index, as it has glue dir lookup/create/remove
since commit 77d3d7c1d561f
("driver-core: fix race condition in get_device_parent()") and
commit e4a60d1390609
("sysfs: driver core: Fix glue dir race condition by gdp_mutex").

A kernfs name lookup in the parent's directory needs no new state, but
takes the kernfs root rwsem under gdp_mutex, behind the writes
concurrent sysfs directory creation generates.

A KUnit suite covers the index: reuse of one parent's glue directory,
two classes below one parent, the parentless "virtual" cases, reap and
recreate, name collision, many parents, device_move(), and class
teardown.

Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/base.h               |   4 +
 drivers/base/core.c               | 112 +++++--
 drivers/base/test/.kunitconfig    |   1 +
 drivers/base/test/Kconfig         |  12 +
 drivers/base/test/Makefile        |   2 +
 drivers/base/test/glue-dir-test.c | 466 ++++++++++++++++++++++++++++++
 6 files changed, 566 insertions(+), 31 deletions(-)
 create mode 100644 drivers/base/test/glue-dir-test.c

diff --git a/drivers/base/base.h b/drivers/base/base.h
index a5b7abc10ff0..f5d608f4aaa5 100644
--- a/drivers/base/base.h
+++ b/drivers/base/base.h
@@ -11,6 +11,7 @@
  *
  */
 #include <linux/notifier.h>
+#include <linux/rbtree_types.h>
 
 /**
  * struct subsys_private - structure to hold the private to the driver core
@@ -32,6 +33,8 @@
  * @dev_root: Default device to use as the parent.
  * @glue_dirs: "glue" directory to put in-between the parent device to
  *	       avoid namespace conflicts
+ * @glue_dirs_index: the class's glue dirs by parent kobject, under gdp_mutex;
+ *		     zeroed is an empty rb_root
  * @class: pointer back to the struct class that this structure is associated
  *	   with.
  * @lock_key: Lock class key for use by the lock validator
@@ -55,6 +58,7 @@ struct subsys_private {
 	struct device *dev_root;
 
 	struct kset glue_dirs;
+	struct rb_root glue_dirs_index;
 	const struct class *class;
 
 	struct lock_class_key lock_key;
diff --git a/drivers/base/core.c b/drivers/base/core.c
index 4c0c373998a1..5dea641cbdb6 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -26,6 +26,7 @@
 #include <linux/of.h>
 #include <linux/of_device.h>
 #include <linux/pm_runtime.h>
+#include <linux/rbtree.h>
 #include <linux/sched/mm.h>
 #include <linux/sched/signal.h>
 #include <linux/slab.h>
@@ -3263,6 +3264,8 @@ struct kobject *virtual_device_parent(void)
 struct class_dir {
 	struct kobject kobj;
 	const struct class *class;
+	/* in the class's glue_dirs_index, keyed by kobj.parent (gdp_mutex) */
+	struct rb_node index_node;
 };
 
 #define to_class_dir(obj) container_of(obj, struct class_dir, kobj)
@@ -3298,6 +3301,7 @@ static struct kobject *class_dir_create_and_add(struct subsys_private *sp,
 
 	dir->class = sp->class;
 	kobject_init(&dir->kobj, &class_dir_ktype);
+	RB_CLEAR_NODE(&dir->index_node);
 
 	dir->kobj.kset = &sp->glue_dirs;
 
@@ -3311,6 +3315,66 @@ static struct kobject *class_dir_create_and_add(struct subsys_private *sp,
 
 static DEFINE_MUTEX(gdp_mutex);
 
+/*
+ * Glue-dir lookup index: each class's glue dirs by parent kobject, in an
+ * rbtree embedded in its subsys_private.  The glue_dirs kset remains the
+ * membership and identity authority (kobj->kset); the index replaces only
+ * the list walk in get_device_parent() and dies with the subsys_private
+ * generation that owns both.
+ */
+static int glue_dir_cmp_key(const void *key, const struct rb_node *node)
+{
+	const struct class_dir *cd = rb_entry(node, struct class_dir,
+					      index_node);
+
+	if ((unsigned long)key < (unsigned long)cd->kobj.parent)
+		return -1;
+	return (unsigned long)key > (unsigned long)cd->kobj.parent;
+}
+
+static bool glue_dir_less(struct rb_node *lhs, const struct rb_node *rhs)
+{
+	const struct class_dir *cd = rb_entry(lhs, struct class_dir,
+					      index_node);
+
+	return glue_dir_cmp_key(cd->kobj.parent, rhs) < 0;
+}
+
+static struct kobject *glue_dir_lookup(struct subsys_private *sp,
+				       struct kobject *parent_kobj)
+{
+	struct rb_node *node;
+
+	lockdep_assert_held(&gdp_mutex);
+
+	node = rb_find(parent_kobj, &sp->glue_dirs_index, glue_dir_cmp_key);
+	if (!node)
+		return NULL;
+
+	return kobject_get(&rb_entry(node, struct class_dir, index_node)->kobj);
+}
+
+static void glue_dir_index(struct subsys_private *sp, struct kobject *glue_dir)
+{
+	lockdep_assert_held(&gdp_mutex);
+
+	/* Lookup-before-create under gdp_mutex keeps keys unique. */
+	rb_add(&to_class_dir(glue_dir)->index_node, &sp->glue_dirs_index,
+	       glue_dir_less);
+}
+
+/*
+ * rb_erase() uses only recorded tree links, but glue_dir_less() keys on
+ * kobj.parent: the dir, indexed at create, leaves the index before
+ * kobject_del() clears its parent -- an indexed node's key must never mutate.
+ */
+static void glue_dir_unindex(struct subsys_private *sp,
+			     struct kobject *glue_dir)
+{
+	lockdep_assert_held(&gdp_mutex);
+	rb_erase(&to_class_dir(glue_dir)->index_node, &sp->glue_dirs_index);
+}
+
 static struct kobject *get_device_parent(struct device *dev,
 					 struct device *parent)
 {
@@ -3338,13 +3402,7 @@ static struct kobject *get_device_parent(struct device *dev,
 		mutex_lock(&gdp_mutex);
 
 		/* find our class-directory at the parent and reference it */
-		spin_lock(&sp->glue_dirs.list_lock);
-		list_for_each_entry(k, &sp->glue_dirs.list, entry)
-			if (k->parent == parent_kobj) {
-				kobj = kobject_get(k);
-				break;
-			}
-		spin_unlock(&sp->glue_dirs.list_lock);
+		kobj = glue_dir_lookup(sp, parent_kobj);
 		if (kobj) {
 			mutex_unlock(&gdp_mutex);
 			subsys_put(sp);
@@ -3354,6 +3412,8 @@ static struct kobject *get_device_parent(struct device *dev,
 		/* or create a new class-directory at the parent device */
 		k = class_dir_create_and_add(sp, parent_kobj);
 		/* do not emit an uevent for this simple "glue" directory */
+		if (!IS_ERR(k))
+			glue_dir_index(sp, k);
 		mutex_unlock(&gdp_mutex);
 		subsys_put(sp);
 		return k;
@@ -3375,28 +3435,6 @@ static struct kobject *get_device_parent(struct device *dev,
 	return NULL;
 }
 
-static inline bool live_in_glue_dir(struct kobject *kobj,
-				    struct device *dev)
-{
-	struct subsys_private *sp;
-	bool retval;
-
-	if (!kobj || !dev->class)
-		return false;
-
-	sp = class_to_subsys(dev->class);
-	if (!sp)
-		return false;
-
-	if (kobj->kset == &sp->glue_dirs)
-		retval = true;
-	else
-		retval = false;
-
-	subsys_put(sp);
-	return retval;
-}
-
 static inline struct kobject *get_glue_dir(struct device *dev)
 {
 	return dev->kobj.parent;
@@ -3426,11 +3464,19 @@ static inline bool kobject_has_children(struct kobject *kobj)
  */
 static void cleanup_glue_dir(struct device *dev, struct kobject *glue_dir)
 {
+	struct subsys_private *sp;
 	unsigned int ref;
 
 	/* see if we live in a "glue" directory */
-	if (!live_in_glue_dir(glue_dir, dev))
+	if (!glue_dir || !dev->class)
+		return;
+	sp = class_to_subsys(dev->class);
+	if (!sp)
+		return;
+	if (glue_dir->kset != &sp->glue_dirs) {
+		subsys_put(sp);
 		return;
+	}
 
 	mutex_lock(&gdp_mutex);
 	/**
@@ -3482,10 +3528,14 @@ static void cleanup_glue_dir(struct device *dev, struct kobject *glue_dir)
 	 * for glue_dir kobj is 1.
 	 */
 	ref = kref_read(&glue_dir->kref);
-	if (!kobject_has_children(glue_dir) && !--ref)
+	if (!kobject_has_children(glue_dir) && !--ref) {
+		glue_dir_unindex(sp, glue_dir);
 		kobject_del(glue_dir);
+	}
 	kobject_put(glue_dir);
 	mutex_unlock(&gdp_mutex);
+	/* outside gdp_mutex: the last put runs the class's release callback */
+	subsys_put(sp);
 }
 
 static int device_add_class_symlinks(struct device *dev)
diff --git a/drivers/base/test/.kunitconfig b/drivers/base/test/.kunitconfig
index 473923f0998b..28322bad39a7 100644
--- a/drivers/base/test/.kunitconfig
+++ b/drivers/base/test/.kunitconfig
@@ -1,2 +1,3 @@
 CONFIG_KUNIT=y
 CONFIG_DM_KUNIT_TEST=y
+CONFIG_GLUE_DIR_KUNIT_TEST=y
diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 542ce07530a1..253b5bd96aff 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -24,3 +24,15 @@ config DRIVER_SWNODE_KUNIT_TEST
 	tristate "KUnit Tests for software node fw_devlink links" if !KUNIT_ALL_TESTS
 	depends on KUNIT
 	default KUNIT_ALL_TESTS
+
+config GLUE_DIR_KUNIT_TEST
+	tristate "KUnit Tests for class glue directories" if !KUNIT_ALL_TESTS
+	depends on KUNIT && SYSFS
+	default KUNIT_ALL_TESTS
+	help
+	  Enable this option to test the class glue directories the driver
+	  core places class devices under: that siblings share one, that a
+	  reaped one is created again, and that a same-named child which is
+	  not a glue directory is never mistaken for one.
+
+	  If unsure say N.
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index 9ced7bbd569f..f13f0c399bea 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -8,3 +8,5 @@ obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) += property-entry-test.o
 CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
 
 obj-$(CONFIG_DRIVER_SWNODE_KUNIT_TEST) += swnode-devlink-test.o
+
+obj-$(CONFIG_GLUE_DIR_KUNIT_TEST) += glue-dir-test.o
diff --git a/drivers/base/test/glue-dir-test.c b/drivers/base/test/glue-dir-test.c
new file mode 100644
index 000000000000..fcdfdd3acd6f
--- /dev/null
+++ b/drivers/base/test/glue-dir-test.c
@@ -0,0 +1,466 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the class glue-directory index: a class device whose
+ * parent is not itself a class device is placed under a per-parent "glue"
+ * directory named after the class, which get_device_parent() finds
+ * through a per-class index keyed by the parent kobject.  Every case
+ * drives that lookup through plain device registration.
+ */
+
+#include <kunit/resource.h>
+#include <kunit/test.h>
+
+#include <linux/device.h>
+#include <linux/kernfs.h>
+#include <linux/kobject.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+
+static void glue_dev_release(struct device *dev)
+{
+	kfree(dev);
+}
+
+static void glue_dev_unregister(void *data)
+{
+	device_unregister(data);
+}
+
+static void glue_root_unregister(void *data)
+{
+	root_device_unregister(data);
+}
+
+static void glue_class_destroy(void *data)
+{
+	class_destroy(data);
+}
+
+/* Does a child named @name exist under @parent? */
+static bool glue_child_visible(struct kobject *parent, const char *name)
+{
+	struct kernfs_node *kn = kernfs_find_and_get(parent->sd, name);
+	bool found = kn;
+
+	kernfs_put(kn);
+	return found;
+}
+
+/* Freed by glue_dev_release() when the last reference drops. */
+static struct device *glue_dev_alloc(struct kunit *test,
+				     struct device *parent,
+				     const struct class *class,
+				     const char *name)
+{
+	struct device *dev;
+	int ret;
+
+	dev = kzalloc_obj(*dev);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+
+	device_initialize(dev);
+	dev->parent = parent;
+	dev->class = class;
+	dev->release = glue_dev_release;
+
+	ret = dev_set_name(dev, "%s", name);
+	if (ret)
+		put_device(dev);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	return dev;
+}
+
+/* Register a class device; unregistered again by a deferred kunit action. */
+static struct device *glue_dev_add(struct kunit *test, struct device *parent,
+				   const struct class *class,
+				   const char *name)
+{
+	struct device *dev = glue_dev_alloc(test, parent, class, name);
+	int ret;
+
+	ret = device_add(dev);
+	if (ret)
+		put_device(dev);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+							glue_dev_unregister,
+							dev), 0);
+	return dev;
+}
+
+static struct class *glue_class(struct kunit *test, const char *name)
+{
+	struct class *class = class_create(name);
+
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, class);
+	KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+							glue_class_destroy,
+							class), 0);
+	return class;
+}
+
+static struct device *glue_root(struct kunit *test, const char *name)
+{
+	struct device *root = root_device_register(name);
+
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, root);
+	KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+							glue_root_unregister,
+							root), 0);
+	return root;
+}
+
+/* Siblings share one glue dir: the second lookup has to hit the first. */
+static void glue_test_reuse(struct kunit *test)
+{
+	struct device *root, *dev_a, *dev_b;
+	struct kobject *glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_reuse");
+	root = glue_root(test, "glue_kunit_reuse_root");
+
+	dev_a = glue_dev_add(test, root, class, "reuseA");
+	dev_b = glue_dev_add(test, root, class, "reuseB");
+
+	glue = dev_a->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue);
+	KUNIT_EXPECT_PTR_NE(test, glue, &root->kobj);
+	KUNIT_EXPECT_PTR_EQ(test, glue, dev_b->kobj.parent);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(glue, "reuseA"));
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(glue, "reuseB"));
+}
+
+/* Two classes below one parent: each consults only its own class's index. */
+static void glue_test_cross_class(struct kunit *test)
+{
+	struct class *class_a, *class_b;
+	struct device *root, *dev_a, *dev_b;
+
+	class_a = glue_class(test, "glue_kunit_xclass_a");
+	class_b = glue_class(test, "glue_kunit_xclass_b");
+	root = glue_root(test, "glue_kunit_xclass_root");
+
+	dev_a = glue_dev_add(test, root, class_a, "xclassA");
+	dev_b = glue_dev_add(test, root, class_b, "xclassB");
+
+	KUNIT_ASSERT_NOT_NULL(test, dev_a->kobj.parent);
+	KUNIT_EXPECT_PTR_NE(test, dev_a->kobj.parent, dev_b->kobj.parent);
+}
+
+/* Reap retires the index entry: the next add must not find the freed dir. */
+static void glue_test_reap_recreate(struct kunit *test)
+{
+	struct device *root, *dev;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_reap");
+	root = glue_root(test, "glue_kunit_reap_root");
+
+	dev = glue_dev_add(test, root, class, "reap0");
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(&root->kobj,
+						   "glue_kunit_reap"));
+
+	/* last child gone: the glue dir goes with it ... */
+	kunit_release_action(test, glue_dev_unregister, dev);
+	KUNIT_EXPECT_FALSE(test, glue_child_visible(&root->kobj,
+						    "glue_kunit_reap"));
+
+	/* ... and a further registration recreates it */
+	dev = glue_dev_add(test, root, class, "reap1");
+	KUNIT_ASSERT_NOT_NULL(test, dev->kobj.parent);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(dev->kobj.parent, "reap1"));
+}
+
+static ssize_t glue_kunit_collide_show(struct device *dev,
+				       struct device_attribute *attr,
+				       char *buf)
+{
+	return sysfs_emit(buf, "\n");
+}
+static DEVICE_ATTR_RO(glue_kunit_collide);
+
+/*
+ * A same-named child that is not a glue dir must never be taken for one:
+ * the colliding add fails on the duplicate sysfs create and leaves
+ * nothing stale, so the same registration succeeds once the file is gone.
+ */
+static void glue_test_name_collision(struct kunit *test)
+{
+	struct device *root, *dev;
+	struct kernfs_node *kn;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_collide");
+	root = glue_root(test, "glue_kunit_collide_root");
+
+	KUNIT_ASSERT_EQ(test,
+			device_create_file(root,
+					   &dev_attr_glue_kunit_collide), 0);
+
+	dev = glue_dev_alloc(test, root, class, "collide0");
+	KUNIT_ASSERT_EQ(test, device_add(dev), -EEXIST);
+	put_device(dev);
+
+	/* the same-named child is still the attribute file */
+	kn = kernfs_find_and_get(root->kobj.sd, "glue_kunit_collide");
+	KUNIT_ASSERT_NOT_NULL(test, kn);
+	KUNIT_EXPECT_EQ(test, kernfs_type(kn), KERNFS_FILE);
+	kernfs_put(kn);
+
+	device_remove_file(root, &dev_attr_glue_kunit_collide);
+	dev = glue_dev_add(test, root, class, "collide0");
+
+	/* and it lands in a glue dir of the class's name, not on the root */
+	KUNIT_ASSERT_NOT_NULL(test, dev->kobj.parent);
+	KUNIT_EXPECT_PTR_NE(test, dev->kobj.parent, &root->kobj);
+	KUNIT_EXPECT_STREQ(test, kobject_name(dev->kobj.parent), class->name);
+	KUNIT_EXPECT_PTR_EQ(test, dev->kobj.parent->parent, &root->kobj);
+}
+
+/*
+ * A parentless class device is keyed on the shared "virtual" kobject
+ * rather than on a device, so two of them still share one glue dir.
+ */
+static void glue_test_virtual_parent(struct kunit *test)
+{
+	struct device *dev_a, *dev_b;
+	struct kobject *glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_virtual");
+
+	dev_a = glue_dev_add(test, NULL, class, "virtA");
+	dev_b = glue_dev_add(test, NULL, class, "virtB");
+
+	glue = dev_a->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue);
+	KUNIT_EXPECT_PTR_EQ(test, glue, dev_b->kobj.parent);
+	KUNIT_ASSERT_NOT_NULL(test, glue->parent);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue->parent), "virtual");
+}
+
+/*
+ * Every class's parentless devices share the one "virtual" key; two
+ * classes below it must still land in dirs of their own class's name.
+ */
+static void glue_test_virtual_cross_class(struct kunit *test)
+{
+	struct class *class_a, *class_b;
+	struct device *dev_a, *dev_b;
+	struct kobject *glue_a, *glue_b;
+
+	class_a = glue_class(test, "glue_kunit_virtxc_a");
+	class_b = glue_class(test, "glue_kunit_virtxc_b");
+
+	dev_a = glue_dev_add(test, NULL, class_a, "virtxcA");
+	dev_b = glue_dev_add(test, NULL, class_b, "virtxcB");
+
+	glue_a = dev_a->kobj.parent;
+	glue_b = dev_b->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue_a);
+	KUNIT_ASSERT_NOT_NULL(test, glue_b);
+
+	KUNIT_EXPECT_PTR_NE(test, glue_a, glue_b);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue_a), class_a->name);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue_b), class_b->name);
+
+	/* both dirs hang off the one shared key */
+	KUNIT_ASSERT_NOT_NULL(test, glue_a->parent);
+	KUNIT_EXPECT_PTR_EQ(test, glue_a->parent, glue_b->parent);
+	KUNIT_EXPECT_STREQ(test, kobject_name(glue_a->parent), "virtual");
+}
+
+#define GLUE_MANY_PARENTS 256
+
+/*
+ * With hundreds of parents indexed, every lookup must resolve the dir
+ * anchored at its own parent -- the case that falsifies the key comparison.
+ */
+static void glue_test_many_parents(struct kunit *test)
+{
+	struct device **roots, **devs;
+	struct class *class;
+	char name[32];
+	int i;
+
+	class = glue_class(test, "glue_kunit_many");
+
+	roots = kunit_kcalloc(test, GLUE_MANY_PARENTS, sizeof(*roots),
+			      GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, roots);
+	devs = kunit_kcalloc(test, GLUE_MANY_PARENTS, sizeof(*devs),
+			     GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, devs);
+
+	for (i = 0; i < GLUE_MANY_PARENTS; i++) {
+		snprintf(name, sizeof(name), "glue_kunit_many_root%d", i);
+		roots[i] = glue_root(test, name);
+		snprintf(name, sizeof(name), "many%d", i);
+		devs[i] = glue_dev_add(test, roots[i], class, name);
+	}
+
+	/* Distinctness via anchoring: a kobject has one parent. */
+	for (i = 0; i < GLUE_MANY_PARENTS; i++) {
+		struct kobject *glue = devs[i]->kobj.parent;
+
+		KUNIT_ASSERT_NOT_NULL(test, glue);
+		KUNIT_EXPECT_PTR_EQ(test, glue->parent, &roots[i]->kobj);
+	}
+}
+
+/*
+ * device_move() looks up against the new parent and must leave the old
+ * parent's entry behind as a valid hit: the old glue dir is never reaped
+ * (base behaviour), so a later device under the old parent reuses it.
+ */
+static void glue_test_device_move(struct kunit *test)
+{
+	struct device *root_a, *root_b, *dev, *dev_a2;
+	struct kobject *old_glue, *new_glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_move");
+	root_a = glue_root(test, "glue_kunit_move_rootA");
+	root_b = glue_root(test, "glue_kunit_move_rootB");
+
+	dev = glue_dev_add(test, root_a, class, "move0");
+	old_glue = dev->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, old_glue);
+
+	KUNIT_ASSERT_EQ(test, device_move(dev, root_b, DPM_ORDER_NONE), 0);
+
+	new_glue = dev->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, new_glue);
+	KUNIT_EXPECT_PTR_NE(test, new_glue, old_glue);
+	KUNIT_EXPECT_PTR_EQ(test, new_glue->parent, &root_b->kobj);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(new_glue, "move0"));
+
+	/* nothing reaps the old dir on the success path: it is still there */
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(&root_a->kobj,
+						   "glue_kunit_move"));
+
+	/* and it must still be the hit for the old parent */
+	dev_a2 = glue_dev_add(test, root_a, class, "move1");
+	KUNIT_EXPECT_PTR_EQ(test, dev_a2->kobj.parent, old_glue);
+}
+
+/*
+ * Removing one of two siblings must not retire the shared glue dir:
+ * the survivor keeps it visible, and a further sibling still reuses
+ * it -- a spurious unindex would fail that add on a duplicate create.
+ */
+static void glue_test_no_reap(struct kunit *test)
+{
+	struct device *root, *dev_a, *dev_b, *dev_c;
+	struct kobject *glue;
+	struct class *class;
+
+	class = glue_class(test, "glue_kunit_noreap");
+	root = glue_root(test, "glue_kunit_noreap_root");
+
+	dev_a = glue_dev_add(test, root, class, "noreapA");
+	dev_b = glue_dev_add(test, root, class, "noreapB");
+	glue = dev_b->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue);
+
+	kunit_release_action(test, glue_dev_unregister, dev_a);
+
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(&root->kobj,
+						   "glue_kunit_noreap"));
+	KUNIT_EXPECT_PTR_EQ(test, dev_b->kobj.parent, glue);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(glue, "noreapB"));
+
+	dev_c = glue_dev_add(test, root, class, "noreapC");
+	KUNIT_EXPECT_PTR_EQ(test, dev_c->kobj.parent, glue);
+}
+
+/*
+ * Static so that unregistering under a live device frees only the
+ * driver-core generation, not the struct class the device points at.
+ */
+static const struct class glue_gone_class_a = {
+	.name = "glue_kunit_gone_a",
+};
+
+static const struct class glue_gone_class_b = {
+	.name = "glue_kunit_gone_b",
+};
+
+static void glue_static_class_unregister(void *data)
+{
+	class_unregister(data);
+}
+
+/*
+ * Unregistering a class under a live device is tolerated API misuse: the
+ * device's glue dir can no longer be reaped and is deliberately leaked
+ * (class_dir, kernfs node, and parent reference), here as in the base
+ * tree; kmemleak reports all three on every run.
+ *
+ * The case pins the index's placement: a dead generation's tree dies
+ * with its subsys_private, so the successor class starts on an empty
+ * tree and cannot resolve the leaked dir.  A global index would put the
+ * dead entry back in its search path.
+ */
+static void glue_test_class_gone(struct kunit *test)
+{
+	struct device *root, *dev_a, *dev_b;
+	struct kobject *glue_a;
+
+	KUNIT_ASSERT_EQ(test, class_register(&glue_gone_class_a), 0);
+	KUNIT_ASSERT_EQ(test,
+			kunit_add_action_or_reset(test,
+						  glue_static_class_unregister,
+						  (void *)&glue_gone_class_a),
+			0);
+	root = glue_root(test, "glue_kunit_gone_root");
+
+	dev_a = glue_dev_add(test, root, &glue_gone_class_a, "goneA");
+	glue_a = dev_a->kobj.parent;
+	KUNIT_ASSERT_NOT_NULL(test, glue_a);
+
+	/* the misuse: the class goes away under a live device */
+	kunit_release_action(test, glue_static_class_unregister,
+			     (void *)&glue_gone_class_a);
+
+	KUNIT_ASSERT_EQ(test, class_register(&glue_gone_class_b), 0);
+	KUNIT_ASSERT_EQ(test,
+			kunit_add_action_or_reset(test,
+						  glue_static_class_unregister,
+						  (void *)&glue_gone_class_b),
+			0);
+
+	dev_b = glue_dev_add(test, root, &glue_gone_class_b, "goneB");
+	KUNIT_ASSERT_NOT_NULL(test, dev_b->kobj.parent);
+	KUNIT_EXPECT_PTR_NE(test, dev_b->kobj.parent, glue_a);
+	KUNIT_EXPECT_PTR_NE(test, dev_b->kobj.parent, &root->kobj);
+	KUNIT_EXPECT_STREQ(test, kobject_name(dev_b->kobj.parent),
+			   glue_gone_class_b.name);
+	KUNIT_EXPECT_TRUE(test, glue_child_visible(dev_b->kobj.parent,
+						   "goneB"));
+}
+
+static struct kunit_case glue_dir_tests[] = {
+	KUNIT_CASE(glue_test_reuse),
+	KUNIT_CASE(glue_test_cross_class),
+	KUNIT_CASE(glue_test_reap_recreate),
+	KUNIT_CASE(glue_test_name_collision),
+	KUNIT_CASE(glue_test_virtual_parent),
+	KUNIT_CASE(glue_test_virtual_cross_class),
+	KUNIT_CASE(glue_test_many_parents),
+	KUNIT_CASE(glue_test_device_move),
+	KUNIT_CASE(glue_test_no_reap),
+	KUNIT_CASE(glue_test_class_gone),
+	{}
+};
+
+static struct kunit_suite glue_dir_test_suite = {
+	.name = "glue_dir",
+	.test_cases = glue_dir_tests,
+};
+
+kunit_test_suite(glue_dir_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for class glue directories");
+MODULE_LICENSE("GPL");
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/3] kernfs: batch inode ID allocation per CPU
  2026-09-11 17:16 [PATCH 0/3] kernfs, driver core: Cut per-node lock traffic in bulk registration Pavol Sakac
  2026-09-11 17:16 ` [PATCH 1/3] kernfs: activate nodes while linking them Pavol Sakac
  2026-09-11 17:16 ` [PATCH 2/3] driver core: Index class glue directories by parent kobject Pavol Sakac
@ 2026-09-11 17:16 ` Pavol Sakac
  2 siblings, 0 replies; 6+ messages in thread
From: Pavol Sakac @ 2026-09-11 17:16 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
	Danilo Krummrich
  Cc: driver-core, linux-kernel, Andy Shevchenko, Xu Yang,
	Bartosz Golaszewski, nh-open-source

Every kernfs node allocates an inode ID from its root's IDR under the
per-root kernfs_idr_lock (per-root since commit cec59c440a05c ("kernfs:
switch global kernfs_idr_lock to per-fs lock")). sysfs is a single root,
so parallel creation funnels every node through one lock, and a
registration storm acquires it once per created node: even
registrations in disjoint subtrees contend on that one spinlock.

Let opted-in roots reserve a batch of IDs as NULL IDR entries and cache
them per CPU, so node creation pops one and installs itself with
idr_replace() under rcu_read_lock() instead of taking the shared lock.

Each call stays within the documented IDR contract: idr_replace() may run
under the RCU read lock concurrently with the idr_alloc() and
idr_remove() other CPUs do under the lock, and installing into an entry
reserved as NULL is the reserve-then-replace pattern of
Documentation/core-api/idr.rst. The property relied on beyond that is
concurrent idr_replace() on distinct reserved IDs, the normal mode here
since each reserved ID goes to exactly one caller; it holds because
replacing a reserved slot (NULL, IDR_FREE clear) is a single
rcu_assign_pointer() store and nothing else, and calculate_count()
returns 0 for such a slot, so the transition is count-neutral, writes no
shared radix-tree state, and cannot interact with a concurrent replace of
a distinct slot even within one radix-tree node.

Refill failure falls back to the locked path. An opted-in root must not
also ask for KERNFS_ROOT_SUPPORT_EXPORTOP, whose fhandle by-id lookups
must not meet an ino that is allocated but owned by no node. Enable
batching for sysfs: with batching, a registration storm takes
kernfs_idr_lock once per KERNFS_INO_BATCH-sized batch of nodes
instead of once per node.

Add root_device_ino_uniqueness_test to the root-device-devm KUnit
suite: it registers devices from several tasks at once, each crossing a
batch refill, and requires the IDs of nodes that are all alive at the
same time to be distinct.

Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@amazon.de>
---
 drivers/base/test/root-device-test.c | 215 +++++++++++++++++++++++++++
 fs/kernfs/dir.c                      | 122 +++++++++++++++
 fs/kernfs/kernfs-internal.h          |   3 +
 fs/sysfs/mount.c                     |   3 +-
 include/linux/kernfs.h               |  13 ++
 5 files changed, 355 insertions(+), 1 deletion(-)

diff --git a/drivers/base/test/root-device-test.c b/drivers/base/test/root-device-test.c
index 9aea23c9123e..f3370d53f9d4 100644
--- a/drivers/base/test/root-device-test.c
+++ b/drivers/base/test/root-device-test.c
@@ -3,7 +3,12 @@
 
 #include <kunit/resource.h>
 
+#include <linux/completion.h>
+#include <linux/cpumask.h>
 #include <linux/device.h>
+#include <linux/kernfs.h>
+#include <linux/kthread.h>
+#include <linux/sort.h>
 
 #define DEVICE_NAME "test"
 
@@ -93,9 +98,219 @@ static void root_device_devm_register_get_unregister_with_devm_test(struct kunit
 	KUNIT_EXPECT_GT(test, ret, 0);
 }
 
+#if IS_ENABLED(CONFIG_SYSFS)
+/*
+ * Inode IDs may be handed out from per-CPU batches (KERNFS_ROOT_INO_BATCH on
+ * the sysfs root); one task's loop only draws from the CPU it runs on and
+ * would pass unchanged if batching were inert.  Hence several tasks at
+ * once.  Per task: more than INO_BATCH IDs for every online CPU, so a
+ * refill is crossed even if a task migrates across all of them, and never
+ * a whole number of batches, so the run also ends mid-batch.
+ */
+#define INO_THREADS		4
+#define INO_BATCH		16	/* KERNFS_INO_BATCH (kernfs-private) */
+#define INO_MIN_PER_THREAD	200
+/* one worker's registrations scale with the CPU count: ~0.5ms each on 4 CPUs */
+#define INO_TIMEOUT_BASE_MS	5000
+#define INO_TIMEOUT_PER_DEV_MS	10
+
+struct ino_worker {
+	int idx;
+	int nr;				/* devices to register */
+	int created;			/* devices actually registered */
+	int err;			/* first registration error */
+	bool joined;			/* completion was waited out */
+	struct device **devs;
+	ino_t *inos;
+	struct completion done;
+};
+
+struct ino_test_ctx {
+	struct ino_worker worker[INO_THREADS];
+	int spawned;
+};
+
+static int root_device_ino_worker(void *data)
+{
+	struct ino_worker *w = data;
+	int i;
+
+	for (i = 0; i < w->nr; i++) {
+		char name[32];
+		struct device *dev;
+
+		snprintf(name, sizeof(name), DEVICE_NAME "-ino-%d-%d",
+			 w->idx, i);
+		dev = root_device_register(name);
+		if (IS_ERR(dev)) {
+			w->err = PTR_ERR(dev);
+			break;
+		}
+		w->devs[i] = dev;
+		w->inos[i] = dev->kobj.sd ? kernfs_ino(dev->kobj.sd) : 0;
+		w->created = i + 1;
+	}
+
+	/*
+	 * complete_all(), not complete(): this is waited on twice, once by
+	 * the test and once by the teardown action, and a plain completion
+	 * is consumed by the first waiter.
+	 */
+	complete_all(&w->done);
+	return 0;
+}
+
+/*
+ * Registered before the first worker is spawned, so it also runs if the test
+ * aborts: join every worker, then undo what it did.
+ */
+static void root_device_ino_teardown(void *data)
+{
+	struct ino_test_ctx *ctx = data;
+	int t, i;
+
+	for (t = 0; t < ctx->spawned; t++)
+		wait_for_completion(&ctx->worker[t].done);
+
+	for (t = 0; t < INO_THREADS; t++)
+		for (i = 0; i < ctx->worker[t].created; i++)
+			root_device_unregister(ctx->worker[t].devs[i]);
+}
+
+static int ino_cmp(const void *a, const void *b)
+{
+	ino_t x = *(const ino_t *)a;
+	ino_t y = *(const ino_t *)b;
+
+	if (x < y)
+		return -1;
+	return x > y;
+}
+
+static void root_device_ino_uniqueness_test(struct kunit *test)
+{
+	int zeros = 0, dups = 0, descents = 0, ids = 0;
+	struct ino_test_ctx *ctx;
+	bool results_valid = true;
+	unsigned long timeout;
+	struct device **devs;
+	ino_t *inos, *sorted;
+	int nr, total, t, i;
+
+	nr = INO_BATCH * num_online_cpus() + INO_BATCH / 2;
+	if (nr < INO_MIN_PER_THREAD)
+		nr = INO_MIN_PER_THREAD;
+	total = INO_THREADS * nr;
+	timeout = msecs_to_jiffies(INO_TIMEOUT_BASE_MS +
+				   nr * INO_TIMEOUT_PER_DEV_MS);
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	devs = kunit_kcalloc(test, total, sizeof(*devs), GFP_KERNEL);
+	inos = kunit_kcalloc(test, total, sizeof(*inos), GFP_KERNEL);
+	sorted = kunit_kcalloc(test, total, sizeof(*sorted), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, devs);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, inos);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sorted);
+
+	for (t = 0; t < INO_THREADS; t++) {
+		struct ino_worker *w = &ctx->worker[t];
+
+		w->idx = t;
+		w->nr = nr;
+		w->devs = devs + t * nr;
+		w->inos = inos + t * nr;
+		init_completion(&w->done);
+	}
+
+	KUNIT_ASSERT_EQ(test,
+			kunit_add_action_or_reset(test,
+						  root_device_ino_teardown,
+						  ctx), 0);
+
+	for (t = 0; t < INO_THREADS; t++) {
+		struct task_struct *task;
+
+		task = kthread_run(root_device_ino_worker, &ctx->worker[t],
+				   "root_dev_ino%d", t);
+		if (IS_ERR(task))
+			break;
+		ctx->spawned++;
+	}
+	KUNIT_EXPECT_EQ(test, ctx->spawned, INO_THREADS);
+	if (ctx->spawned != INO_THREADS)
+		results_valid = false;
+
+	/*
+	 * complete_all() is a worker's last act, so only a join that succeeds
+	 * orders its stores before the reads below; a worker that timed out
+	 * may still be writing its slice.  The teardown action joins those
+	 * unbounded, before any of this memory can be freed.
+	 */
+	for (t = 0; t < ctx->spawned; t++) {
+		struct ino_worker *w = &ctx->worker[t];
+
+		if (wait_for_completion_timeout(&w->done, timeout) > 0) {
+			w->joined = true;
+		} else {
+			KUNIT_FAIL(test, "worker %d timed out", t);
+			results_valid = false;
+		}
+	}
+
+	for (t = 0; t < ctx->spawned; t++) {
+		struct ino_worker *w = &ctx->worker[t];
+
+		if (!w->joined)
+			continue;
+
+		KUNIT_EXPECT_EQ(test, w->err, 0);
+		KUNIT_EXPECT_EQ(test, w->created, nr);
+		if (w->err || w->created != nr)
+			results_valid = false;
+
+		for (i = 0; i < w->created; i++) {
+			if (!w->inos[i])
+				zeros++;
+			/*
+			 * Batches are consumed from the top: IDs step down
+			 * within one, while the locked path only ascends, so
+			 * one descent is evidence the cache was in use.
+			 */
+			if (i && w->inos[i] < w->inos[i - 1])
+				descents++;
+			sorted[ids++] = w->inos[i];
+		}
+	}
+	KUNIT_EXPECT_EQ(test, zeros, 0);
+
+	/* every ID belongs to a node that is still alive here */
+	sort(sorted, ids, sizeof(*sorted), ino_cmp, NULL);
+	for (i = 1; i < ids; i++)
+		if (sorted[i] == sorted[i - 1])
+			dups++;
+	KUNIT_EXPECT_EQ(test, dups, 0);
+	if (zeros || dups)
+		results_valid = false;
+
+	/*
+	 * The batch cache is best-effort: when its allocation failed, every
+	 * ID above came from the locked fallback and no descent can have been
+	 * observed.  Make that visibly unavailable coverage rather than a
+	 * PASS that never exercised batching -- but only when everything
+	 * above held, since kunit_skip() would overwrite a recorded failure.
+	 */
+	if (IS_ENABLED(CONFIG_SMP) && results_valid && !descents)
+		kunit_skip(test, "sysfs inode batching fast path was not observed");
+}
+#endif
+
 static struct kunit_case root_device_devm_tests[] = {
 	KUNIT_CASE(root_device_devm_register_unregister_test),
 	KUNIT_CASE(root_device_devm_register_get_unregister_with_devm_test),
+#if IS_ENABLED(CONFIG_SYSFS)
+	KUNIT_CASE(root_device_ino_uniqueness_test),
+#endif
 	{}
 };
 
diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
index d68bce0b0b41..1938edd39eff 100644
--- a/fs/kernfs/dir.c
+++ b/fs/kernfs/dir.c
@@ -14,6 +14,7 @@
 #include <linux/slab.h>
 #include <linux/security.h>
 #include <linux/hash.h>
+#include <linux/local_lock.h>
 #include <linux/ns_common.h>
 
 #include "kernfs-internal.h"
@@ -28,6 +29,96 @@
 static DEFINE_SPINLOCK(kernfs_pr_cont_lock);
 static char kernfs_pr_cont_buf[PATH_MAX];	/* protected by pr_cont_lock */
 
+/*
+ * Cached IDs remain reserved as NULL IDR entries until they are consumed or
+ * the owning root is destroyed.
+ */
+#define KERNFS_INO_BATCH	16
+
+struct kernfs_ino_cache {
+	local_lock_t lock;
+	int count;
+	u64 id[KERNFS_INO_BATCH];
+};
+
+static int kernfs_ino_cache_alloc(struct kernfs_root *root)
+{
+	struct kernfs_ino_cache __percpu *cache;
+	int cpu;
+
+	cache = alloc_percpu(struct kernfs_ino_cache);
+	if (!cache)
+		return -ENOMEM;
+
+	for_each_possible_cpu(cpu) {
+		struct kernfs_ino_cache *c = per_cpu_ptr(cache, cpu);
+
+		local_lock_init(&c->lock);
+		c->count = 0;
+	}
+
+	root->ino_cache = cache;
+	return 0;
+}
+
+static void kernfs_ino_cache_free(struct kernfs_root *root)
+{
+	free_percpu(root->ino_cache);
+}
+
+/*
+ * Refill this CPU's cache in place: the free space is computed under
+ * the same local lock that guards the pop, so every reserved ID lands
+ * in the cache and none is ever returned. Lock ordering is
+ * one-directional: idr_preload() returns holding the radix-tree
+ * preload local_lock, the ino_cache local_lock nests inside it, and
+ * root->kernfs_idr_lock nests inside that. idr_alloc_cyclic(GFP_ATOMIC)
+ * does not sleep and is legal under both.
+ */
+static u64 kernfs_ino_batch_refill_and_get(struct kernfs_root *root)
+{
+	struct kernfs_ino_cache *c;
+	u64 id = 0;
+
+	idr_preload(GFP_KERNEL);
+	local_lock(&root->ino_cache->lock);
+	c = this_cpu_ptr(root->ino_cache);
+	spin_lock(&root->kernfs_idr_lock);
+	while (c->count < KERNFS_INO_BATCH) {
+		int ino = idr_alloc_cyclic(&root->ino_idr, NULL, 1, 0,
+					   GFP_ATOMIC);
+
+		if (ino < 0)
+			break;
+		if (ino < root->last_id_lowbits)
+			root->id_highbits++;
+		root->last_id_lowbits = ino;
+		c->id[c->count++] = (u64)root->id_highbits << 32 | ino;
+	}
+	spin_unlock(&root->kernfs_idr_lock);
+	if (c->count)
+		id = c->id[--c->count];
+	local_unlock(&root->ino_cache->lock);
+	idr_preload_end();
+	return id;
+}
+
+static u64 kernfs_ino_batch_get(struct kernfs_root *root)
+{
+	struct kernfs_ino_cache *c;
+	u64 id = 0;
+
+	local_lock(&root->ino_cache->lock);
+	c = this_cpu_ptr(root->ino_cache);
+	if (c->count > 0)
+		id = c->id[--c->count];
+	local_unlock(&root->ino_cache->lock);
+
+	if (!id)
+		id = kernfs_ino_batch_refill_and_get(root);
+	return id;
+}
+
 #define rb_to_kn(X) rb_entry((X), struct kernfs_node, rb)
 
 static bool __kernfs_active(struct kernfs_node *kn)
@@ -625,6 +716,7 @@ void kernfs_put(struct kernfs_node *kn)
 			goto repeat;
 	} else {
 		/* just released the root kn, free @root too */
+		kernfs_ino_cache_free(root);
 		idr_destroy(&root->ino_idr);
 		simple_xattr_cache_cleanup(&root->xa_cache);
 		kfree_rcu(root, rcu);
@@ -668,6 +760,28 @@ static struct kernfs_node *__kernfs_new_node(struct kernfs_root *root,
 	if (!kn)
 		goto err_out1;
 
+	if (root->ino_cache) {
+		u64 id = kernfs_ino_batch_get(root);
+
+		if (likely(id)) {
+			kn->id = id;
+			/*
+			 * The reservation cleared IDR_FREE, so this
+			 * is one count-neutral rcu_assign_pointer()
+			 * into a slot handed to exactly one caller:
+			 * nothing can concurrently remove or replace
+			 * it.  Publication matches the locked
+			 * idr_alloc() below -- count==0 makes ID
+			 * lookups refuse @kn until initialized.
+			 */
+			rcu_read_lock();
+			WARN_ON_ONCE(idr_replace(&root->ino_idr, kn,
+						 (u32)kernfs_ino(kn)));
+			rcu_read_unlock();
+			goto ino_done;
+		}
+	}
+
 	idr_preload(GFP_KERNEL);
 	spin_lock(&root->kernfs_idr_lock);
 	ret = idr_alloc_cyclic(&root->ino_idr, kn, 1, 0, GFP_ATOMIC);
@@ -682,6 +796,7 @@ static struct kernfs_node *__kernfs_new_node(struct kernfs_root *root,
 
 	kn->id = (u64)id_highbits << 32 | ret;
 
+ ino_done:
 	atomic_set(&kn->count, 1);
 	atomic_set(&kn->active, KN_DEACTIVATED_BIAS);
 	RB_CLEAR_NODE(&kn->rb);
@@ -1058,6 +1173,13 @@ struct kernfs_root *kernfs_create_root(struct kernfs_syscall_ops *scops,
 
 	root->syscall_ops = scops;
 	root->flags = flags;
+	/*
+	 * Batching is best-effort: without the cache every node takes the
+	 * locked path, so an allocation failure only needs to be visible.
+	 */
+	if (IS_ENABLED(CONFIG_SMP) && (flags & KERNFS_ROOT_INO_BATCH) &&
+	    kernfs_ino_cache_alloc(root))
+		pr_warn_once("kernfs: inode ID batching unavailable, using the locked allocation path\n");
 	root->kn = kn;
 	init_waitqueue_head(&root->deactivate_waitq);
 
diff --git a/fs/kernfs/kernfs-internal.h b/fs/kernfs/kernfs-internal.h
index aa784b540b36..6e12233731e6 100644
--- a/fs/kernfs/kernfs-internal.h
+++ b/fs/kernfs/kernfs-internal.h
@@ -30,6 +30,8 @@ struct kernfs_iattrs {
 	struct simple_xattr_limits xattr_limits;
 };
 
+struct kernfs_ino_cache;
+
 struct kernfs_root {
 	/* published fields */
 	struct kernfs_node	*kn;
@@ -40,6 +42,7 @@ struct kernfs_root {
 	spinlock_t		kernfs_idr_lock;	/* root->ino_idr */
 	u32			last_id_lowbits;
 	u32			id_highbits;
+	struct kernfs_ino_cache __percpu *ino_cache;
 	struct kernfs_syscall_ops *syscall_ops;
 
 	/* list of kernfs_super_info of this root, protected by kernfs_rwsem */
diff --git a/fs/sysfs/mount.c b/fs/sysfs/mount.c
index 88c10823fcaf..86288c5e34e8 100644
--- a/fs/sysfs/mount.c
+++ b/fs/sysfs/mount.c
@@ -86,7 +86,8 @@ int __init sysfs_init(void)
 {
 	int err;
 
-	sysfs_root = kernfs_create_root(NULL, KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK,
+	sysfs_root = kernfs_create_root(NULL, KERNFS_ROOT_EXTRA_OPEN_PERM_CHECK |
+					KERNFS_ROOT_INO_BATCH,
 					NULL);
 	if (IS_ERR(sysfs_root))
 		return PTR_ERR(sysfs_root);
diff --git a/include/linux/kernfs.h b/include/linux/kernfs.h
index 351a5101c862..6440882b7d58 100644
--- a/include/linux/kernfs.h
+++ b/include/linux/kernfs.h
@@ -156,6 +156,19 @@ enum kernfs_root_flag {
 	 * Renames must not change the parent node.
 	 */
 	KERNFS_ROOT_INVARIANT_PARENT		= 0x0010,
+
+	/*
+	 * Reserve inode numbers for this root in per-CPU batches, taking the
+	 * root's ID allocation lock out of the per-node creation path.
+	 * Best-effort: if the cache cannot be allocated the root falls back to
+	 * the locked path.  Costs up to KERNFS_INO_BATCH - 1
+	 * reserved-but-unused IDs per possible CPU per opted-in root, held as
+	 * NULL IDR entries until the root's IDR is destroyed.  Must not be
+	 * combined with KERNFS_ROOT_SUPPORT_EXPORTOP: reserved IDs are
+	 * allocated inos with no node, which the fhandle by-id lookup must not
+	 * resolve.
+	 */
+	KERNFS_ROOT_INO_BATCH			= 0x0020,
 };
 
 /* type-specific structures for kernfs_node union members */
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/3] driver core: Index class glue directories by parent kobject
  2026-09-11 17:16 ` [PATCH 2/3] driver core: Index class glue directories by parent kobject Pavol Sakac
@ 2026-09-11 18:11   ` Andy Shevchenko
  0 siblings, 0 replies; 6+ messages in thread
From: Andy Shevchenko @ 2026-09-11 18:11 UTC (permalink / raw)
  To: Pavol Sakac
  Cc: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
	Danilo Krummrich, driver-core, linux-kernel, Xu Yang,
	Bartosz Golaszewski, nh-open-source

On Fri, Sep 11, 2026 at 07:16:04PM +0200, Pavol Sakac wrote:
> get_device_parent() finds a parent's glue directory by walking the
> class's glue_dirs kset list under gdp_mutex, so one per parent, as
> vfio-dev needs per SR-IOV VF, is quadratic.
> 
> Index them by parent kobject in an rbtree embedded in the class's
> subsys_private, which dies with the kset list it indexes, so no
> per-entry class check is needed: two classes below one parent are told
> apart by tree selection. The key is a kobject because a parentless
> class device hangs off the shared "virtual" kobject, referenced by the
> glue dir while indexed. The rb_node lives in struct class_dir, adding
> no allocation and no failure mode.
> 
> gdp_mutex serializes the index, as it has glue dir lookup/create/remove
> since commit 77d3d7c1d561f
> ("driver-core: fix race condition in get_device_parent()") and
> commit e4a60d1390609
> ("sysfs: driver core: Fix glue dir race condition by gdp_mutex").

> A kernfs name lookup in the parent's directory needs no new state, but
> takes the kernfs root rwsem under gdp_mutex, behind the writes
> concurrent sysfs directory creation generates.
> 
> A KUnit suite covers the index: reuse of one parent's glue directory,
> two classes below one parent, the parentless "virtual" cases, reap and
> recreate, name collision, many parents, device_move(), and class
> teardown.

...

> +++ b/drivers/base/test/.kunitconfig
> @@ -1,2 +1,3 @@
>  CONFIG_KUNIT=y
>  CONFIG_DM_KUNIT_TEST=y
> +CONFIG_GLUE_DIR_KUNIT_TEST=y

Is this test case is so important that it must *always* run?

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 1/3] kernfs: activate nodes while linking them
  2026-09-11 17:16 ` [PATCH 1/3] kernfs: activate nodes while linking them Pavol Sakac
@ 2026-09-11 18:12   ` Andy Shevchenko
  0 siblings, 0 replies; 6+ messages in thread
From: Andy Shevchenko @ 2026-09-11 18:12 UTC (permalink / raw)
  To: Pavol Sakac
  Cc: Greg Kroah-Hartman, Tejun Heo, Rafael J . Wysocki,
	Danilo Krummrich, driver-core, linux-kernel, Xu Yang,
	Bartosz Golaszewski, nh-open-source

On Fri, Sep 11, 2026 at 07:16:03PM +0200, Pavol Sakac wrote:
> kernfs_add_one() links a new node, releases kernfs_rwsem, and takes it
> again through kernfs_activate(). A new node has no descendants, so the
> second hold only activates that node.
> 
> Activate ordinary nodes before releasing the linking hold, removing a
> second write-side acquisition and the linked-but-inactive interval.
> KERNFS_ROOT_CREATE_DEACTIVATED roots retain explicit subtree activation.
> 
> Removing the unlock-to-lock pair also removes its publication ordering.
> The in-tree lockless ID lookups use CREATE_DEACTIVATED roots and retain
> kernfs_activate(); unsynchronized callers cannot rely on observing a
> node.

...

> +static void kernfs_activate_one(struct kernfs_node *kn);
> +

Can we avoid adding forward declarations like this? Or is there circular
dependencies? If not, add another patch that simply moves the existing code
upper, so this patch won't need a forward declaration.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-09-11 18:12 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 17:16 [PATCH 0/3] kernfs, driver core: Cut per-node lock traffic in bulk registration Pavol Sakac
2026-09-11 17:16 ` [PATCH 1/3] kernfs: activate nodes while linking them Pavol Sakac
2026-09-11 18:12   ` Andy Shevchenko
2026-09-11 17:16 ` [PATCH 2/3] driver core: Index class glue directories by parent kobject Pavol Sakac
2026-09-11 18:11   ` Andy Shevchenko
2026-09-11 17:16 ` [PATCH 3/3] kernfs: batch inode ID allocation per CPU Pavol Sakac

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox