[PATCH 2/2] block: add a sysfs interface to reserve write streams for a partition
From: Keoseong Park
Date: Fri Sep 11 2026 - 02:53:02 EST
Add /sys/block/<disk>/<partition>/write_streams. Writing N reserves N
of the disk's write streams that no other partition can use, exposed as
its streams 1..N. Writing 0 gives them back. N is capped at U8_MAX
because bio->bi_write_stream is a u8, and asking for more streams than
the disk has left fails with -ENOSPC. The IDs come from a per-disk
bitmap protected by open_mutex and are released when the partition is
removed, including by a partition table rescan. I/O to the whole-disk
device can use any write stream, just as it can write to any sector of
a partition.
The count can only be changed while the partition is not open, the
same rule as for a partition table change: users see a fixed number of
streams from open to close, and no I/O can be in flight while the map
is replaced. Writing the current value is always allowed so that a
rule that sets it can be re-run. Growing or shrinking keeps the
streams that stay, so that a partition keeps writing through the same
disk streams and its data stays together.
The store must not wait for open_mutex while holding the sysfs active
reference, as drop_partition() removes the attribute with open_mutex
held. Break the active protection first and re-check that the
partition still exists under the mutex.
Nothing changes until the file is written.
Signed-off-by: Keoseong Park <keosung.park@xxxxxxxxxxx>
---
Documentation/ABI/stable/sysfs-block | 19 ++++
block/partitions/core.c | 128 +++++++++++++++++++++++++++
include/linux/blkdev.h | 2 +
3 files changed, 149 insertions(+)
diff --git a/Documentation/ABI/stable/sysfs-block b/Documentation/ABI/stable/sysfs-block
index 900b3fc4c72d..442f4c221bde 100644
--- a/Documentation/ABI/stable/sysfs-block
+++ b/Documentation/ABI/stable/sysfs-block
@@ -218,6 +218,25 @@ Description:
same as the format of /sys/block/<disk>/stat.
+What: /sys/block/<disk>/<partition>/write_streams
+Date: September 2026
+Contact: Keoseong Park <keosung.park@xxxxxxxxxxx>
+Description:
+ [RW] Number of the disk's write streams reserved for the
+ partition. Writing N reserves N disk write streams that no
+ other partition can use and exposes them as the partition's
+ write streams 1 to N. Writing 0 gives them back. At most
+ /sys/block/<disk>/queue/max_write_streams streams, and never
+ more than 255, can be reserved on a disk. The number can
+ only be changed while the partition is not open, and a change
+ keeps the stream numbers that survive it pointing at the same
+ disk write streams. Partitions have no write streams unless
+ reserved here, and the reservation is lost when the partition
+ is removed, including by a partition table rescan. I/O to
+ the whole-disk device can use any write stream, just as it
+ can write to any sector of a partition.
+
+
What: /sys/block/<disk>/queue/add_random
Date: June 2010
Contact: linux-block@xxxxxxxxxxxxxxx
diff --git a/block/partitions/core.c b/block/partitions/core.c
index b5c59b79ca7c..77852eb2acbe 100644
--- a/block/partitions/core.c
+++ b/block/partitions/core.c
@@ -205,6 +205,130 @@ static ssize_t part_discard_alignment_show(struct device *dev,
return sysfs_emit(buf, "%u\n", bdev_discard_alignment(dev_to_bdev(dev)));
}
+static ssize_t part_write_streams_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", dev_to_bdev(dev)->bd_nr_write_streams);
+}
+
+/*
+ * Give the write streams of @part back to the disk. Called from
+ * drop_partition() with open_mutex held; the map is left alone because a
+ * write racing with del_gendisk() still uses it until the bdev is freed.
+ */
+static void part_put_write_streams(struct block_device *part)
+{
+ struct gendisk *disk = part->bd_disk;
+ unsigned int i;
+
+ lockdep_assert_held(&disk->open_mutex);
+
+ for (i = 0; i < part->bd_nr_write_streams; i++)
+ __clear_bit(part->bd_write_stream_map[i],
+ disk->write_streams_reserved);
+}
+
+static int part_set_write_streams(struct block_device *part, u8 nr)
+{
+ struct gendisk *disk = part->bd_disk;
+ unsigned int max = min_t(unsigned int,
+ bdev_limits(part)->max_write_streams, U8_MAX);
+ u8 *map = NULL;
+ unsigned int i, id, nr_free;
+ int ret = 0;
+ u8 cur;
+
+ if (nr > max)
+ return -EINVAL;
+
+ if (nr) {
+ map = kmalloc(nr, GFP_KERNEL);
+ if (!map)
+ return -ENOMEM;
+ }
+
+ mutex_lock(&disk->open_mutex);
+
+ /* the partition may have been dropped while waiting for the mutex */
+ if (xa_load(&disk->part_tbl, bdev_partno(part)) != part) {
+ ret = -ENXIO;
+ goto out;
+ }
+
+ cur = part->bd_nr_write_streams;
+
+ /* a no-op change is allowed while open */
+ if (nr == cur)
+ goto out;
+
+ /* the streams must not change under a user of the partition */
+ if (atomic_read(&part->bd_openers)) {
+ ret = -EBUSY;
+ goto out;
+ }
+
+ nr_free = max - bitmap_weight(disk->write_streams_reserved, max + 1);
+ if (nr > cur && nr - cur > nr_free) {
+ ret = -ENOSPC;
+ goto out;
+ }
+
+ /*
+ * Keep the streams that stay, so that a partition keeps writing
+ * through the same disk streams and its data stays together.
+ */
+ for (i = 0; i < min(cur, nr); i++)
+ map[i] = part->bd_write_stream_map[i];
+
+ if (nr < cur) {
+ for (i = nr; i < cur; i++)
+ __clear_bit(part->bd_write_stream_map[i],
+ disk->write_streams_reserved);
+ } else {
+ for (i = cur; i < nr; i++) {
+ id = find_next_zero_bit(disk->write_streams_reserved,
+ max + 1, 1);
+ __set_bit(id, disk->write_streams_reserved);
+ map[i] = id;
+ }
+ }
+
+ swap(part->bd_write_stream_map, map);
+ part->bd_nr_write_streams = nr;
+
+out:
+ mutex_unlock(&disk->open_mutex);
+ kfree(map);
+
+ return ret;
+}
+
+static ssize_t part_write_streams_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct kernfs_node *kn;
+ u8 nr;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &nr);
+ if (ret)
+ return ret;
+
+ /*
+ * drop_partition() removes this attribute with open_mutex held, so
+ * don't hold the active reference while waiting for the mutex.
+ */
+ kn = sysfs_break_active_protection(&dev->kobj, &attr->attr);
+ if (!kn)
+ return -ENXIO;
+
+ ret = part_set_write_streams(dev_to_bdev(dev), nr);
+ sysfs_unbreak_active_protection(kn);
+
+ return ret ? ret : count;
+}
+
static DEVICE_ATTR(partition, 0444, part_partition_show, NULL);
static DEVICE_ATTR(start, 0444, part_start_show, NULL);
static DEVICE_ATTR(size, 0444, part_size_show, NULL);
@@ -213,6 +337,8 @@ static DEVICE_ATTR(alignment_offset, 0444, part_alignment_offset_show, NULL);
static DEVICE_ATTR(discard_alignment, 0444, part_discard_alignment_show, NULL);
static DEVICE_ATTR(stat, 0444, part_stat_show, NULL);
static DEVICE_ATTR(inflight, 0444, part_inflight_show, NULL);
+static DEVICE_ATTR(write_streams, 0644, part_write_streams_show,
+ part_write_streams_store);
#ifdef CONFIG_FAIL_MAKE_REQUEST
static struct device_attribute dev_attr_fail =
__ATTR(make-it-fail, 0644, part_fail_show, part_fail_store);
@@ -227,6 +353,7 @@ static struct attribute *part_attrs[] = {
&dev_attr_discard_alignment.attr,
&dev_attr_stat.attr,
&dev_attr_inflight.attr,
+ &dev_attr_write_streams.attr,
#ifdef CONFIG_FAIL_MAKE_REQUEST
&dev_attr_fail.attr,
#endif
@@ -274,6 +401,7 @@ void drop_partition(struct block_device *part)
{
lockdep_assert_held(&part->bd_disk->open_mutex);
+ part_put_write_streams(part);
xa_erase(&part->bd_disk->part_tbl, bdev_partno(part));
kobject_put(part->bd_holder_dir);
diff --git a/include/linux/blkdev.h b/include/linux/blkdev.h
index 686ed60a0bb7..5d241d7b0802 100644
--- a/include/linux/blkdev.h
+++ b/include/linux/blkdev.h
@@ -178,6 +178,8 @@ struct gendisk {
struct mutex open_mutex; /* open/close mutex */
unsigned open_partitions; /* number of open partitions */
+ /* reserved by partitions, bit N = stream N (0 unused), open_mutex */
+ DECLARE_BITMAP(write_streams_reserved, U8_MAX + 1);
struct backing_dev_info *bdi;
struct kobject queue_kobj; /* the queue/ directory */
--
2.43.0