* [PATCH v5 2/2] nvmet: add cgroup_id to charge namespace I/O to a cgroup
2026-09-26 6:33 [PATCH v5 0/2] nvmet: add cgroup_id to charge namespace I/O to a cgroup Peng Yu
2026-09-26 6:33 ` [PATCH v5 1/2] cgroup: export cgroup_e_css() Peng Yu
@ 2026-09-26 6:33 ` Peng Yu
1 sibling, 0 replies; 3+ messages in thread
From: Peng Yu @ 2026-09-26 6:33 UTC (permalink / raw)
To: Christoph Hellwig, Sagi Grimberg, Chaitanya Kulkarni
Cc: Tejun Heo, Johannes Weiner, Michal Koutný,
Josef Bacik, Jens Axboe, Maurizio Lombardi, cgroups, linux-block,
linux-nvme, linux-kernel, Peng Yu
Implementation:
* Add a `cgroup_id` attribute under the nvmet namespace folder, e.g.:
/sys/kernel/config/nvmet/subsystems/nqn.2026-09.io.test01:bdev/namespaces/1/cgroup_id
* We can write a cgroup id to it, then that cgroup will control
the IOs used by the namespace.
* Write 0 to clear it.
* The cgroup_id could be modified when the namespace is disabled.
* Support both block device backed namespaces and file backed namespaces.
* Only the direct io scenario is supported, enabling a namespace that
has both cgroup_id and buffered_io set fails with -EINVAL.
Change since v2:
* Fix the in ns->cgroup_path error free issue nvmet_ns_cgroup_path_store.
* Use nvmet_blkcg_begin/end in nvmet_bdev_zmgmt_send_work.
Change since v3:
* Use cgroup_id instead of cgroup_path.
Signed-off-by: Peng Yu <yupeng0921@gmail.com>
Assisted-by: Claude:claude-fable-5 [Claude Code]
Assisted-by: Claude:claude-opus-5 [Claude Code]
---
drivers/nvme/target/configfs.c | 47 ++++++++++++++++++++++++
drivers/nvme/target/core.c | 54 ++++++++++++++++++++++++++++
drivers/nvme/target/io-cmd-bdev.c | 18 +++++++++-
drivers/nvme/target/io-cmd-file.c | 21 +++++++++--
drivers/nvme/target/nvmet.h | 59 +++++++++++++++++++++++++++++++
drivers/nvme/target/zns.c | 5 +++
6 files changed, 201 insertions(+), 3 deletions(-)
diff --git a/drivers/nvme/target/configfs.c b/drivers/nvme/target/configfs.c
index 6286e38436dd..cef832303d72 100644
--- a/drivers/nvme/target/configfs.c
+++ b/drivers/nvme/target/configfs.c
@@ -561,6 +561,50 @@ static ssize_t nvmet_ns_device_path_store(struct config_item *item,
CONFIGFS_ATTR(nvmet_ns_, device_path);
+#ifdef CONFIG_BLK_CGROUP
+static ssize_t nvmet_ns_cgroup_id_show(struct config_item *item, char *page)
+{
+ struct nvmet_ns *ns = to_nvmet_ns(item);
+ struct nvmet_subsys *subsys = ns->subsys;
+ ssize_t ret;
+
+ mutex_lock(&subsys->lock);
+ ret = snprintf(page, PAGE_SIZE, "%llu\n", ns->cgroup_id);
+ mutex_unlock(&subsys->lock);
+ return ret;
+}
+
+static ssize_t nvmet_ns_cgroup_id_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct nvmet_ns *ns = to_nvmet_ns(item);
+ struct nvmet_subsys *subsys = ns->subsys;
+ u64 cgroup_id;
+ int ret;
+
+ ret = kstrtou64(page, 0, &cgroup_id);
+ if (ret)
+ return ret;
+
+ mutex_lock(&subsys->lock);
+
+ if (ns->enabled) {
+ ret = -EBUSY;
+ goto out_unlock;
+ }
+
+ /* Writing 0 clears the association. */
+ ns->cgroup_id = cgroup_id;
+ ret = count;
+
+out_unlock:
+ mutex_unlock(&subsys->lock);
+ return ret;
+}
+
+CONFIGFS_ATTR(nvmet_ns_, cgroup_id);
+#endif /* CONFIG_BLK_CGROUP */
+
#ifdef CONFIG_PCI_P2PDMA
static ssize_t nvmet_ns_p2pmem_show(struct config_item *item, char *page)
{
@@ -833,6 +877,9 @@ static struct configfs_attribute *nvmet_ns_attrs[] = {
&nvmet_ns_attr_buffered_io,
&nvmet_ns_attr_revalidate_size,
&nvmet_ns_attr_resv_enable,
+#ifdef CONFIG_BLK_CGROUP
+ &nvmet_ns_attr_cgroup_id,
+#endif
#ifdef CONFIG_PCI_P2PDMA
&nvmet_ns_attr_p2pmem,
#endif
diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c
index 43871a8f56ca..6d9d55eb11eb 100644
--- a/drivers/nvme/target/core.c
+++ b/drivers/nvme/target/core.c
@@ -4,6 +4,7 @@
* Copyright (c) 2015-2016 HGST, a Western Digital Company.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <linux/cgroup.h>
#include <linux/hex.h>
#include <linux/module.h>
#include <linux/random.h>
@@ -474,8 +475,57 @@ void nvmet_put_namespace(struct nvmet_ns *ns)
percpu_ref_put(&ns->ref);
}
+#ifdef CONFIG_BLK_CGROUP
+static int nvmet_blkcg_ns_enable(struct nvmet_ns *ns)
+{
+ struct cgroup *cgrp;
+
+ if (!ns->cgroup_id)
+ return 0;
+
+ /*
+ * Buffered writes will be handled by a separate thread,
+ * these IOs have no namespace/cgroup information at that time,
+ * so we don't support buffered io.
+ */
+ if (ns->buffered_io) {
+ pr_err("cgroup_id is not supported with buffered_io: %s\n",
+ ns->device_path);
+ return -EINVAL;
+ }
+
+ cgrp = cgroup_get_from_id(ns->cgroup_id);
+ if (IS_ERR(cgrp)) {
+ pr_err("failed to resolve cgroup id %llu: %ld\n",
+ ns->cgroup_id, PTR_ERR(cgrp));
+ return PTR_ERR(cgrp);
+ }
+
+ ns->cgrp = cgrp;
+ return 0;
+}
+
+static void nvmet_blkcg_ns_disable(struct nvmet_ns *ns)
+{
+ if (ns->cgrp) {
+ cgroup_put(ns->cgrp);
+ ns->cgrp = NULL;
+ }
+}
+#else
+static inline int nvmet_blkcg_ns_enable(struct nvmet_ns *ns)
+{
+ return 0;
+}
+
+static inline void nvmet_blkcg_ns_disable(struct nvmet_ns *ns)
+{
+}
+#endif /* CONFIG_BLK_CGROUP */
+
static void nvmet_ns_dev_disable(struct nvmet_ns *ns)
{
+ nvmet_blkcg_ns_disable(ns);
nvmet_bdev_ns_disable(ns);
nvmet_file_ns_disable(ns);
}
@@ -602,6 +652,10 @@ int nvmet_ns_enable(struct nvmet_ns *ns)
if (ret)
goto out_unlock;
+ ret = nvmet_blkcg_ns_enable(ns);
+ if (ret)
+ goto out_dev_disable;
+
ret = nvmet_p2pmem_ns_enable(ns);
if (ret)
goto out_dev_disable;
diff --git a/drivers/nvme/target/io-cmd-bdev.c b/drivers/nvme/target/io-cmd-bdev.c
index f2d9e8901df4..0014e5ef2853 100644
--- a/drivers/nvme/target/io-cmd-bdev.c
+++ b/drivers/nvme/target/io-cmd-bdev.c
@@ -297,6 +297,7 @@ static void nvmet_bdev_execute_rw(struct nvmet_req *req)
bio = bio_alloc(req->ns->bdev, bio_max_segs(sg_cnt), opf,
GFP_KERNEL);
}
+ nvmet_blkcg_set_bio(req->ns, bio);
bio->bi_iter.bi_sector = sector;
bio->bi_private = req;
bio->bi_end_io = nvmet_bio_done;
@@ -322,6 +323,7 @@ static void nvmet_bdev_execute_rw(struct nvmet_req *req)
bio = bio_alloc(req->ns->bdev, bio_max_segs(sg_cnt),
opf, GFP_KERNEL);
+ nvmet_blkcg_set_bio(req->ns, bio);
bio->bi_iter.bi_sector = sector;
bio_chain(bio, prev);
@@ -358,6 +360,7 @@ static void nvmet_bdev_execute_flush(struct nvmet_req *req)
bio_init(bio, req->ns->bdev, req->inline_bvec,
ARRAY_SIZE(req->inline_bvec), REQ_OP_WRITE | REQ_PREFLUSH);
+ nvmet_blkcg_set_bio(req->ns, bio);
bio->bi_private = req;
bio->bi_end_io = nvmet_bio_done;
@@ -366,10 +369,16 @@ static void nvmet_bdev_execute_flush(struct nvmet_req *req)
u16 nvmet_bdev_flush(struct nvmet_req *req)
{
+ bool associated;
+ int ret;
+
if (!bdev_write_cache(req->ns->bdev))
return 0;
- if (blkdev_issue_flush(req->ns->bdev))
+ associated = nvmet_blkcg_begin(req->ns);
+ ret = blkdev_issue_flush(req->ns->bdev);
+ nvmet_blkcg_end(associated);
+ if (ret)
return NVME_SC_INTERNAL | NVME_STATUS_DNR;
return 0;
}
@@ -380,9 +389,11 @@ static void nvmet_bdev_execute_discard(struct nvmet_req *req)
struct nvme_dsm_range range;
struct bio *bio = NULL;
sector_t nr_sects;
+ bool associated;
int i;
u16 status = NVME_SC_SUCCESS;
+ associated = nvmet_blkcg_begin(ns);
for (i = 0; i <= le32_to_cpu(req->cmd->dsm.nr); i++) {
status = nvmet_copy_from_sgl(req, i * sizeof(range), &range,
sizeof(range));
@@ -394,6 +405,7 @@ static void nvmet_bdev_execute_discard(struct nvmet_req *req)
nvmet_lba_to_sect(ns, range.slba), nr_sects,
GFP_KERNEL, &bio);
}
+ nvmet_blkcg_end(associated);
if (bio) {
bio->bi_private = req;
@@ -431,6 +443,7 @@ static void nvmet_bdev_execute_write_zeroes(struct nvmet_req *req)
struct bio *bio = NULL;
sector_t sector;
sector_t nr_sector;
+ bool associated;
int ret;
if (!nvmet_check_transfer_len(req, 0))
@@ -440,8 +453,11 @@ static void nvmet_bdev_execute_write_zeroes(struct nvmet_req *req)
nr_sector = (((sector_t)le16_to_cpu(write_zeroes->length) + 1) <<
(req->ns->blksize_shift - 9));
+ associated = nvmet_blkcg_begin(req->ns);
ret = __blkdev_issue_zeroout(req->ns->bdev, sector, nr_sector,
GFP_KERNEL, &bio, 0);
+ nvmet_blkcg_end(associated);
+
if (bio) {
bio->bi_private = req;
bio->bi_end_io = nvmet_bio_done;
diff --git a/drivers/nvme/target/io-cmd-file.c b/drivers/nvme/target/io-cmd-file.c
index 0b22d183f927..570e65259ad8 100644
--- a/drivers/nvme/target/io-cmd-file.c
+++ b/drivers/nvme/target/io-cmd-file.c
@@ -79,6 +79,8 @@ static ssize_t nvmet_file_submit_bvec(struct nvmet_req *req, loff_t pos,
struct kiocb *iocb = &req->f.iocb;
ssize_t (*call_iter)(struct kiocb *iocb, struct iov_iter *iter);
struct iov_iter iter;
+ bool associated;
+ ssize_t ret;
int rw;
if (req->cmd->rw.opcode == nvme_cmd_write) {
@@ -97,7 +99,10 @@ static ssize_t nvmet_file_submit_bvec(struct nvmet_req *req, loff_t pos,
iocb->ki_filp = req->ns->file;
iocb->ki_flags = ki_flags | iocb->ki_filp->f_iocb_flags;
- return call_iter(iocb, &iter);
+ associated = nvmet_blkcg_begin(req->ns);
+ ret = call_iter(iocb, &iter);
+ nvmet_blkcg_end(associated);
+ return ret;
}
static void nvmet_file_io_done(struct kiocb *iocb, long ret)
@@ -251,7 +256,13 @@ static void nvmet_file_execute_rw(struct nvmet_req *req)
u16 nvmet_file_flush(struct nvmet_req *req)
{
- return errno_to_nvme_status(req, vfs_fsync(req->ns->file, 1));
+ bool associated;
+ int ret;
+
+ associated = nvmet_blkcg_begin(req->ns);
+ ret = vfs_fsync(req->ns->file, 1);
+ nvmet_blkcg_end(associated);
+ return errno_to_nvme_status(req, ret);
}
static void nvmet_file_flush_work(struct work_struct *w)
@@ -274,10 +285,12 @@ static void nvmet_file_execute_discard(struct nvmet_req *req)
int mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE;
struct nvme_dsm_range range;
loff_t offset, len;
+ bool associated;
u16 status = 0;
int ret;
int i;
+ associated = nvmet_blkcg_begin(req->ns);
for (i = 0; i <= le32_to_cpu(req->cmd->dsm.nr); i++) {
status = nvmet_copy_from_sgl(req, i * sizeof(range), &range,
sizeof(range));
@@ -300,6 +313,7 @@ static void nvmet_file_execute_discard(struct nvmet_req *req)
break;
}
}
+ nvmet_blkcg_end(associated);
nvmet_req_complete(req, status);
}
@@ -336,6 +350,7 @@ static void nvmet_file_write_zeroes_work(struct work_struct *w)
int mode = FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE;
loff_t offset;
loff_t len;
+ bool associated;
int ret;
offset = le64_to_cpu(write_zeroes->slba) << req->ns->blksize_shift;
@@ -347,7 +362,9 @@ static void nvmet_file_write_zeroes_work(struct work_struct *w)
return;
}
+ associated = nvmet_blkcg_begin(req->ns);
ret = vfs_fallocate(req->ns->file, mode, offset, len);
+ nvmet_blkcg_end(associated);
nvmet_req_complete(req, ret < 0 ? errno_to_nvme_status(req, ret) : 0);
}
diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h
index dbda55895f4f..494091d7c88b 100644
--- a/drivers/nvme/target/nvmet.h
+++ b/drivers/nvme/target/nvmet.h
@@ -21,6 +21,8 @@
#include <linux/radix-tree.h>
#include <linux/t10-pi.h>
#include <linux/kfifo.h>
+#include <linux/kthread.h>
+#include <linux/cgroup.h>
#define NVMET_DEFAULT_VS NVME_VS(2, 1, 0)
@@ -115,6 +117,16 @@ struct nvmet_ns {
struct nvmet_subsys *subsys;
const char *device_path;
+#ifdef CONFIG_BLK_CGROUP
+ u64 cgroup_id;
+ /*
+ * Resolved from ->cgroup_id when the namespace is enabled and
+ * released when it is disabled, so it has the same lifetime and
+ * visibility rules as ->bdev and ->file.
+ */
+ struct cgroup *cgrp;
+#endif
+
struct config_group device_group;
struct config_group group;
@@ -732,6 +744,53 @@ void nvmet_bdev_execute_zone_mgmt_recv(struct nvmet_req *req);
void nvmet_bdev_execute_zone_mgmt_send(struct nvmet_req *req);
void nvmet_bdev_execute_zone_append(struct nvmet_req *req);
+#ifdef CONFIG_BLK_CGROUP
+static inline void nvmet_blkcg_set_bio(struct nvmet_ns *ns, struct bio *bio)
+{
+ struct cgroup_subsys_state *css;
+
+ if (!ns->cgrp)
+ return;
+
+ rcu_read_lock();
+ css = cgroup_e_css(ns->cgrp, &io_cgrp_subsys);
+ bio_associate_blkg_from_css(bio, css);
+ rcu_read_unlock();
+}
+
+static inline bool nvmet_blkcg_begin(struct nvmet_ns *ns)
+{
+ struct cgroup_subsys_state *css;
+
+ if (!ns->cgrp || !in_task() || !(current->flags & PF_KTHREAD))
+ return false;
+
+ css = cgroup_get_e_css(ns->cgrp, &io_cgrp_subsys);
+ kthread_associate_blkcg(css);
+ css_put(css);
+ return true;
+}
+
+static inline void nvmet_blkcg_end(bool associated)
+{
+ if (associated)
+ kthread_associate_blkcg(NULL);
+}
+#else
+static inline void nvmet_blkcg_set_bio(struct nvmet_ns *ns, struct bio *bio)
+{
+}
+
+static inline bool nvmet_blkcg_begin(struct nvmet_ns *ns)
+{
+ return false;
+}
+
+static inline void nvmet_blkcg_end(bool associated)
+{
+}
+#endif /* CONFIG_BLK_CGROUP */
+
static inline u32 nvmet_rw_data_len(struct nvmet_req *req)
{
return ((u32)le16_to_cpu(req->cmd->rw.length) + 1) <<
diff --git a/drivers/nvme/target/zns.c b/drivers/nvme/target/zns.c
index 23a17c02abee..a9836d44f5e6 100644
--- a/drivers/nvme/target/zns.c
+++ b/drivers/nvme/target/zns.c
@@ -480,8 +480,11 @@ static void nvmet_bdev_zmgmt_send_work(struct work_struct *w)
struct block_device *bdev = req->ns->bdev;
sector_t zone_sectors = bdev_zone_sectors(bdev);
u16 status = NVME_SC_SUCCESS;
+ bool associated;
int ret;
+ associated = nvmet_blkcg_begin(req->ns);
+
if (op == REQ_OP_LAST) {
req->error_loc = offsetof(struct nvme_zone_mgmt_send_cmd, zsa);
status = NVME_SC_ZONE_INVALID_TRANSITION | NVME_STATUS_DNR;
@@ -511,6 +514,7 @@ static void nvmet_bdev_zmgmt_send_work(struct work_struct *w)
status = blkdev_zone_mgmt_errno_to_nvme_status(ret);
out:
+ nvmet_blkcg_end(associated);
nvmet_req_complete(req, status);
}
@@ -580,6 +584,7 @@ void nvmet_bdev_execute_zone_append(struct nvmet_req *req)
bio = bio_alloc(req->ns->bdev, req->sg_cnt, opf, GFP_KERNEL);
}
+ nvmet_blkcg_set_bio(req->ns, bio);
bio->bi_end_io = nvmet_bdev_zone_append_bio_done;
bio->bi_iter.bi_sector = sect;
bio->bi_private = req;
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread