* [PATCH v6 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
@ 2026-10-23 23:04 Saravanan D
2026-10-23 23:04 ` [PATCH v6 1/2] nvme: add per-queue sysfs directories Saravanan D
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Saravanan D @ 2026-10-23 23:04 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen,
Saravanan D
On hosts that partition their CPUs after connect time, the connect
time io_cpu selection can place a queue's socket work on CPUs owned by
another workload. This series lets a control plane set each queue's
io_cpu through /sys/class/nvme/nvmeX/queues/<qid>/. Patch 1 adds the
core helpers, patch 2 the nvme-tcp attributes.
Changes since v5 [1]:
- The queue directory is added once the io_cpu selection has run, so
readers never see a placement that is about to change (Sashiko).
- Rebased onto nvme-7.4, and the ABI entry targets 7.5.
managed stays per queue, as discussed in [1]. A blktests patch,
nvme/075, covers the interface and follows in this thread.
[1] https://lore.kernel.org/linux-nvme/20261018010059.83120-1-saravanand@crusoe.ai/
Saravanan D (2):
nvme: add per-queue sysfs directories
nvme-tcp: allow setting per-queue io_cpu through sysfs
Documentation/ABI/stable/sysfs-nvme | 31 +++++
drivers/nvme/host/core.c | 6 +
drivers/nvme/host/nvme.h | 23 ++++
drivers/nvme/host/sysfs.c | 88 ++++++++++++++
drivers/nvme/host/tcp.c | 172 ++++++++++++++++++++++++----
5 files changed, 300 insertions(+), 20 deletions(-)
base-commit: aaedcff9014876387504a8b92d227766cb95015c
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v6 1/2] nvme: add per-queue sysfs directories
2026-10-23 23:04 [PATCH v6 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-10-23 23:04 ` Saravanan D
2026-10-23 23:04 ` [PATCH v6 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-23 23:08 ` [PATCH blktests] nvme/075: test the per-queue io_cpu sysfs attribute Saravanan D
2 siblings, 0 replies; 5+ messages in thread
From: Saravanan D @ 2026-10-23 23:04 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen,
Saravanan D
Per I/O queue transport state, such as the nvme-tcp io_cpu, has
nowhere to live in sysfs, since the core has no directory for
individual queues.
Add helpers that create the controller's queues directory and add or
remove one directory per queue
/sys/class/nvme/nvmeX/queues/<qid>/
carrying attributes provided by the transport. The transport creates
the queues directory from its controller setup path, adds a queue's
directory when it sets the queue up and removes it when it frees the
queue, so the directories follow the queues across reconnects and
changes in the queue count. The core allocates each queue directory
and its release frees only that allocation. Removal waits for running
attribute callbacks, so the transport may tear the queue down once it
returns.
The queues directory holds a reference on the controller device, so
nvme_uninit_ctrl() deletes it after the transport has removed the
queue directories.
Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
drivers/nvme/host/core.c | 6 +++
drivers/nvme/host/nvme.h | 23 ++++++++++
drivers/nvme/host/sysfs.c | 88 +++++++++++++++++++++++++++++++++++++++
3 files changed, 117 insertions(+)
diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index 531f9bb14bab..41e1c9d0ee58 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -5153,6 +5153,12 @@ void nvme_uninit_ctrl(struct nvme_ctrl *ctrl)
{
nvme_stop_keep_alive(ctrl);
nvme_hwmon_exit(ctrl);
+ /* transports remove their queue directories before this point */
+ if (ctrl->queues_kobj) {
+ kobject_del(ctrl->queues_kobj);
+ kobject_put(ctrl->queues_kobj);
+ ctrl->queues_kobj = NULL;
+ }
nvme_fault_inject_fini(&ctrl->fault_inject);
dev_pm_qos_hide_latency_tolerance(ctrl->device);
cdev_device_del(&ctrl->cdev, ctrl->device);
diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h
index a8ef62b8e34b..d198214d7f8b 100644
--- a/drivers/nvme/host/nvme.h
+++ b/drivers/nvme/host/nvme.h
@@ -339,6 +339,28 @@ enum nvme_ctrl_flags {
NVME_CTRL_FROZEN = 6,
};
+/*
+ * Per-queue directory /sys/class/nvme/nvmeX/queues/<qid>/, allocated by the
+ * core and carrying attributes provided by the transport.
+ */
+struct nvme_queue_info {
+ struct kobject kobj;
+ void *data;
+};
+
+static inline void *nvme_queue_info_data(struct kobject *kobj)
+{
+ return container_of(kobj, struct nvme_queue_info, kobj)->data;
+}
+
+struct nvme_ctrl;
+
+int nvme_queue_dir_create(struct nvme_ctrl *ctrl);
+struct nvme_queue_info *
+nvme_add_queue_info(struct nvme_ctrl *ctrl, unsigned int qid,
+ const struct attribute_group **groups, void *data);
+void nvme_remove_queue_info(struct nvme_queue_info *qinfo);
+
struct nvme_ctrl {
bool comp_seen;
bool identified;
@@ -360,6 +382,7 @@ struct nvme_ctrl {
struct srcu_struct srcu;
struct device ctrl_device;
struct device *device; /* char device */
+ struct kobject *queues_kobj;
#ifdef CONFIG_NVME_HWMON
struct device *hwmon_device;
#endif
diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c
index 5224597a32f1..cf62ff905a35 100644
--- a/drivers/nvme/host/sysfs.c
+++ b/drivers/nvme/host/sysfs.c
@@ -1313,3 +1313,91 @@ const struct attribute_group *nvme_subsys_attrs_groups[] = {
&nvme_subsys_attrs_group,
NULL,
};
+
+static void nvme_queue_info_release(struct kobject *kobj)
+{
+ kfree(container_of(kobj, struct nvme_queue_info, kobj));
+}
+
+static const struct kobj_type nvme_queue_info_ktype = {
+ .sysfs_ops = &kobj_sysfs_ops,
+ .release = nvme_queue_info_release,
+};
+
+/**
+ * nvme_queue_dir_create - create the controller's queues directory
+ * @ctrl: controller
+ *
+ * Creates /sys/class/nvme/nvmeX/queues/ if it does not exist yet. The
+ * transport calls this from its controller setup path, which must not run
+ * it concurrently for the same controller. nvme_uninit_ctrl() deletes the
+ * directory.
+ */
+int nvme_queue_dir_create(struct nvme_ctrl *ctrl)
+{
+ if (ctrl->queues_kobj)
+ return 0;
+
+ ctrl->queues_kobj = kobject_create_and_add("queues",
+ &ctrl->device->kobj);
+ if (!ctrl->queues_kobj)
+ return -ENOMEM;
+ return 0;
+}
+EXPORT_SYMBOL_GPL(nvme_queue_dir_create);
+
+/**
+ * nvme_add_queue_info - add a directory for one queue
+ * @ctrl: controller, whose queues directory must exist
+ * @qid: queue id, used as the directory name
+ * @groups: attributes provided by the transport
+ * @data: transport data, returned by nvme_queue_info_data()
+ *
+ * Adds /sys/class/nvme/nvmeX/queues/<qid>/. Calls for different queues of
+ * the same controller may run concurrently. Returns the queue info, to be
+ * passed to nvme_remove_queue_info(), or an ERR_PTR().
+ */
+struct nvme_queue_info *
+nvme_add_queue_info(struct nvme_ctrl *ctrl, unsigned int qid,
+ const struct attribute_group **groups, void *data)
+{
+ struct nvme_queue_info *qinfo;
+ int ret;
+
+ if (WARN_ON_ONCE(!ctrl->queues_kobj))
+ return ERR_PTR(-EINVAL);
+
+ qinfo = kzalloc_obj(*qinfo);
+ if (!qinfo)
+ return ERR_PTR(-ENOMEM);
+ qinfo->data = data;
+
+ ret = kobject_init_and_add(&qinfo->kobj, &nvme_queue_info_ktype,
+ ctrl->queues_kobj, "%u", qid);
+ if (ret)
+ goto out_put;
+
+ ret = sysfs_create_groups(&qinfo->kobj, groups);
+ if (ret)
+ goto out_put;
+ return qinfo;
+
+out_put:
+ kobject_put(&qinfo->kobj);
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_GPL(nvme_add_queue_info);
+
+/**
+ * nvme_remove_queue_info - remove a queue directory
+ * @qinfo: queue info returned by nvme_add_queue_info()
+ *
+ * Waits for running attribute callbacks, so none run once this returns
+ * and the transport data may be torn down.
+ */
+void nvme_remove_queue_info(struct nvme_queue_info *qinfo)
+{
+ kobject_del(&qinfo->kobj);
+ kobject_put(&qinfo->kobj);
+}
+EXPORT_SYMBOL_GPL(nvme_remove_queue_info);
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH v6 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs
2026-10-23 23:04 [PATCH v6 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-23 23:04 ` [PATCH v6 1/2] nvme: add per-queue sysfs directories Saravanan D
@ 2026-10-23 23:04 ` Saravanan D
2026-10-23 23:08 ` [PATCH blktests] nvme/075: test the per-queue io_cpu sysfs attribute Saravanan D
2 siblings, 0 replies; 5+ messages in thread
From: Saravanan D @ 2026-10-23 23:04 UTC (permalink / raw)
To: linux-nvme
Cc: kbusch, hch, sagi, axboe, nilay, dwagner, sgogte, hare, hare,
linux-kernel, iyamahata, kiyer, ganbalagane, sjpark, bchen,
Saravanan D
nvme_tcp_set_queue_io_cpu() picks each queue's io_cpu at connect time
as the least loaded CPU in the queue's blk-mq map group, and all socket
work runs there for the connection's lifetime. This decision falls
short when the host partitions its CPUs after connect time. On a 384
cpu multi tenant host with 128 queue controllers, blk-mq folds three
CPUs into every map group, some groups straddle two tenants' cpusets,
and 9% of nvme_tcp_io_work executions ran outside the submitting VM's
cpuset, seen by the neighbor as steal time.
Expose the io_cpu in each I/O queue's directory
/sys/class/nvme/nvmeX/queues/<qid>/io_cpu
so a control plane that owns CPU placement can set it directly instead
of relying on the driver's heuristic. The attribute accepts a decimal
cpu number, -1 or "unbound". A user assignment, unbound included, is
marked in the queue flags, persists across reconnects and is never
reselected by the driver. The read-only managed attribute reports 1
while io_cpu is driver managed and 0 once the user assigned it. The
accounting of nvme_tcp_cpu_queues applies regardless of who set the
io_cpu.
A queue's directory is added once the connect time selection has set its
io_cpu, so readers never see a placement that is about to change, and
removed when the queue is freed, before it is torn down, so a sysfs
store never runs on a freed queue. The queues directory is created
before the I/O queues are set up in parallel. All io_cpu writes, from
sysfs and from the driver, serialize with the accounting under
nvme_tcp_cpu_queues_lock, while the I/O paths read io_cpu locklessly.
The queue stop now clears NVME_TCP_Q_IO_CPU_SET under that lock, since a
sysfs store may move io_cpu in between.
nvme_tcp_free_queue() now clears NVME_TCP_Q_IO_CPU_SET and drops its
count, because a queue can still be counted when it is freed. A sysfs
store may have run after the queue stop, or the connect may have
failed, and the setup error path frees such a queue without calling
nvme_tcp_stop_queue(), where the count is normally dropped.
Suggested-by: Sagi Grimberg <sagi@grimberg.me>
Link: https://lore.kernel.org/linux-nvme/13c160d0-0657-47ce-9a16-58d348267ab0@grimberg.me/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
Documentation/ABI/stable/sysfs-nvme | 31 +++++
drivers/nvme/host/tcp.c | 172 ++++++++++++++++++++++++----
2 files changed, 183 insertions(+), 20 deletions(-)
diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme
index 9291595f8102..aec327cf507a 100644
--- a/Documentation/ABI/stable/sysfs-nvme
+++ b/Documentation/ABI/stable/sysfs-nvme
@@ -503,3 +503,34 @@ Contact: Hannes Reinecke <hare@suse.de>
Description:
Shows the subsystem type. Possible values: "discovery",
"nvm", "reserved".
+
+What: /sys/class/nvme/nvmeX/queues/<qid>/io_cpu
+What: /sys/class/nvme/nvmeX/queues/<qid>/managed
+Date: October 2026
+KernelVersion: 7.5
+Contact: Saravanan D <saravanand@crusoe.ai>
+Description:
+ Per I/O queue directories, populated by the transport
+ backing the controller. A directory appears once the
+ driver has selected the queue's CPU at connect time and
+ is removed when the queue is torn down, so the
+ directories follow the queue count and are absent while
+ the controller reconnects.
+ NVMe over TCP exposes:
+
+ io_cpu: (RW) The CPU that runs the socket work for I/O
+ queue <qid>, selected by the driver at connect time.
+ Writing a decimal online CPU number overrides the
+ selection, and writing -1 or "unbound" leaves the socket
+ work unbound. A written value persists across reconnects
+ for the life of the controller, also when its CPU goes
+ offline. Unbound socket work runs on the CPU that queued
+ it. When the nvme_tcp module is loaded with wq_unbound
+ set, the socket work runs on an unbound workqueue and
+ how closely it follows io_cpu depends on the affinity
+ scope of the nvme_tcp_wq workqueue. Reads return the
+ current CPU, or -1 when the queue is unbound.
+
+ managed: (RO) Shows who owns the CPU placement. 1 while
+ io_cpu is driver managed, 0 once the user assigned it,
+ for the life of the controller.
diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c
index f876375dffd2..cf92626b4971 100644
--- a/drivers/nvme/host/tcp.c
+++ b/drivers/nvme/host/tcp.c
@@ -95,6 +95,7 @@ enum nvme_tcp_queue_flags {
NVME_TCP_Q_LIVE = 1,
NVME_TCP_Q_POLLING = 2,
NVME_TCP_Q_IO_CPU_SET = 3,
+ NVME_TCP_Q_IO_CPU_USER = 4,
};
enum nvme_tcp_recv_state {
@@ -142,6 +143,7 @@ struct nvme_tcp_queue {
struct completion tls_complete;
int tls_err;
struct page_frag_cache pf_cache;
+ struct nvme_queue_info *qinfo;
void (*state_change)(struct sock *);
void (*data_ready)(struct sock *);
@@ -230,6 +232,19 @@ static inline int nvme_tcp_queue_id(struct nvme_tcp_queue *queue)
return queue - queue->ctrl->queues;
}
+/* the I/O paths read io_cpu locklessly, writers hold the accounting lock */
+static inline int nvme_tcp_queue_io_cpu(struct nvme_tcp_queue *queue)
+{
+ return READ_ONCE(queue->io_cpu);
+}
+
+static inline void nvme_tcp_queue_change_io_cpu(struct nvme_tcp_queue *queue,
+ int cpu)
+ __must_hold(&nvme_tcp_cpu_queues_lock)
+{
+ WRITE_ONCE(queue->io_cpu, cpu);
+}
+
static inline bool nvme_tcp_recv_pdu_supported(enum nvme_tcp_pdu_type type)
{
switch (type) {
@@ -427,7 +442,7 @@ static inline void nvme_tcp_queue_request(struct nvme_tcp_request *req,
* lockdep reports circular locking via elevator_lock. Defer TLS
* sends to the io workqueue instead of inline from this path.
*/
- if (queue->io_cpu == raw_smp_processor_id() &&
+ if (nvme_tcp_queue_io_cpu(queue) == raw_smp_processor_id() &&
!nvme_tcp_queue_tls(queue) &&
empty && mutex_trylock(&queue->send_mutex)) {
nvme_tcp_send_all(queue);
@@ -435,7 +450,8 @@ static inline void nvme_tcp_queue_request(struct nvme_tcp_request *req,
}
if (last && nvme_tcp_queue_has_pending(queue))
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
static void nvme_tcp_process_req_list(struct nvme_tcp_queue *queue)
@@ -844,7 +860,8 @@ static int nvme_tcp_handle_r2t(struct nvme_tcp_queue *queue,
nvme_tcp_setup_h2c_data_pdu(req);
llist_add(&req->lentry, &queue->req_list);
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
return 0;
}
@@ -1131,7 +1148,8 @@ static void nvme_tcp_data_ready(struct sock *sk)
queue = sk->sk_user_data;
if (likely(queue && queue->rd_enabled) &&
!test_bit(NVME_TCP_Q_POLLING, &queue->flags))
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
read_unlock_bh(&sk->sk_callback_lock);
}
@@ -1146,7 +1164,8 @@ static void nvme_tcp_write_space(struct sock *sk)
/* Ensure pending TLS partial records are retried */
if (nvme_tcp_queue_tls(queue))
queue->write_space(sk);
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
read_unlock_bh(&sk->sk_callback_lock);
}
@@ -1469,7 +1488,8 @@ static void nvme_tcp_io_work(struct work_struct *w)
} while (!time_after(jiffies, deadline)); /* quota is exhausted */
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
static void nvme_tcp_free_async_req(struct nvme_tcp_ctrl *ctrl)
@@ -1503,9 +1523,22 @@ static void nvme_tcp_free_queue(struct nvme_ctrl *nctrl, int qid)
struct nvme_tcp_queue *queue = &ctrl->queues[qid];
unsigned int noio_flag;
- if (!test_and_clear_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
+ if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
return;
+ /* no io_cpu store runs once the directory is removed */
+ if (queue->qinfo) {
+ nvme_remove_queue_info(queue->qinfo);
+ queue->qinfo = NULL;
+ }
+ clear_bit(NVME_TCP_Q_ALLOCATED, &queue->flags);
+
+ /* a failed connect or a store after the stop left it counted */
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[nvme_tcp_queue_io_cpu(queue)]--;
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
+
page_frag_cache_drain(&queue->pf_cache);
/**
@@ -1724,8 +1757,18 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
unsigned int *mq_map = NULL;
int cpu, min_queues = INT_MAX, io_cpu;
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ /* a user assigned io_cpu is kept across reconnects */
+ if (test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags)) {
+ io_cpu = nvme_tcp_queue_io_cpu(queue);
+ if (io_cpu != WORK_CPU_UNBOUND &&
+ !test_and_set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[io_cpu]++;
+ goto out_unlock;
+ }
+
if (wq_unbound)
- goto out;
+ goto out_unlock;
if (nvme_tcp_default_queue(queue))
mq_map = set->map[HCTX_TYPE_DEFAULT].mq_map;
@@ -1735,10 +1778,9 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
mq_map = set->map[HCTX_TYPE_POLL].mq_map;
if (WARN_ON(!mq_map))
- goto out;
+ goto out_unlock;
/* Search for the least used cpu from the mq_map */
- spin_lock(&nvme_tcp_cpu_queues_lock);
io_cpu = WORK_CPU_UNBOUND;
for_each_online_cpu(cpu) {
int num_queues = nvme_tcp_cpu_queues[cpu];
@@ -1751,14 +1793,14 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue)
}
}
if (io_cpu != WORK_CPU_UNBOUND) {
- queue->io_cpu = io_cpu;
+ nvme_tcp_queue_change_io_cpu(queue, io_cpu);
nvme_tcp_cpu_queues[io_cpu]++;
set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
}
+out_unlock:
spin_unlock(&nvme_tcp_cpu_queues_lock);
-out:
dev_dbg(ctrl->ctrl.device, "queue %d: using cpu %d\n",
- qid, queue->io_cpu);
+ qid, nvme_tcp_queue_io_cpu(queue));
}
static void nvme_tcp_tls_done(void *data, int status, key_serial_t pskid)
@@ -1846,6 +1888,85 @@ static int nvme_tcp_start_tls(struct nvme_ctrl *nctrl,
return ret;
}
+static ssize_t io_cpu_show(struct kobject *kobj, struct kobj_attribute *attr,
+ char *buf)
+{
+ struct nvme_tcp_queue *queue = nvme_queue_info_data(kobj);
+ int io_cpu = nvme_tcp_queue_io_cpu(queue);
+
+ return sysfs_emit(buf, "%d\n",
+ io_cpu == WORK_CPU_UNBOUND ? -1 : io_cpu);
+}
+
+static ssize_t io_cpu_store(struct kobject *kobj, struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct nvme_tcp_queue *queue = nvme_queue_info_data(kobj);
+ int cpu, old, ret;
+
+ if (sysfs_streq(buf, "unbound")) {
+ cpu = WORK_CPU_UNBOUND;
+ } else {
+ ret = kstrtoint(buf, 10, &cpu);
+ if (ret)
+ return ret;
+ if (cpu == -1)
+ cpu = WORK_CPU_UNBOUND;
+ else if ((unsigned int)cpu >= nr_cpu_ids || !cpu_online(cpu))
+ return -EINVAL;
+ }
+
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ old = nvme_tcp_queue_io_cpu(queue);
+ nvme_tcp_queue_change_io_cpu(queue, cpu);
+ set_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[old]--;
+ if (cpu != WORK_CPU_UNBOUND) {
+ nvme_tcp_cpu_queues[cpu]++;
+ set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags);
+ }
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
+
+ return count;
+}
+
+static ssize_t managed_show(struct kobject *kobj, struct kobj_attribute *attr,
+ char *buf)
+{
+ struct nvme_tcp_queue *queue = nvme_queue_info_data(kobj);
+
+ return sysfs_emit(buf, "%d\n",
+ !test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags));
+}
+
+static struct kobj_attribute nvme_tcp_io_cpu_attr =
+ __ATTR(io_cpu, 0644, io_cpu_show, io_cpu_store);
+static struct kobj_attribute nvme_tcp_managed_attr =
+ __ATTR_RO(managed);
+
+static struct attribute *nvme_tcp_queue_attrs[] = {
+ &nvme_tcp_io_cpu_attr.attr,
+ &nvme_tcp_managed_attr.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(nvme_tcp_queue);
+
+static void nvme_tcp_add_queue_sysfs(struct nvme_tcp_queue *queue)
+{
+ struct nvme_ctrl *nctrl = &queue->ctrl->ctrl;
+ struct nvme_queue_info *qinfo;
+
+ qinfo = nvme_add_queue_info(nctrl, nvme_tcp_queue_id(queue),
+ nvme_tcp_queue_groups, queue);
+ if (IS_ERR(qinfo)) {
+ dev_warn(nctrl->device, "queue %d: no sysfs directory: %ld\n",
+ nvme_tcp_queue_id(queue), PTR_ERR(qinfo));
+ return;
+ }
+ queue->qinfo = qinfo;
+}
+
static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
key_serial_t pskid)
{
@@ -1915,7 +2036,11 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid,
queue->sock->sk->sk_allocation = GFP_ATOMIC;
queue->sock->sk->sk_use_task_frag = false;
- queue->io_cpu = WORK_CPU_UNBOUND;
+ /* no store races here, the lock is for the write helper */
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ if (!test_bit(NVME_TCP_Q_IO_CPU_USER, &queue->flags))
+ nvme_tcp_queue_change_io_cpu(queue, WORK_CPU_UNBOUND);
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
queue->request = NULL;
queue->data_remaining = 0;
queue->ddgst_remaining = 0;
@@ -2030,11 +2155,10 @@ static void nvme_tcp_stop_queue_nowait(struct nvme_ctrl *nctrl, int qid)
if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags))
return;
- if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) {
- spin_lock(&nvme_tcp_cpu_queues_lock);
- nvme_tcp_cpu_queues[queue->io_cpu]--;
- spin_unlock(&nvme_tcp_cpu_queues_lock);
- }
+ spin_lock(&nvme_tcp_cpu_queues_lock);
+ if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags))
+ nvme_tcp_cpu_queues[nvme_tcp_queue_io_cpu(queue)]--;
+ spin_unlock(&nvme_tcp_cpu_queues_lock);
mutex_lock(&queue->queue_lock);
if (test_and_clear_bit(NVME_TCP_Q_LIVE, &queue->flags))
@@ -2097,6 +2221,8 @@ static int nvme_tcp_start_queue(struct nvme_ctrl *nctrl, int idx)
if (idx) {
nvme_tcp_set_queue_io_cpu(queue);
+ /* expose io_cpu only once the driver has selected it */
+ nvme_tcp_add_queue_sysfs(queue);
ret = nvmf_connect_io_queue(nctrl, idx);
} else
ret = nvmf_connect_admin_queue(nctrl);
@@ -2303,6 +2429,11 @@ static int nvme_tcp_configure_io_queues(struct nvme_ctrl *ctrl, bool new)
if (ret)
return ret;
+ /* the parallel queue setup below adds the queue directories */
+ ret = nvme_queue_dir_create(ctrl);
+ if (ret)
+ return ret;
+
if (new) {
ret = nvme_alloc_io_tag_set(ctrl, &to_tcp_ctrl(ctrl)->tag_set,
&nvme_tcp_mq_ops,
@@ -2890,7 +3021,8 @@ static void nvme_tcp_commit_rqs(struct blk_mq_hw_ctx *hctx)
struct nvme_tcp_queue *queue = hctx->driver_data;
if (!llist_empty(&queue->req_list))
- queue_work_on(queue->io_cpu, nvme_tcp_wq, &queue->io_work);
+ queue_work_on(nvme_tcp_queue_io_cpu(queue), nvme_tcp_wq,
+ &queue->io_work);
}
static blk_status_t nvme_tcp_queue_rq(struct blk_mq_hw_ctx *hctx,
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH blktests] nvme/075: test the per-queue io_cpu sysfs attribute
2026-10-23 23:04 [PATCH v6 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-23 23:04 ` [PATCH v6 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-10-23 23:04 ` [PATCH v6 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
@ 2026-10-23 23:08 ` Saravanan D
2026-10-29 11:50 ` Shin'ichiro Kawasaki
2 siblings, 1 reply; 5+ messages in thread
From: Saravanan D @ 2026-10-23 23:08 UTC (permalink / raw)
To: linux-block
Cc: shinichiro.kawasaki, linux-nvme, dwagner, hare, nilay, sagi,
kbusch, iyamahata, kiyer, ganbalagane, sjpark, bchen, Saravanan D
nvme-tcp exposes /sys/class/nvme/nvmeX/queues/<qid>/io_cpu and managed,
so a control plane can choose the CPU that runs each I/O queue's socket
work [1]. Test that
- io_cpu reads back a written CPU and rejects invalid values
- managed is read only and turns 0 once io_cpu is written
- an assignment survives a controller reset
- the queue directories follow the queue count as attr_qid_max
shrinks and grows it
- removing the controller while io_cpu is written completes
The test skips when the kernel does not have the attribute.
[1] https://lore.kernel.org/linux-nvme/20261018010059.83120-1-saravanand@crusoe.ai/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand@crusoe.ai>
---
This tests the kernel series in the parent thread, which is under
review on linux-nvme.
tests/nvme/075 | 185 +++++++++++++++++++++++++++++++++++++++++++++
tests/nvme/075.out | 2 +
2 files changed, 187 insertions(+)
create mode 100755 tests/nvme/075
create mode 100644 tests/nvme/075.out
diff --git a/tests/nvme/075 b/tests/nvme/075
new file mode 100755
index 0000000..0b4bce5
--- /dev/null
+++ b/tests/nvme/075
@@ -0,0 +1,185 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Saravanan D <saravanand@crusoe.ai>
+#
+# Test the per-queue io_cpu sysfs attribute of nvme-tcp
+
+. tests/nvme/rc
+
+DESCRIPTION="test the per-queue io_cpu sysfs attribute"
+
+requires() {
+ _nvme_requires
+ _have_loop
+ _require_nvme_trtype tcp
+ _require_min_cpus 2
+}
+
+set_conditions() {
+ _set_nvme_trtype "$@"
+}
+
+# last CPU in the online list, and the first CPU id past the possible ones
+last_online_cpu() {
+ awk -F'[-,]' '{ print $NF }' /sys/devices/system/cpu/online
+}
+
+first_impossible_cpu() {
+ echo $(( $(awk -F'[-,]' '{ print $NF }' /sys/devices/system/cpu/possible) + 1 ))
+}
+
+nr_queue_dirs() {
+ local nvmedev="$1"
+
+ find "/sys/class/nvme/${nvmedev}/queues" -mindepth 1 -maxdepth 1 \
+ -type d 2>/dev/null | wc -l
+}
+
+expect_attr() {
+ local file="$1"
+ local expected="$2"
+ local value
+
+ value=$(cat "${file}")
+ if [[ "${value}" != "${expected}" ]]; then
+ echo "${file}: read ${value}, expected ${expected}"
+ return 1
+ fi
+ return 0
+}
+
+test_read_write() {
+ local qdir="$1"
+ local cpu="$2"
+ local bad
+
+ expect_attr "${qdir}/1/managed" 1
+
+ echo "${cpu}" > "${qdir}/1/io_cpu"
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ expect_attr "${qdir}/1/managed" 0
+
+ for bad in "$(first_impossible_cpu)" -2 abc 0x1; do
+ if { echo "${bad}" > "${qdir}/1/io_cpu"; } 2>/dev/null; then
+ echo "io_cpu accepted ${bad}"
+ fi
+ done
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+
+ if { echo 1 > "${qdir}/1/managed"; } 2>/dev/null; then
+ echo "managed is writable"
+ fi
+
+ echo unbound > "${qdir}/2/io_cpu"
+ expect_attr "${qdir}/2/io_cpu" -1
+ echo -1 > "${qdir}/2/io_cpu"
+ expect_attr "${qdir}/2/io_cpu" -1
+ expect_attr "${qdir}/2/managed" 0
+}
+
+test_reset() {
+ local nvmedev="$1"
+ local qdir="$2"
+ local cpu="$3"
+
+ echo 1 > "/sys/class/nvme/${nvmedev}/reset_controller"
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ expect_attr "${qdir}/1/managed" 0
+ expect_attr "${qdir}/2/io_cpu" -1
+ expect_attr "${qdir}/2/managed" 0
+}
+
+set_qid_max() {
+ local nvmedev="$1"
+ local qid_max="$2"
+ local queue_count_file="/sys/class/nvme-fabrics/ctl/${nvmedev}/queue_count"
+ local retries=10
+
+ echo "${qid_max}" > \
+ "${NVMET_CFS}/subsystems/${def_subsysnqn}/attr_qid_max"
+ while [[ "$(cat "${queue_count_file}")" -ne $((qid_max + 1)) ]]; do
+ if (( retries-- == 0 )); then
+ echo "queue count did not change to $((qid_max + 1))"
+ return 1
+ fi
+ sleep 1
+ done
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ if [[ "$(nr_queue_dirs "${nvmedev}")" -ne "${qid_max}" ]]; then
+ echo "$(nr_queue_dirs "${nvmedev}") queue directories," \
+ "expected ${qid_max}"
+ return 1
+ fi
+ return 0
+}
+
+io_cpu_writer() {
+ local qdir="$1"
+ local cpu="$2"
+ local file
+
+ while true; do
+ for file in "${qdir}"/*/io_cpu; do
+ { echo "${cpu}" > "${file}"; } 2>/dev/null
+ { echo unbound > "${file}"; } 2>/dev/null
+ done
+ done
+}
+
+test() {
+ echo "Running ${TEST_NAME}"
+
+ _setup_nvmet
+
+ local cfs_path="${NVMET_CFS}/subsystems/${def_subsysnqn}"
+ local skipped=false
+ local nvmedev qdir cpu writer
+
+ _nvmet_target_setup --blkdev file
+
+ _nvme_connect_subsys --keep-alive-tmo 1 --reconnect-delay 2
+ _nvmf_wait_for_state "${def_subsysnqn}" "live"
+ nvmedev=$(_find_nvme_dev "${def_subsysnqn}")
+ qdir="/sys/class/nvme/${nvmedev}/queues"
+ cpu=$(last_online_cpu)
+
+ if [[ ! -f "${qdir}/1/io_cpu" ]]; then
+ SKIP_REASONS+=("missing per-queue io_cpu attribute")
+ skipped=true
+ elif [[ "$(nr_queue_dirs "${nvmedev}")" -lt 2 ]]; then
+ SKIP_REASONS+=("controller has fewer than 2 I/O queues")
+ skipped=true
+ else
+ test_read_write "${qdir}" "${cpu}"
+ test_reset "${nvmedev}" "${qdir}" "${cpu}"
+
+ if [[ -f "${cfs_path}/attr_qid_max" ]]; then
+ set_qid_max "${nvmedev}" 1
+ expect_attr "${qdir}/1/io_cpu" "${cpu}"
+ set_qid_max "${nvmedev}" 2
+ fi
+
+ # remove the controller while its queue directories are written
+ io_cpu_writer "${qdir}" "${cpu}" &
+ writer=$!
+ sleep 1
+ fi
+
+ _nvme_disconnect_subsys
+
+ if [[ -n "${writer}" ]]; then
+ kill "${writer}"
+ wait "${writer}" 2>/dev/null
+ fi
+
+ _nvmet_target_cleanup
+
+ if [[ "${skipped}" = true ]] ; then
+ return 1
+ fi
+
+ echo "Test complete"
+}
diff --git a/tests/nvme/075.out b/tests/nvme/075.out
new file mode 100644
index 0000000..0613dfa
--- /dev/null
+++ b/tests/nvme/075.out
@@ -0,0 +1,2 @@
+Running nvme/075
+Test complete
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH blktests] nvme/075: test the per-queue io_cpu sysfs attribute
2026-10-23 23:08 ` [PATCH blktests] nvme/075: test the per-queue io_cpu sysfs attribute Saravanan D
@ 2026-10-29 11:50 ` Shin'ichiro Kawasaki
0 siblings, 0 replies; 5+ messages in thread
From: Shin'ichiro Kawasaki @ 2026-10-29 11:50 UTC (permalink / raw)
To: Saravanan D
Cc: linux-block, linux-nvme, dwagner, hare, nilay, sagi, kbusch,
iyamahata, kiyer, ganbalagane, sjpark, bchen
On Oct 23, 2026 / 16:08, Saravanan D wrote:
> nvme-tcp exposes /sys/class/nvme/nvmeX/queues/<qid>/io_cpu and managed,
> so a control plane can choose the CPU that runs each I/O queue's socket
> work [1]. Test that
>
> - io_cpu reads back a written CPU and rejects invalid values
> - managed is read only and turns 0 once io_cpu is written
> - an assignment survives a controller reset
> - the queue directories follow the queue count as attr_qid_max
> shrinks and grows it
> - removing the controller while io_cpu is written completes
>
> The test skips when the kernel does not have the attribute.
>
> [1] https://lore.kernel.org/linux-nvme/20261018010059.83120-1-saravanand@crusoe.ai/
>
> Assisted-by: Claude:claude-opus-5-5 [Claude Code]
> Signed-off-by: Saravanan D <saravanand@crusoe.ai>
> ---
> This tests the kernel series in the parent thread, which is under
> review on linux-nvme.
Thanks for the patch. The patch content looks good to me. I applied the kernel
series [1] on top of the current Linus master branch, and confirmed this new
test case passes. Good.
I'll wait for the kernel series get settled. I expect the kernel series will be
revised a few more times, and this blktests patch will need to adjust to it,
probably.
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-29 11:52 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-23 23:04 [PATCH v6 0/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-23 23:04 ` [PATCH v6 1/2] nvme: add per-queue sysfs directories Saravanan D
2026-10-23 23:04 ` [PATCH v6 2/2] nvme-tcp: allow setting per-queue io_cpu through sysfs Saravanan D
2026-10-23 23:08 ` [PATCH blktests] nvme/075: test the per-queue io_cpu sysfs attribute Saravanan D
2026-10-29 11:50 ` Shin'ichiro Kawasaki
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox