[PATCH blktests v2] nvme/081: test the per-queue io_cpu sysfs attribute
Saravanan D
saravanand at crusoe.ai
Tue Nov 10 03:50:05 PST 2026
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 and the queue directories survive 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 queues/ directory or
the io_cpu attribute.
[1] https://lore.kernel.org/linux-nvme/cover.1794015622.git.saravanand@crusoe.ai/
Assisted-by: Claude:claude-opus-5-5 [Claude Code]
Signed-off-by: Saravanan D <saravanand at crusoe.ai>
---
Changes since v1 [2]:
- Connect with --nr-io-queues 2 and expect exactly two queue
directories (Nilay).
- Skip with separate reasons when the queues directory or the io_cpu
attribute is missing (Nilay).
- Check that queue_count is unchanged across a controller reset and
that the queue directories match it (Nilay).
[2] https://lore.kernel.org/all/20261107095350.98072-1-saravanand@crusoe.ai/
tests/nvme/081 | 208 +++++++++++++++++++++++++++++++++++++++++++++
tests/nvme/081.out | 2 +
2 files changed, 210 insertions(+)
create mode 100755 tests/nvme/081
create mode 100644 tests/nvme/081.out
diff --git a/tests/nvme/081 b/tests/nvme/081
new file mode 100755
index 0000000..0bda988
--- /dev/null
+++ b/tests/nvme/081
@@ -0,0 +1,208 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0+
+# Copyright (C) 2026 Saravanan D <saravanand at 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
+}
+
+# queue_count includes the admin queue
+queue_count() {
+ cat "/sys/class/nvme-fabrics/ctl/$1/queue_count"
+}
+
+expect_queue_dirs() {
+ local nvmedev="$1"
+ local expected="$2"
+ local nr
+
+ nr=$(nr_queue_dirs "${nvmedev}")
+ if [[ "${nr}" -ne "${expected}" ]]; then
+ echo "${nr} queue directories, expected ${expected}"
+ return 1
+ fi
+ return 0
+}
+
+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"
+ local count
+
+ count=$(queue_count "${nvmedev}")
+ echo 1 > "/sys/class/nvme/${nvmedev}/reset_controller"
+ _nvmf_wait_for_state "${def_subsysnqn}" "live" || return 1
+
+ if [[ "$(queue_count "${nvmedev}")" -ne "${count}" ]]; then
+ echo "queue count $(queue_count "${nvmedev}") after reset," \
+ "expected ${count}"
+ fi
+ expect_queue_dirs "${nvmedev}" $((count - 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 retries=10
+
+ echo "${qid_max}" > \
+ "${NVMET_CFS}/subsystems/${def_subsysnqn}/attr_qid_max"
+ while [[ "$(queue_count "${nvmedev}")" -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
+
+ expect_queue_dirs "${nvmedev}" "${qid_max}"
+}
+
+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
+
+ # fabrics caps nr_io_queues at the online CPU count, hence the
+ # _require_min_cpus 2
+ _nvme_connect_subsys --keep-alive-tmo 1 --reconnect-delay 2 \
+ --nr-io-queues 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 [[ ! -d "${qdir}" ]]; then
+ SKIP_REASONS+=("missing per-queue sysfs directories")
+ skipped=true
+ elif [[ ! -f "${qdir}/1/io_cpu" ]]; then
+ SKIP_REASONS+=("missing per-queue io_cpu attribute")
+ skipped=true
+ else
+ expect_queue_dirs "${nvmedev}" 2
+ 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/081.out b/tests/nvme/081.out
new file mode 100644
index 0000000..5deb638
--- /dev/null
+++ b/tests/nvme/081.out
@@ -0,0 +1,2 @@
+Running nvme/081
+Test complete
--
2.56.0
More information about the Linux-nvme
mailing list