[PATCH v2 67/83] block: rnull: add an option to change the number of hardware queues

From: Andreas Hindborg

Date: Tue Jun 09 2026 - 15:24:18 EST


Add a feature to rnull that allows changing the number of simulated
hardware queues during device operation.

Signed-off-by: Andreas Hindborg <a.hindborg@xxxxxxxxxx>
---
drivers/block/rnull/configfs.rs | 117 ++++++++++++++++++++++++++--------------
drivers/block/rnull/rnull.rs | 46 ++++++++++------
2 files changed, 108 insertions(+), 55 deletions(-)

diff --git a/drivers/block/rnull/configfs.rs b/drivers/block/rnull/configfs.rs
index 8195d645ecc6..d9246b9150f4 100644
--- a/drivers/block/rnull/configfs.rs
+++ b/drivers/block/rnull/configfs.rs
@@ -148,7 +148,13 @@ fn make_group(
completion_time: time::Delta::ZERO,
name: name.try_into()?,
memory_backed: false,
- submit_queues: 1,
+ queue_config: Arc::pin_init(
+ new_mutex!(QueueConfig {
+ submit_queues: 1,
+ poll_queues: 0
+ }),
+ GFP_KERNEL
+ )?,
home_node: bindings::NUMA_NO_NODE,
discard: false,
no_sched: false,
@@ -169,7 +175,6 @@ fn make_group(
zone_max_open: 0,
zone_max_active: 0,
zone_append_max_sectors: u32::MAX,
- poll_queues: 0,
fua: true,
}),
}),
@@ -236,7 +241,7 @@ struct DeviceConfigInner {
completion_time: time::Delta,
disk: Option<Arc<GenDisk<NullBlkDevice>>>,
memory_backed: bool,
- submit_queues: u32,
+ queue_config: Arc<Mutex<QueueConfig>>,
home_node: i32,
discard: bool,
no_sched: bool,
@@ -257,7 +262,6 @@ struct DeviceConfigInner {
zone_max_open: u32,
zone_max_active: u32,
zone_append_max_sectors: u32,
- poll_queues: u32,
fua: bool,
}

@@ -310,9 +314,8 @@ fn store(this: &DeviceConfig, page: &[u8]) -> Result {
bandwidth_limit: u64::from(guard.mbps) * 2u64.pow(20),
shared_tag_set: guard.shared_tags.then(|| guard.shared_tag_set.clone()),
tag_set: crate::TagSetOptions {
- submit_queues: guard.submit_queues,
- poll_queues: guard.poll_queues,
home_node: guard.home_node,
+ queue_config: guard.queue_config.clone(),
blocking: guard.blocking,
memory_backed: guard.memory_backed,
no_sched: guard.no_sched,
@@ -337,9 +340,17 @@ fn store(this: &DeviceConfig, page: &[u8]) -> Result {
}
}

-configfs_simple_field!(DeviceConfig, 1,
- block_size, u32,
- check GenDiskBuilder::<NullBlkDevice>::validate_block_size
+pub(crate) struct QueueConfig {
+ pub(crate) submit_queues: u32,
+ pub(crate) poll_queues: u32,
+}
+
+configfs_simple_field!(
+ DeviceConfig,
+ 1,
+ block_size,
+ u32,
+ check GenDiskBuilder::<NullBlkDevice>::validate_block_size
);
configfs_simple_bool_field!(DeviceConfig, 2, rotational);
configfs_simple_field!(DeviceConfig, 3, capacity_mib, u64);
@@ -363,38 +374,44 @@ fn from_str(s: &str) -> Result<Self> {

configfs_simple_bool_field!(DeviceConfig, 6, memory_backed);

-#[vtable]
-impl configfs::AttributeOperations<7> for DeviceConfig {
- type Data = DeviceConfig;
-
- fn show(this: &DeviceConfig, page: &mut [u8; PAGE_SIZE]) -> Result<usize> {
- let mut writer = kernel::str::Formatter::new(page);
- writer.write_fmt(fmt!("{}\n", this.data.lock().submit_queues))?;
- Ok(writer.bytes_written())
- }
+configfs_attribute! {
+ DeviceConfig,
+ 7,
+ show: |this, page| show_field(this.data.lock().queue_config.lock().submit_queues, page),
+ store: |this,page| {
+ let config_guard = this.data.lock();
+ let mut queue_config = config_guard.queue_config.lock();

- fn store(this: &DeviceConfig, page: &[u8]) -> Result {
- if this.data.lock().powered {
- return Err(EBUSY);
+ let text = core::str::from_utf8(page)?.trim();
+ let value = text.parse().map_err(|_| EINVAL)?;
+ if value > kernel::cpu::num_possible_cpus() {
+ return Err(kernel::error::code::EINVAL)
}

- let text = core::str::from_utf8(page)?.trim();
- let value = text
- .parse::<u32>()
- .map_err(|_| kernel::error::code::EINVAL)?;
+ let old_submit_queues = queue_config.submit_queues;
+ queue_config.submit_queues = value;
+ let total_queue_count = queue_config.submit_queues + queue_config.poll_queues;
+
+ let disk = config_guard.disk.clone();
+
+ drop(queue_config);
+ drop(config_guard);

- if value == 0 || value > kernel::cpu::num_possible_cpus() {
- return Err(kernel::error::code::EINVAL);
+ if let Some(disk) = &disk {
+ if let Err(e) = disk.tag_set().update_hw_queue_count(total_queue_count) {
+ this.data.lock().queue_config.lock().submit_queues = old_submit_queues;
+ return Err(e);
+ }
}

- this.data.lock().submit_queues = value;
Ok(())
- }
+ },
}

configfs_attribute!(DeviceConfig, 8,
show: |this, page| show_field(
- this.data.lock().submit_queues == kernel::numa::num_online_nodes(), page
+ this.data.lock().queue_config.lock().submit_queues == kernel::numa::num_online_nodes(),
+ page
),
store: |this, page| store_with_power_check(this, page, |data, page| {
let value = core::str::from_utf8(page)?
@@ -404,7 +421,7 @@ fn store(this: &DeviceConfig, page: &[u8]) -> Result {
!= 0;

if value {
- data.submit_queues = kernel::numa::num_online_nodes();
+ data.queue_config.lock().submit_queues = kernel::numa::num_online_nodes();
}
Ok(())
})
@@ -506,17 +523,37 @@ fn store(this: &DeviceConfig, page: &[u8]) -> Result {
configfs_simple_field!(DeviceConfig, 24, zone_max_open, u32);
configfs_simple_field!(DeviceConfig, 25, zone_max_active, u32);
configfs_simple_field!(DeviceConfig, 26, zone_append_max_sectors, u32);
-configfs_simple_field!(
+configfs_attribute! {
DeviceConfig,
27,
- poll_queues,
- u32,
- check(|value| {
+ show: |this, page| show_field(this.data.lock().queue_config.lock().poll_queues, page),
+ store: |this,page| {
+ let config_guard = this.data.lock();
+ let mut queue_config = config_guard.queue_config.lock();
+
+ let text = core::str::from_utf8(page)?.trim();
+ let value = text.parse().map_err(|_| EINVAL)?;
if value > kernel::cpu::num_possible_cpus() {
- Err(kernel::error::code::EINVAL)
- } else {
- Ok(())
+ return Err(kernel::error::code::EINVAL)
}
- })
-);
+
+ let old_poll_queues = queue_config.poll_queues;
+ queue_config.poll_queues = value;
+ let total_queue_count = queue_config.submit_queues + queue_config.poll_queues;
+
+ let disk = config_guard.disk.clone();
+
+ drop(queue_config);
+ drop(config_guard);
+
+ if let Some(disk) = &disk {
+ if let Err(e) = disk.tag_set().update_hw_queue_count(total_queue_count) {
+ this.data.lock().queue_config.lock().poll_queues = old_poll_queues;
+ return Err(e);
+ }
+ }
+
+ Ok(())
+ },
+}
configfs_simple_bool_field!(DeviceConfig, 28, fua);
diff --git a/drivers/block/rnull/rnull.rs b/drivers/block/rnull/rnull.rs
index c3126b923367..6653db5c069b 100644
--- a/drivers/block/rnull/rnull.rs
+++ b/drivers/block/rnull/rnull.rs
@@ -10,7 +10,10 @@
#[cfg(CONFIG_BLK_DEV_ZONED)]
mod zoned;

-use configfs::IRQMode;
+use configfs::{
+ IRQMode,
+ QueueConfig, //
+};
use disk_storage::{
DiskStorage,
NullBlockPage,
@@ -224,9 +227,14 @@ fn init(_module: &'static ThisModule) -> impl PinInit<Self, Error> {
let hw_queue_depth = module_parameters::hw_queue_depth.value();

let shared_tag_set = NullBlkDevice::build_tag_set(TagSetOptions {
- submit_queues,
- poll_queues,
home_node,
+ queue_config: Arc::pin_init(
+ new_mutex!(QueueConfig {
+ submit_queues,
+ poll_queues,
+ }),
+ GFP_KERNEL,
+ )?,
blocking,
memory_backed,
no_sched,
@@ -256,9 +264,14 @@ fn init(_module: &'static ThisModule) -> impl PinInit<Self, Error> {
.value()
.then(|| shared_tag_set.clone()),
tag_set: TagSetOptions {
- submit_queues,
- poll_queues,
home_node,
+ queue_config: Arc::pin_init(
+ new_mutex!(QueueConfig {
+ submit_queues,
+ poll_queues,
+ }),
+ GFP_KERNEL,
+ )?,
blocking,
memory_backed,
no_sched,
@@ -338,9 +351,8 @@ struct NullBlkDevice {
}

struct TagSetOptions {
- submit_queues: u32,
- poll_queues: u32,
home_node: i32,
+ queue_config: Arc<Mutex<QueueConfig>>,
blocking: bool,
memory_backed: bool,
no_sched: bool,
@@ -352,9 +364,8 @@ impl NullBlkDevice {

fn build_tag_set(options: TagSetOptions) -> Result<Arc<TagSet<Self>>> {
let TagSetOptions {
- submit_queues,
- poll_queues,
home_node,
+ queue_config,
blocking,
memory_backed,
no_sched,
@@ -379,14 +390,18 @@ fn build_tag_set(options: TagSetOptions) -> Result<Arc<TagSet<Self>>> {
flags |= mq::tag_set::Flag::NoDefaultScheduler;
}

+ let queue_config_guard = queue_config.lock();
+ let submit_queues = queue_config_guard.submit_queues;
+ let poll_queues = queue_config_guard.poll_queues;
+ drop(queue_config_guard);
+
Arc::pin_init(
TagSet::new(
submit_queues + poll_queues,
KBox::new(
NullBlkTagsetData {
queue_depth: hw_queue_depth,
- submit_queue_count: submit_queues,
- poll_queue_count: poll_queues,
+ queue_config,
},
GFP_KERNEL,
)?,
@@ -823,8 +838,7 @@ impl HasHrTimer<Self> for Pdu {

struct NullBlkTagsetData {
queue_depth: u32,
- submit_queue_count: u32,
- poll_queue_count: u32,
+ queue_config: Arc<Mutex<QueueConfig>>,
}

#[vtable]
@@ -970,8 +984,10 @@ fn report_zones(
}

fn map_queues(tag_set: Pin<&mut TagSet<Self>>) {
- let mut submit_queue_count = tag_set.data().submit_queue_count;
- let mut poll_queue_count = tag_set.data().poll_queue_count;
+ let queue_config = tag_set.data().queue_config.lock();
+ let mut submit_queue_count = queue_config.submit_queues;
+ let mut poll_queue_count = queue_config.poll_queues;
+ drop(queue_config);

if tag_set.hw_queue_count() != submit_queue_count + poll_queue_count {
pr_warn!(

--
2.51.2