From 72f1c59c44688668c55d0891101553da09723301 Mon Sep 17 00:00:00 2001 From: deepin-wm Date: Thu, 18 Jun 2026 19:23:56 +0800 Subject: [PATCH 1/8] cgroup/dmem: Add dmem cgroup controller and page_counter_calculate_protection Backport the dmem cgroup controller from kernel 6.14 and the page_counter_calculate_protection function from kernel 6.11. The dmem cgroup controller allows tracking and limiting device memory (such as GPU VRAM) consumption via cgroups. It uses the same min/low/max semantics as the memory cgroup. page_counter_calculate_protection is needed by dmem to calculate effective memory protection values. This function was factored out of memcontrol.c in kernel 6.11. --- Documentation/admin-guide/cgroup-v2.rst | 16 + include/linux/cgroup_dmem.h | 66 ++ include/linux/cgroup_subsys.h | 4 + include/linux/page_counter.h | 4 + init/Kconfig | 12 + kernel/cgroup/Makefile | 1 + kernel/cgroup/dmem.c | 829 ++++++++++++++++++++++++ mm/page_counter.c | 174 +++++ 8 files changed, 1106 insertions(+) create mode 100644 include/linux/cgroup_dmem.h create mode 100644 kernel/cgroup/dmem.c diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst index 606b2e0eac4b1..8e1fa6babfce0 100644 --- a/Documentation/admin-guide/cgroup-v2.rst +++ b/Documentation/admin-guide/cgroup-v2.rst @@ -2524,6 +2524,22 @@ Miscellaneous controller provides 3 interface files. If two misc resources (res_ The number of times the cgroup's resource usage was about to go over the max boundary. +DMEM +---- + +The "dmem" controller regulates device memory (such as GPU VRAM) +consumption. The controller is similar to the memory controller +but tracks device-specific memory rather than system RAM. + +dmem.min, dmem.low, and dmem.max +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +dmem.min and dmem.low define minimum and best-effort protection +for device memory, similar to memory.min and memory.low for RAM. +dmem.max sets a hard limit on device memory usage. + +The interface is available at /sys/fs/cgroup//dmem.{min,low,max}. + Migration and Ownership ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/include/linux/cgroup_dmem.h b/include/linux/cgroup_dmem.h new file mode 100644 index 0000000000000..dd4869f1d736e --- /dev/null +++ b/include/linux/cgroup_dmem.h @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2023-2024 Intel Corporation + */ + +#ifndef _CGROUP_DMEM_H +#define _CGROUP_DMEM_H + +#include +#include + +struct dmem_cgroup_pool_state; + +/* Opaque definition of a cgroup region, used internally */ +struct dmem_cgroup_region; + +#if IS_ENABLED(CONFIG_CGROUP_DMEM) +struct dmem_cgroup_region *dmem_cgroup_register_region(u64 size, const char *name_fmt, ...) __printf(2,3); +void dmem_cgroup_unregister_region(struct dmem_cgroup_region *region); +int dmem_cgroup_try_charge(struct dmem_cgroup_region *region, u64 size, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool); +void dmem_cgroup_uncharge(struct dmem_cgroup_pool_state *pool, u64 size); +bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, + struct dmem_cgroup_pool_state *test_pool, + bool ignore_low, bool *ret_hit_low); + +void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool); +#else +static inline __printf(2,3) struct dmem_cgroup_region * +dmem_cgroup_register_region(u64 size, const char *name_fmt, ...) +{ + return NULL; +} + +static inline void dmem_cgroup_unregister_region(struct dmem_cgroup_region *region) +{ } + +static inline int dmem_cgroup_try_charge(struct dmem_cgroup_region *region, u64 size, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool) +{ + *ret_pool = NULL; + + if (ret_limit_pool) + *ret_limit_pool = NULL; + + return 0; +} + +static inline void dmem_cgroup_uncharge(struct dmem_cgroup_pool_state *pool, u64 size) +{ } + +static inline +bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, + struct dmem_cgroup_pool_state *test_pool, + bool ignore_low, bool *ret_hit_low) +{ + return true; +} + +static inline void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) +{ } + +#endif +#endif /* _CGROUP_DMEM_H */ diff --git a/include/linux/cgroup_subsys.h b/include/linux/cgroup_subsys.h index 4452354872307..3fd0bcbf30803 100644 --- a/include/linux/cgroup_subsys.h +++ b/include/linux/cgroup_subsys.h @@ -65,6 +65,10 @@ SUBSYS(rdma) SUBSYS(misc) #endif +#if IS_ENABLED(CONFIG_CGROUP_DMEM) +SUBSYS(dmem) +#endif + /* * The following subsystems are not supported on the default hierarchy. */ diff --git a/include/linux/page_counter.h b/include/linux/page_counter.h index c141ea9a95ef8..e24e918146382 100644 --- a/include/linux/page_counter.h +++ b/include/linux/page_counter.h @@ -81,4 +81,8 @@ static inline void page_counter_reset_watermark(struct page_counter *counter) counter->watermark = page_counter_read(counter); } +void page_counter_calculate_protection(struct page_counter *root, + struct page_counter *counter, + bool recursive_protection); + #endif /* _LINUX_PAGE_COUNTER_H */ diff --git a/init/Kconfig b/init/Kconfig index 8cd1dbbbf0e10..bc22f1f5ded47 100644 --- a/init/Kconfig +++ b/init/Kconfig @@ -1179,6 +1179,18 @@ config CGROUP_MISC For more information, please check misc cgroup section in /Documentation/admin-guide/cgroup-v2.rst. +config CGROUP_DMEM + bool "Device memory controller" + depends on MEMCG + default n + help + Select this option to enable tracking of device memory usage + (e.g. GPU VRAM) in cgroups. Device memory can be limited + using dmem.max, and protection can be set using dmem.low + and dmem.min. + + If unsure, say N. + config CGROUP_DEBUG bool "Debug controller" default n diff --git a/kernel/cgroup/Makefile b/kernel/cgroup/Makefile index 12f8457ad1f90..1961779f958f7 100644 --- a/kernel/cgroup/Makefile +++ b/kernel/cgroup/Makefile @@ -6,4 +6,5 @@ obj-$(CONFIG_CGROUP_PIDS) += pids.o obj-$(CONFIG_CGROUP_RDMA) += rdma.o obj-$(CONFIG_CPUSETS) += cpuset.o obj-$(CONFIG_CGROUP_MISC) += misc.o +obj-$(CONFIG_CGROUP_DMEM) += dmem.o obj-$(CONFIG_CGROUP_DEBUG) += debug.o diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c new file mode 100644 index 0000000000000..10b63433f0573 --- /dev/null +++ b/kernel/cgroup/dmem.c @@ -0,0 +1,829 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2023-2024 Intel Corporation (Maarten Lankhorst ) + * Copyright 2024 Red Hat (Maxime Ripard ) + * Partially based on the rdma and misc controllers, which bear the following copyrights: + * + * Copyright 2020 Google LLC + * Copyright (C) 2016 Parav Pandit + */ + +#include +#include +#include +#include +#include +#include +#include + +struct dmem_cgroup_region { + /** + * @ref: References keeping the region alive. + * Keeps the region reference alive after a succesful RCU lookup. + */ + struct kref ref; + + /** @rcu: RCU head for freeing */ + struct rcu_head rcu; + + /** + * @region_node: Linked into &dmem_cgroup_regions list. + * Protected by RCU and global spinlock. + */ + struct list_head region_node; + + /** + * @pools: List of pools linked to this region. + * Protected by global spinlock only + */ + struct list_head pools; + + /** @size: Size of region, in bytes */ + u64 size; + + /** @name: Name describing the node, set by dmem_cgroup_register_region */ + char *name; + + /** + * @unregistered: Whether the region is unregistered by its caller. + * No new pools should be added to the region afterwards. + */ + bool unregistered; +}; + +struct dmemcg_state { + struct cgroup_subsys_state css; + + struct list_head pools; +}; + +struct dmem_cgroup_pool_state { + struct dmem_cgroup_region *region; + struct dmemcg_state *cs; + + /* css node, RCU protected against region teardown */ + struct list_head css_node; + + /* dev node, no RCU protection required */ + struct list_head region_node; + + struct rcu_head rcu; + + struct page_counter cnt; + + bool inited; +}; + +/* + * 3 operations require locking protection: + * - Registering and unregistering region to/from list, requires global lock. + * - Adding a dmem_cgroup_pool_state to a CSS, removing when CSS is freed. + * - Adding a dmem_cgroup_pool_state to a region list. + * + * Since for the most common operations RCU provides enough protection, I + * do not think more granular locking makes sense. Most protection is offered + * by RCU and the lockless operating page_counter. + */ +static DEFINE_SPINLOCK(dmemcg_lock); +static LIST_HEAD(dmem_cgroup_regions); + +static inline struct dmemcg_state * +css_to_dmemcs(struct cgroup_subsys_state *css) +{ + return container_of(css, struct dmemcg_state, css); +} + +static inline struct dmemcg_state *get_current_dmemcs(void) +{ + return css_to_dmemcs(task_get_css(current, dmem_cgrp_id)); +} + +static struct dmemcg_state *parent_dmemcs(struct dmemcg_state *cg) +{ + return cg->css.parent ? css_to_dmemcs(cg->css.parent) : NULL; +} + +static void free_cg_pool(struct dmem_cgroup_pool_state *pool) +{ + list_del(&pool->region_node); + kfree(pool); +} + +static void +set_resource_min(struct dmem_cgroup_pool_state *pool, u64 val) +{ + page_counter_set_min(&pool->cnt, val); +} + +static void +set_resource_low(struct dmem_cgroup_pool_state *pool, u64 val) +{ + page_counter_set_low(&pool->cnt, val); +} + +static void +set_resource_max(struct dmem_cgroup_pool_state *pool, u64 val) +{ + page_counter_set_max(&pool->cnt, val); +} + +static u64 get_resource_low(struct dmem_cgroup_pool_state *pool) +{ + return pool ? READ_ONCE(pool->cnt.low) : 0; +} + +static u64 get_resource_min(struct dmem_cgroup_pool_state *pool) +{ + return pool ? READ_ONCE(pool->cnt.min) : 0; +} + +static u64 get_resource_max(struct dmem_cgroup_pool_state *pool) +{ + return pool ? READ_ONCE(pool->cnt.max) : PAGE_COUNTER_MAX; +} + +static u64 get_resource_current(struct dmem_cgroup_pool_state *pool) +{ + return pool ? page_counter_read(&pool->cnt) : 0; +} + +static void reset_all_resource_limits(struct dmem_cgroup_pool_state *rpool) +{ + set_resource_min(rpool, 0); + set_resource_low(rpool, 0); + set_resource_max(rpool, PAGE_COUNTER_MAX); +} + +static void dmemcs_offline(struct cgroup_subsys_state *css) +{ + struct dmemcg_state *dmemcs = css_to_dmemcs(css); + struct dmem_cgroup_pool_state *pool; + + rcu_read_lock(); + list_for_each_entry_rcu(pool, &dmemcs->pools, css_node) + reset_all_resource_limits(pool); + rcu_read_unlock(); +} + +static void dmemcs_free(struct cgroup_subsys_state *css) +{ + struct dmemcg_state *dmemcs = css_to_dmemcs(css); + struct dmem_cgroup_pool_state *pool, *next; + + spin_lock(&dmemcg_lock); + list_for_each_entry_safe(pool, next, &dmemcs->pools, css_node) { + /* + *The pool is dead and all references are 0, + * no need for RCU protection with list_del_rcu or freeing. + */ + list_del(&pool->css_node); + free_cg_pool(pool); + } + spin_unlock(&dmemcg_lock); + + kfree(dmemcs); +} + +static struct cgroup_subsys_state * +dmemcs_alloc(struct cgroup_subsys_state *parent_css) +{ + struct dmemcg_state *dmemcs = kzalloc(sizeof(*dmemcs), GFP_KERNEL); + if (!dmemcs) + return ERR_PTR(-ENOMEM); + + INIT_LIST_HEAD(&dmemcs->pools); + return &dmemcs->css; +} + +static struct dmem_cgroup_pool_state * +find_cg_pool_locked(struct dmemcg_state *dmemcs, struct dmem_cgroup_region *region) +{ + struct dmem_cgroup_pool_state *pool; + + list_for_each_entry_rcu(pool, &dmemcs->pools, css_node, spin_is_locked(&dmemcg_lock)) + if (pool->region == region) + return pool; + + return NULL; +} + +static struct dmem_cgroup_pool_state *pool_parent(struct dmem_cgroup_pool_state *pool) +{ + if (!pool->cnt.parent) + return NULL; + + return container_of(pool->cnt.parent, typeof(*pool), cnt); +} + +static void +dmem_cgroup_calculate_protection(struct dmem_cgroup_pool_state *limit_pool, + struct dmem_cgroup_pool_state *test_pool) +{ + struct page_counter *climit; + struct cgroup_subsys_state *css; + struct dmemcg_state *dmemcg_iter; + struct dmem_cgroup_pool_state *pool, *found_pool; + + climit = &limit_pool->cnt; + + rcu_read_lock(); + + css_for_each_descendant_pre(css, &limit_pool->cs->css) { + dmemcg_iter = container_of(css, struct dmemcg_state, css); + found_pool = NULL; + + list_for_each_entry_rcu(pool, &dmemcg_iter->pools, css_node) { + if (pool->region == limit_pool->region) { + found_pool = pool; + break; + } + } + if (!found_pool) + continue; + + page_counter_calculate_protection( + climit, &found_pool->cnt, true); + + if (found_pool == test_pool) + break; + } + rcu_read_unlock(); +} + +/** + * dmem_cgroup_state_evict_valuable() - Check if we should evict from test_pool + * @limit_pool: The pool for which we hit limits + * @test_pool: The pool for which to test + * @ignore_low: Whether we have to respect low watermarks. + * @ret_hit_low: Pointer to whether it makes sense to consider low watermark. + * + * This function returns true if we can evict from @test_pool, false if not. + * When returning false and @ignore_low is false, @ret_hit_low may + * be set to true to indicate this function can be retried with @ignore_low + * set to true. + * + * Return: bool + */ +bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, + struct dmem_cgroup_pool_state *test_pool, + bool ignore_low, bool *ret_hit_low) +{ + struct dmem_cgroup_pool_state *pool = test_pool; + struct page_counter *ctest; + u64 used, min, low; + + /* Can always evict from current pool, despite limits */ + if (limit_pool == test_pool) + return true; + + if (limit_pool) { + if (!parent_dmemcs(limit_pool->cs)) + return true; + + for (pool = test_pool; pool && limit_pool != pool; pool = pool_parent(pool)) + {} + + if (!pool) + return false; + } else { + /* + * If there is no cgroup limiting memory usage, use the root + * cgroup instead for limit calculations. + */ + for (limit_pool = test_pool; pool_parent(limit_pool); limit_pool = pool_parent(limit_pool)) + {} + } + + ctest = &test_pool->cnt; + + dmem_cgroup_calculate_protection(limit_pool, test_pool); + + used = page_counter_read(ctest); + min = READ_ONCE(ctest->emin); + + if (used <= min) + return false; + + if (!ignore_low) { + low = READ_ONCE(ctest->elow); + if (used > low) + return true; + + *ret_hit_low = true; + return false; + } + return true; +} +EXPORT_SYMBOL_GPL(dmem_cgroup_state_evict_valuable); + +static struct dmem_cgroup_pool_state * +alloc_pool_single(struct dmemcg_state *dmemcs, struct dmem_cgroup_region *region, + struct dmem_cgroup_pool_state **allocpool) +{ + struct dmemcg_state *parent = parent_dmemcs(dmemcs); + struct dmem_cgroup_pool_state *pool, *ppool = NULL; + + if (!*allocpool) { + pool = kzalloc(sizeof(*pool), GFP_NOWAIT); + if (!pool) + return ERR_PTR(-ENOMEM); + } else { + pool = *allocpool; + *allocpool = NULL; + } + + pool->region = region; + pool->cs = dmemcs; + + if (parent) + ppool = find_cg_pool_locked(parent, region); + + page_counter_init(&pool->cnt, + ppool ? &ppool->cnt : NULL, true); + reset_all_resource_limits(pool); + + list_add_tail_rcu(&pool->css_node, &dmemcs->pools); + list_add_tail(&pool->region_node, ®ion->pools); + + if (!parent) + pool->inited = true; + else + pool->inited = ppool ? ppool->inited : false; + return pool; +} + +static struct dmem_cgroup_pool_state * +get_cg_pool_locked(struct dmemcg_state *dmemcs, struct dmem_cgroup_region *region, + struct dmem_cgroup_pool_state **allocpool) +{ + struct dmem_cgroup_pool_state *pool, *ppool, *retpool; + struct dmemcg_state *p, *pp; + + /* + * Recursively create pool, we may not initialize yet on + * recursion, this is done as a separate step. + */ + for (p = dmemcs; p; p = parent_dmemcs(p)) { + pool = find_cg_pool_locked(p, region); + if (!pool) + pool = alloc_pool_single(p, region, allocpool); + + if (IS_ERR(pool)) + return pool; + + if (p == dmemcs && pool->inited) + return pool; + + if (pool->inited) + break; + } + + retpool = pool = find_cg_pool_locked(dmemcs, region); + for (p = dmemcs, pp = parent_dmemcs(dmemcs); pp; p = pp, pp = parent_dmemcs(p)) { + if (pool->inited) + break; + + /* ppool was created if it didn't exist by above loop. */ + ppool = find_cg_pool_locked(pp, region); + + /* Fix up parent links, mark as inited. */ + pool->cnt.parent = &ppool->cnt; + pool->inited = true; + + pool = ppool; + } + + return retpool; +} + +static void dmemcg_free_rcu(struct rcu_head *rcu) +{ + struct dmem_cgroup_region *region = container_of(rcu, typeof(*region), rcu); + struct dmem_cgroup_pool_state *pool, *next; + + list_for_each_entry_safe(pool, next, ®ion->pools, region_node) + free_cg_pool(pool); + kfree(region->name); + kfree(region); +} + +static void dmemcg_free_region(struct kref *ref) +{ + struct dmem_cgroup_region *cgregion = container_of(ref, typeof(*cgregion), ref); + + call_rcu(&cgregion->rcu, dmemcg_free_rcu); +} + +/** + * dmem_cgroup_unregister_region() - Unregister a previously registered region. + * @region: The region to unregister. + * + * This function undoes dmem_cgroup_register_region. + */ +void dmem_cgroup_unregister_region(struct dmem_cgroup_region *region) +{ + struct list_head *entry; + + if (!region) + return; + + spin_lock(&dmemcg_lock); + + /* Remove from global region list */ + list_del_rcu(®ion->region_node); + + list_for_each_rcu(entry, ®ion->pools) { + struct dmem_cgroup_pool_state *pool = + container_of(entry, typeof(*pool), region_node); + + list_del_rcu(&pool->css_node); + } + + /* + * Ensure any RCU based lookups fail. Additionally, + * no new pools should be added to the dead region + * by get_cg_pool_unlocked. + */ + region->unregistered = true; + spin_unlock(&dmemcg_lock); + + kref_put(®ion->ref, dmemcg_free_region); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_unregister_region); + +/** + * dmem_cgroup_register_region() - Register a regions for dev cgroup. + * @size: Size of region to register, in bytes. + * @fmt: Region parameters to register + * + * This function registers a node in the dmem cgroup with the + * name given. After calling this function, the region can be + * used for allocations. + * + * Return: NULL or a struct on success, PTR_ERR on failure. + */ +struct dmem_cgroup_region *dmem_cgroup_register_region(u64 size, const char *fmt, ...) +{ + struct dmem_cgroup_region *ret; + char *region_name; + va_list ap; + + if (!size) + return NULL; + + va_start(ap, fmt); + region_name = kvasprintf(GFP_KERNEL, fmt, ap); + va_end(ap); + if (!region_name) + return ERR_PTR(-ENOMEM); + + ret = kzalloc(sizeof(*ret), GFP_KERNEL); + if (!ret) { + kfree(region_name); + return ERR_PTR(-ENOMEM); + } + + INIT_LIST_HEAD(&ret->pools); + ret->name = region_name; + ret->size = size; + kref_init(&ret->ref); + + spin_lock(&dmemcg_lock); + list_add_tail_rcu(&ret->region_node, &dmem_cgroup_regions); + spin_unlock(&dmemcg_lock); + + return ret; +} +EXPORT_SYMBOL_GPL(dmem_cgroup_register_region); + +static struct dmem_cgroup_region *dmemcg_get_region_by_name(const char *name) +{ + struct dmem_cgroup_region *region; + + list_for_each_entry_rcu(region, &dmem_cgroup_regions, region_node, spin_is_locked(&dmemcg_lock)) + if (!strcmp(name, region->name) && + kref_get_unless_zero(®ion->ref)) + return region; + + return NULL; +} + +/** + * dmem_cgroup_pool_state_put() - Drop a reference to a dmem_cgroup_pool_state + * @pool: &dmem_cgroup_pool_state + * + * Called to drop a reference to the limiting pool returned by + * dmem_cgroup_try_charge(). + */ +void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) +{ + if (pool) + css_put(&pool->cs->css); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_pool_state_put); + +static struct dmem_cgroup_pool_state * +get_cg_pool_unlocked(struct dmemcg_state *cg, struct dmem_cgroup_region *region) +{ + struct dmem_cgroup_pool_state *pool, *allocpool = NULL; + + /* fastpath lookup? */ + rcu_read_lock(); + pool = find_cg_pool_locked(cg, region); + if (pool && !READ_ONCE(pool->inited)) + pool = NULL; + rcu_read_unlock(); + + while (!pool) { + spin_lock(&dmemcg_lock); + if (!region->unregistered) + pool = get_cg_pool_locked(cg, region, &allocpool); + else + pool = ERR_PTR(-ENODEV); + spin_unlock(&dmemcg_lock); + + if (pool == ERR_PTR(-ENOMEM)) { + pool = NULL; + if (WARN_ON(allocpool)) + continue; + + allocpool = kzalloc(sizeof(*allocpool), GFP_KERNEL); + if (allocpool) { + pool = NULL; + continue; + } + } + } + + kfree(allocpool); + return pool; +} + +/** + * dmem_cgroup_uncharge() - Uncharge a pool. + * @pool: Pool to uncharge. + * @size: Size to uncharge. + * + * Undoes the effects of dmem_cgroup_try_charge. + * Must be called with the returned pool as argument, + * and same @index and @size. + */ +void dmem_cgroup_uncharge(struct dmem_cgroup_pool_state *pool, u64 size) +{ + if (!pool) + return; + + page_counter_uncharge(&pool->cnt, size); + css_put(&pool->cs->css); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_uncharge); + +/** + * dmem_cgroup_try_charge() - Try charging a new allocation to a region. + * @region: dmem region to charge + * @size: Size (in bytes) to charge. + * @ret_pool: On succesfull allocation, the pool that is charged. + * @ret_limit_pool: On a failed allocation, the limiting pool. + * + * This function charges the @region region for a size of @size bytes. + * + * If the function succeeds, @ret_pool is set, which must be passed to + * dmem_cgroup_uncharge() when undoing the allocation. + * + * When this function fails with -EAGAIN and @ret_limit_pool is non-null, it + * will be set to the pool for which the limit is hit. This can be used for + * eviction as argument to dmem_cgroup_evict_valuable(). This reference must be freed + * with @dmem_cgroup_pool_state_put(). + * + * Return: 0 on success, -EAGAIN on hitting a limit, or a negative errno on failure. + */ +int dmem_cgroup_try_charge(struct dmem_cgroup_region *region, u64 size, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool) +{ + struct dmemcg_state *cg; + struct dmem_cgroup_pool_state *pool; + struct page_counter *fail; + int ret; + + *ret_pool = NULL; + if (ret_limit_pool) + *ret_limit_pool = NULL; + + /* + * hold on to css, as cgroup can be removed but resource + * accounting happens on css. + */ + cg = get_current_dmemcs(); + + pool = get_cg_pool_unlocked(cg, region); + if (IS_ERR(pool)) { + ret = PTR_ERR(pool); + goto err; + } + + if (!page_counter_try_charge(&pool->cnt, size, &fail)) { + if (ret_limit_pool) { + *ret_limit_pool = container_of(fail, struct dmem_cgroup_pool_state, cnt); + css_get(&(*ret_limit_pool)->cs->css); + } + ret = -EAGAIN; + goto err; + } + + /* On success, reference from get_current_dmemcs is transferred to *ret_pool */ + *ret_pool = pool; + return 0; + +err: + css_put(&cg->css); + return ret; +} +EXPORT_SYMBOL_GPL(dmem_cgroup_try_charge); + +static int dmem_cgroup_region_capacity_show(struct seq_file *sf, void *v) +{ + struct dmem_cgroup_region *region; + + rcu_read_lock(); + list_for_each_entry_rcu(region, &dmem_cgroup_regions, region_node) { + seq_puts(sf, region->name); + seq_printf(sf, " %llu\n", region->size); + } + rcu_read_unlock(); + return 0; +} + +static int dmemcg_parse_limit(char *options, struct dmem_cgroup_region *region, + u64 *new_limit) +{ + char *end; + + if (!strcmp(options, "max")) { + *new_limit = PAGE_COUNTER_MAX; + return 0; + } + + *new_limit = memparse(options, &end); + if (*end != '\0') + return -EINVAL; + + return 0; +} + +static ssize_t dmemcg_limit_write(struct kernfs_open_file *of, + char *buf, size_t nbytes, loff_t off, + void (*apply)(struct dmem_cgroup_pool_state *, u64)) +{ + struct dmemcg_state *dmemcs = css_to_dmemcs(of_css(of)); + int err = 0; + + while (buf && !err) { + struct dmem_cgroup_pool_state *pool = NULL; + char *options, *region_name; + struct dmem_cgroup_region *region; + u64 new_limit; + + options = buf; + buf = strchr(buf, '\n'); + if (buf) + *buf++ = '\0'; + + options = strstrip(options); + + /* eat empty lines */ + if (!options[0]) + continue; + + region_name = strsep(&options, " \t"); + if (!region_name[0]) + continue; + + rcu_read_lock(); + region = dmemcg_get_region_by_name(region_name); + rcu_read_unlock(); + + if (!region) + return -EINVAL; + + err = dmemcg_parse_limit(options, region, &new_limit); + if (err < 0) + goto out_put; + + pool = get_cg_pool_unlocked(dmemcs, region); + if (IS_ERR(pool)) { + err = PTR_ERR(pool); + goto out_put; + } + + /* And commit */ + apply(pool, new_limit); + +out_put: + kref_put(®ion->ref, dmemcg_free_region); + } + + + return err ?: nbytes; +} + +static int dmemcg_limit_show(struct seq_file *sf, void *v, + u64 (*fn)(struct dmem_cgroup_pool_state *)) +{ + struct dmemcg_state *dmemcs = css_to_dmemcs(seq_css(sf)); + struct dmem_cgroup_region *region; + + rcu_read_lock(); + list_for_each_entry_rcu(region, &dmem_cgroup_regions, region_node) { + struct dmem_cgroup_pool_state *pool = find_cg_pool_locked(dmemcs, region); + u64 val; + + seq_puts(sf, region->name); + + val = fn(pool); + if (val < PAGE_COUNTER_MAX) + seq_printf(sf, " %lld\n", val); + else + seq_puts(sf, " max\n"); + } + rcu_read_unlock(); + + return 0; +} + +static int dmem_cgroup_region_current_show(struct seq_file *sf, void *v) +{ + return dmemcg_limit_show(sf, v, get_resource_current); +} + +static int dmem_cgroup_region_min_show(struct seq_file *sf, void *v) +{ + return dmemcg_limit_show(sf, v, get_resource_min); +} + +static ssize_t dmem_cgroup_region_min_write(struct kernfs_open_file *of, + char *buf, size_t nbytes, loff_t off) +{ + return dmemcg_limit_write(of, buf, nbytes, off, set_resource_min); +} + +static int dmem_cgroup_region_low_show(struct seq_file *sf, void *v) +{ + return dmemcg_limit_show(sf, v, get_resource_low); +} + +static ssize_t dmem_cgroup_region_low_write(struct kernfs_open_file *of, + char *buf, size_t nbytes, loff_t off) +{ + return dmemcg_limit_write(of, buf, nbytes, off, set_resource_low); +} + +static int dmem_cgroup_region_max_show(struct seq_file *sf, void *v) +{ + return dmemcg_limit_show(sf, v, get_resource_max); +} + +static ssize_t dmem_cgroup_region_max_write(struct kernfs_open_file *of, + char *buf, size_t nbytes, loff_t off) +{ + return dmemcg_limit_write(of, buf, nbytes, off, set_resource_max); +} + +static struct cftype files[] = { + { + .name = "capacity", + .seq_show = dmem_cgroup_region_capacity_show, + .flags = CFTYPE_ONLY_ON_ROOT, + }, + { + .name = "current", + .seq_show = dmem_cgroup_region_current_show, + }, + { + .name = "min", + .write = dmem_cgroup_region_min_write, + .seq_show = dmem_cgroup_region_min_show, + .flags = CFTYPE_NOT_ON_ROOT, + }, + { + .name = "low", + .write = dmem_cgroup_region_low_write, + .seq_show = dmem_cgroup_region_low_show, + .flags = CFTYPE_NOT_ON_ROOT, + }, + { + .name = "max", + .write = dmem_cgroup_region_max_write, + .seq_show = dmem_cgroup_region_max_show, + .flags = CFTYPE_NOT_ON_ROOT, + }, + { } /* Zero entry terminates. */ +}; + +struct cgroup_subsys dmem_cgrp_subsys = { + .css_alloc = dmemcs_alloc, + .css_free = dmemcs_free, + .css_offline = dmemcs_offline, + .legacy_cftypes = files, + .dfl_cftypes = files, +}; diff --git a/mm/page_counter.c b/mm/page_counter.c index db20d6452b715..74f5c91877861 100644 --- a/mm/page_counter.c +++ b/mm/page_counter.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -262,3 +263,176 @@ int page_counter_memparse(const char *buf, const char *max, return 0; } + +/* + * This function calculates an individual page counter's effective + * protection which is derived from its own memory.min/low, its + * parent's and siblings' settings, as well as the actual memory + * distribution in the tree. + * + * The following rules apply to the effective protection values: + * + * 1. At the first level of reclaim, effective protection is equal to + * the declared protection in memory.min and memory.low. + * + * 2. To enable safe delegation of the protection configuration, at + * subsequent levels the effective protection is capped to the + * parent's effective protection. + * + * 3. To make complex and dynamic subtrees easier to configure, the + * user is allowed to overcommit the declared protection at a given + * level. If that is the case, the parent's effective protection is + * distributed to the children in proportion to how much protection + * they have declared and how much of it they are utilizing. + * + * This makes distribution proportional, but also work-conserving: + * if one counter claims much more protection than it uses memory, + * the unused remainder is available to its siblings. + * + * 4. Conversely, when the declared protection is undercommitted at a + * given level, the distribution of the larger parental protection + * budget is NOT proportional. A counter's protection from a sibling + * is capped to its own memory.min/low setting. + * + * 5. However, to allow protecting recursive subtrees from each other + * without having to declare each individual counter's fixed share + * of the ancestor's claim to protection, any unutilized - + * "floating" - protection from up the tree is distributed in + * proportion to each counter's *usage*. This makes the protection + * neutral wrt sibling cgroups and lets them compete freely over + * the shared parental protection budget, but it protects the + * subtree as a whole from neighboring subtrees. + * + * Note that 4. and 5. are not in conflict: 4. is about protecting + * against immediate siblings whereas 5. is about protecting against + * neighboring subtrees. + */ +static unsigned long effective_protection(unsigned long usage, + unsigned long parent_usage, + unsigned long setting, + unsigned long parent_effective, + unsigned long siblings_protected, + bool recursive_protection) +{ + unsigned long protected; + unsigned long ep; + + protected = min(usage, setting); + /* + * If all cgroups at this level combined claim and use more + * protection than what the parent affords them, distribute + * shares in proportion to utilization. + * + * We are using actual utilization rather than the statically + * claimed protection in order to be work-conserving: claimed + * but unused protection is available to siblings that would + * otherwise get a smaller chunk than what they claimed. + */ + if (siblings_protected > parent_effective) + return protected * parent_effective / siblings_protected; + + /* + * Ok, utilized protection of all children is within what the + * parent affords them, so we know whatever this child claims + * and utilizes is effectively protected. + * + * If there is unprotected usage beyond this value, reclaim + * will apply pressure in proportion to that amount. + * + * If there is unutilized protection, the cgroup will be fully + * shielded from reclaim, but we do return a smaller value for + * protection than what the group could enjoy in theory. This + * is okay. With the overcommit distribution above, effective + * protection is always dependent on how memory is actually + * consumed among the siblings anyway. + */ + ep = protected; + + /* + * If the children aren't claiming (all of) the protection + * afforded to them by the parent, distribute the remainder in + * proportion to the (unprotected) memory of each cgroup. That + * way, cgroups that aren't explicitly prioritized wrt each + * other compete freely over the allowance, but they are + * collectively protected from neighboring trees. + * + * We're using unprotected memory for the weight so that if + * some cgroups DO claim explicit protection, we don't protect + * the same bytes twice. + * + * Check both usage and parent_usage against the respective + * protected values. One should imply the other, but they + * aren't read atomically - make sure the division is sane. + */ + if (!recursive_protection) + return ep; + + if (parent_effective > siblings_protected && + parent_usage > siblings_protected && + usage > protected) { + unsigned long unclaimed; + + unclaimed = parent_effective - siblings_protected; + unclaimed *= usage - protected; + unclaimed /= parent_usage - siblings_protected; + + ep += unclaimed; + } + + return ep; +} + +/** + * page_counter_calculate_protection - check if memory consumption is in the normal range + * @root: the top ancestor of the sub-tree being checked + * @counter: the page_counter the counter to update + * @recursive_protection: Whether to use memory_recursiveprot behavior. + * + * Calculates elow/emin thresholds for given page_counter. + * + * WARNING: This function is not stateless! It can only be used as part + * of a top-down tree iteration, not for isolated queries. + */ +#if IS_ENABLED(CONFIG_MEMCG) || IS_ENABLED(CONFIG_CGROUP_DMEM) +void page_counter_calculate_protection(struct page_counter *root, + struct page_counter *counter, + bool recursive_protection) +{ + unsigned long usage, parent_usage; + struct page_counter *parent = counter->parent; + + /* + * Effective values of the reclaim targets are ignored so they + * can be stale. Have a look at mem_cgroup_protection for more + * details. + * TODO: calculation should be more robust so that we do not need + * that special casing. + */ + if (root == counter) + return; + + usage = page_counter_read(counter); + if (!usage) + return; + + if (parent == root) { + counter->emin = READ_ONCE(counter->min); + counter->elow = READ_ONCE(counter->low); + return; + } + + parent_usage = page_counter_read(parent); + + WRITE_ONCE(counter->emin, effective_protection(usage, parent_usage, + READ_ONCE(counter->min), + READ_ONCE(parent->emin), + atomic_long_read(&parent->children_min_usage), + recursive_protection)); + + WRITE_ONCE(counter->elow, effective_protection(usage, parent_usage, + READ_ONCE(counter->low), + READ_ONCE(parent->elow), + atomic_long_read(&parent->children_low_usage), + recursive_protection)); +} +#endif From 391b2dc4a083f93d5024f0054dca589ae28d9b32 Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Sun, 31 Aug 2025 17:17:20 +0200 Subject: [PATCH 2/8] cgroup/dmem: Add queries for protection values Callers can use this feedback to be more aggressive in making space for allocations of a cgroup if they know it is protected. These are counterparts to memcg's mem_cgroup_below_{min,low}. Signed-off-by: Natalie Vock --- include/linux/cgroup_dmem.h | 16 ++++++++++ kernel/cgroup/dmem.c | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/include/linux/cgroup_dmem.h b/include/linux/cgroup_dmem.h index dd4869f1d736e..1a88cd0c9eb00 100644 --- a/include/linux/cgroup_dmem.h +++ b/include/linux/cgroup_dmem.h @@ -24,6 +24,10 @@ void dmem_cgroup_uncharge(struct dmem_cgroup_pool_state *pool, u64 size); bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, struct dmem_cgroup_pool_state *test_pool, bool ignore_low, bool *ret_hit_low); +bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test); +bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test); void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool); #else @@ -59,6 +63,18 @@ bool dmem_cgroup_state_evict_valuable(struct dmem_cgroup_pool_state *limit_pool, return true; } +static inline bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + return false; +} + +static inline bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + return false; +} + static inline void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) { } diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index 10b63433f0573..314c37c06f81e 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -641,6 +641,68 @@ int dmem_cgroup_try_charge(struct dmem_cgroup_region *region, u64 size, } EXPORT_SYMBOL_GPL(dmem_cgroup_try_charge); +/** + * dmem_cgroup_below_min() - Tests whether current usage is within min limit. + * + * @root: Root of the subtree to calculate protection for, or NULL to calculate global protection. + * @test: The pool to test the usage/min limit of. + * + * Return: true if usage is below min and the cgroup is protected, false otherwise. + */ +bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + if (root == test || !pool_parent(test)) + return false; + + if (!root) { + for (root = test; pool_parent(root); root = pool_parent(root)) + {} + } + + /* + * In mem_cgroup_below_min(), the memcg pendant, this call is missing. + * mem_cgroup_below_min() gets called during traversal of the cgroup tree, where + * protection is already calculated as part of the traversal. dmem cgroup eviction + * does not traverse the cgroup tree, so we need to recalculate effective protection + * here. + */ + dmem_cgroup_calculate_protection(root, test); + return page_counter_read(&test->cnt) <= READ_ONCE(test->cnt.emin); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_below_min); + +/** + * dmem_cgroup_below_low() - Tests whether current usage is within low limit. + * + * @root: Root of the subtree to calculate protection for, or NULL to calculate global protection. + * @test: The pool to test the usage/low limit of. + * + * Return: true if usage is below low and the cgroup is protected, false otherwise. + */ +bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, + struct dmem_cgroup_pool_state *test) +{ + if (root == test || !pool_parent(test)) + return false; + + if (!root) { + for (root = test; pool_parent(root); root = pool_parent(root)) + {} + } + + /* + * In mem_cgroup_below_low(), the memcg pendant, this call is missing. + * mem_cgroup_below_low() gets called during traversal of the cgroup tree, where + * protection is already calculated as part of the traversal. dmem cgroup eviction + * does not traverse the cgroup tree, so we need to recalculate effective protection + * here. + */ + dmem_cgroup_calculate_protection(root, test); + return page_counter_read(&test->cnt) <= READ_ONCE(test->cnt.elow); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_below_low); + static int dmem_cgroup_region_capacity_show(struct seq_file *sf, void *v) { struct dmem_cgroup_region *region; From 38403965adc7f2c5d5e217d2aff0b367798a034b Mon Sep 17 00:00:00 2001 From: Natalie Vock Date: Sun, 14 Sep 2025 18:35:45 +0200 Subject: [PATCH 3/8] cgroup,cgroup/dmem: Add (dmem_)cgroup_common_ancestor helper This helps to find a common subtree of two resources, which is important when determining whether it's helpful to evict one resource in favor of another. To facilitate this, add a common helper to find the ancestor of two cgroups using each cgroup's ancestor array. Signed-off-by: Natalie Vock --- include/linux/cgroup.h | 21 +++++++++++++++++++++ include/linux/cgroup_dmem.h | 9 +++++++++ kernel/cgroup/dmem.c | 29 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h index fa87b6a15082c..3191475279c28 100644 --- a/include/linux/cgroup.h +++ b/include/linux/cgroup.h @@ -537,6 +537,27 @@ static inline struct cgroup *cgroup_ancestor(struct cgroup *cgrp, return cgrp->ancestors[ancestor_level]; } +/** + * cgroup_common_ancestor - find common ancestor of two cgroups + * @a: first cgroup to find common ancestor of + * @b: second cgroup to find common ancestor of + * + * Find the first cgroup that is an ancestor of both @a and @b, if it exists + * and return a pointer to it. If such a cgroup doesn't exist, return NULL. + * + * This function is safe to call as long as both @a and @b are accessible. + */ +static inline struct cgroup *cgroup_common_ancestor(struct cgroup *a, + struct cgroup *b) +{ + int level; + + for (level = min(a->level, b->level); level >= 0; level--) + if (a->ancestors[level] == b->ancestors[level]) + return a->ancestors[level]; + return NULL; +} + /** * task_under_cgroup_hierarchy - test task's membership of cgroup ancestry * @task: the task to be tested diff --git a/include/linux/cgroup_dmem.h b/include/linux/cgroup_dmem.h index 1a88cd0c9eb00..9d72457c4cb9d 100644 --- a/include/linux/cgroup_dmem.h +++ b/include/linux/cgroup_dmem.h @@ -28,6 +28,8 @@ bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, struct dmem_cgroup_pool_state *test); bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, struct dmem_cgroup_pool_state *test); +struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgroup_pool_state *a, + struct dmem_cgroup_pool_state *b); void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool); #else @@ -75,6 +77,13 @@ static inline bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, return false; } +static inline +struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgroup_pool_state *a, + struct dmem_cgroup_pool_state *b) +{ + return NULL; +} + static inline void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) { } diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index 314c37c06f81e..f89a2a0cbcbbf 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -703,6 +703,35 @@ bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, } EXPORT_SYMBOL_GPL(dmem_cgroup_below_low); +/** + * dmem_cgroup_get_common_ancestor(): Find the first common ancestor of two pools. + * @a: First pool to find the common ancestor of. + * @b: First pool to find the common ancestor of. + * + * Return: The first pool that is a parent of both @a and @b, or NULL if either @a or @b are NULL, + * or if such a pool does not exist. A reference to the returned pool is grabbed and must be + * released by the caller when it is done using the pool. + */ +struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgroup_pool_state *a, + struct dmem_cgroup_pool_state *b) +{ + struct cgroup *ancestor_cgroup; + struct cgroup_subsys_state *ancestor_css; + + if (!a || !b) + return NULL; + + ancestor_cgroup = cgroup_common_ancestor(a->cs->css.cgroup, b->cs->css.cgroup); + if (!ancestor_cgroup) + return NULL; + + ancestor_css = cgroup_e_css(ancestor_cgroup, &dmem_cgrp_subsys); + css_get(ancestor_css); + + return get_cg_pool_unlocked(css_to_dmemcs(ancestor_css), a->region); +} +EXPORT_SYMBOL_GPL(dmem_cgroup_get_common_ancestor); + static int dmem_cgroup_region_capacity_show(struct seq_file *sf, void *v) { struct dmem_cgroup_region *region; From fe82eadd1564d3810889166ee77d2d0335fa02e0 Mon Sep 17 00:00:00 2001 From: deepin-wm Date: Thu, 18 Jun 2026 19:31:45 +0800 Subject: [PATCH 4/8] drm/ttm: Add dmem cgroup support for VRAM management Add dmem cgroup integration to TTM for kernel 6.6, adapting the VRAM management improvements from pixelcluster's dmemcg-aggressive-protect branch for the 6.6 TTM code structure. Key changes: - Add ttm_bo_alloc_state for tracking allocation state (charge_pool, limit_pool, in_evict, may_try_low) - Add ttm_bo_alloc_at_place() for dmem-aware allocation attempts - Add ttm_resource_try_charge() for pre-charging cgroups before resource allocation - Split cgroup charge from resource allocation in ttm_resource_alloc - Add dmem cgroup pool state (css) to ttm_resource - Add dmem cgroup region (cg) to ttm_resource_manager - Add ttm_mem_evict_first_dmem() that skips protected BOs during eviction - Add ttm_bo_evict_valuable_dmem() for cgroup-aware eviction decisions using common ancestor for correct protection calculation - Be more aggressive when allocating below dmem cgroup protection limits - Retry eviction with low-protected BOs when may_try_low is set This is a functional equivalent of pixelcluster's patches 3-6, adapted for 6.6's TTM eviction mechanism (ttm_mem_evict_first instead of ttm_bo_evict_alloc with ttm_lru_walk). Original patches by Natalie Vock Adapted for deepin-community/kernel linux-6.6.y branch. --- drivers/gpu/drm/ttm/ttm_bo.c | 313 +++++++++++++++++++++++++++-- drivers/gpu/drm/ttm/ttm_resource.c | 44 +++- include/drm/ttm/ttm_resource.h | 22 +- 3 files changed, 358 insertions(+), 21 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index d07fe37f7dda0..94414bc15baeb 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include "ttm_module.h" @@ -662,7 +663,239 @@ int ttm_mem_evict_first(struct ttm_device *bdev, } /** - * ttm_bo_pin - Pin the buffer object. + * struct ttm_bo_alloc_state - State for a TTM BO allocation attempt + * + * @charge_pool: The memory pool the resource is charged to + * @limit_pool: Which pool limit we should test against + * @in_evict: Whether we are currently evicting buffers + * @may_try_low: If only unprotected BOs should be considered for eviction + */ +struct ttm_bo_alloc_state { + struct dmem_cgroup_pool_state *charge_pool; + struct dmem_cgroup_pool_state *limit_pool; + bool in_evict; + bool may_try_low; +}; + +/** + * ttm_bo_alloc_at_place - Attempt allocating a BO's backing store in a place + * + * @bo: The buffer to allocate the backing store of + * @place: The place to attempt allocation in + * @ctx: ttm_operation_ctx associated with this allocation + * @force_space: If we should evict buffers to force space + * @res: On allocation success, the resulting struct ttm_resource. + * @alloc_state: Object holding allocation state such as charged cgroups. + * + * Returns: + * -EBUSY: No space available, but allocation should be retried with eviction. + * -ENOSPC: No space available, allocation should not be retried. + * -ERESTARTSYS: An interruptible sleep was interrupted by a signal. + */ +static int ttm_bo_alloc_at_place(struct ttm_buffer_object *bo, + const struct ttm_place *place, + struct ttm_operation_ctx *ctx, + bool force_space, + struct ttm_resource **res, + struct ttm_bo_alloc_state *alloc_state) +{ + bool may_evict; + int ret; + + may_evict = !alloc_state->in_evict && force_space && + place->mem_type != TTM_PL_SYSTEM; + + if (!alloc_state->charge_pool) { + ret = ttm_resource_try_charge(bo, place, &alloc_state->charge_pool, + force_space ? &alloc_state->limit_pool + : NULL); + if (ret) { + if (ret == -EAGAIN) + ret = may_evict ? -EBUSY : -ENOSPC; + return ret; + } + } + + /* + * When the cgroup's memory usage is below the low/min limit, + * try evicting unprotected buffers to make space. Otherwise, + * application buffers may be forced to go into GTT even though + * usage is below the corresponding low/min limit. + */ + if (!alloc_state->in_evict) { + may_evict |= dmem_cgroup_below_min(NULL, alloc_state->charge_pool); + alloc_state->may_try_low = may_evict; + + may_evict |= dmem_cgroup_below_low(NULL, alloc_state->charge_pool); + } + + ret = ttm_resource_alloc(bo, place, res, alloc_state->charge_pool); + if (ret) { + if (ret == -ENOSPC && may_evict) + ret = -EBUSY; + return ret; + } + + /* Ownership of charge_pool has been transferred to the TTM resource */ + alloc_state->charge_pool = NULL; + return 0; +} + +/** + * ttm_bo_evict_valuable_dmem - Check if BO eviction is valuable considering dmem cgroup + * + * @bo: The buffer object being considered for eviction + * @alloc_state: State of the allocation attempt + * @try_low: Whether to consider low-protected BOs + * @hit_low: Set to true if we skipped a low-protected BO + * + * Returns true if the BO should be considered for eviction. + */ +static bool ttm_bo_evict_valuable_dmem(struct ttm_buffer_object *bo, + struct ttm_bo_alloc_state *alloc_state, + bool try_low, bool *hit_low) +{ + struct dmem_cgroup_pool_state *limit_pool, *ancestor = NULL; + bool evict_valuable; + + if (!alloc_state) + return true; + + /* Skip BOs from the same cgroup when not trying low-protected ones */ + if (!alloc_state->may_try_low && + bo->resource->css == alloc_state->charge_pool) + return false; + + limit_pool = alloc_state->limit_pool; + /* + * If there is no explicit limit pool, find the root of the shared + * subtree between evictor and evictee for correct protection + * calculation. + */ + if (!limit_pool) { + ancestor = dmem_cgroup_get_common_ancestor(bo->resource->css, + alloc_state->charge_pool); + limit_pool = ancestor; + } + + evict_valuable = dmem_cgroup_state_evict_valuable(limit_pool, + bo->resource->css, + try_low, hit_low); + if (ancestor) + dmem_cgroup_pool_state_put(ancestor); + + return evict_valuable; +} + +/** + * ttm_mem_evict_first_dmem - Evict first BO considering dmem cgroup protection + * + * Same as ttm_mem_evict_first but skips BOs protected by dmem cgroup. + */ +static int ttm_mem_evict_first_dmem(struct ttm_device *bdev, + struct ttm_resource_manager *man, + const struct ttm_place *place, + struct ttm_operation_ctx *ctx, + struct ww_acquire_ctx *ticket, + struct ttm_bo_alloc_state *alloc_state) +{ + struct ttm_buffer_object *bo = NULL, *busy_bo = NULL; + struct ttm_resource_cursor cursor; + struct ttm_resource *res; + bool locked = false; + bool hit_low = false; + int ret; + + spin_lock(&bdev->lru_lock); + ttm_resource_manager_for_each_res(man, &cursor, res) { + bool busy; + + if (!ttm_bo_evict_swapout_allowable(res->bo, ctx, place, + &locked, &busy)) { + if (busy && !busy_bo && ticket != + dma_resv_locking_ctx(res->bo->base.resv)) + busy_bo = res->bo; + continue; + } + + /* Check dmem cgroup protection */ + if (!ttm_bo_evict_valuable_dmem(res->bo, alloc_state, + false, &hit_low)) { + if (locked) { + dma_resv_unlock(res->bo->base.resv); + locked = false; + } + continue; + } + + if (ttm_bo_get_unless_zero(res->bo)) { + bo = res->bo; + break; + } + if (locked) + dma_resv_unlock(res->bo->base.resv); + } + + /* If we hit low-protected BOs and may_try_low, retry with try_low */ + if (!bo && hit_low && alloc_state && alloc_state->may_try_low) { + ttm_resource_cursor_fini(&cursor); + ttm_resource_manager_for_each_res(man, &cursor, res) { + bool busy; + + if (!ttm_bo_evict_swapout_allowable(res->bo, ctx, place, + &locked, &busy)) { + if (busy && !busy_bo && ticket != + dma_resv_locking_ctx(res->bo->base.resv)) + busy_bo = res->bo; + continue; + } + + if (!ttm_bo_evict_valuable_dmem(res->bo, alloc_state, + true, &hit_low)) { + if (locked) { + dma_resv_unlock(res->bo->base.resv); + locked = false; + } + continue; + } + + if (ttm_bo_get_unless_zero(res->bo)) { + bo = res->bo; + break; + } + if (locked) + dma_resv_unlock(res->bo->base.resv); + } + } + + if (!bo) { + if (busy_bo && !ttm_bo_get_unless_zero(busy_bo)) + busy_bo = NULL; + spin_unlock(&bdev->lru_lock); + ret = ttm_mem_evict_wait_busy(busy_bo, ctx, ticket); + if (busy_bo) + ttm_bo_put(busy_bo); + return ret; + } + + if (bo->deleted) { + ret = ttm_bo_cleanup_refs(bo, ctx->interruptible, + ctx->no_wait_gpu, locked); + ttm_bo_put(bo); + return ret; + } + + spin_unlock(&bdev->lru_lock); + + ret = ttm_bo_evict(bo, ctx); + if (locked) + ttm_bo_unreserve(bo); + else + ttm_bo_move_to_lru_tail_unlocked(bo); + + ttm_bo_put(bo); + return ret; +} * @bo: The buffer object to pin * * Make sure the buffer is not evicted any more during memory pressure. @@ -740,7 +973,8 @@ static int ttm_bo_add_move_fence(struct ttm_buffer_object *bo, static int ttm_bo_mem_force_space(struct ttm_buffer_object *bo, const struct ttm_place *place, struct ttm_resource **mem, - struct ttm_operation_ctx *ctx) + struct ttm_operation_ctx *ctx, + struct ttm_bo_alloc_state *alloc_state) { struct ttm_device *bdev = bo->bdev; struct ttm_resource_manager *man; @@ -749,19 +983,38 @@ static int ttm_bo_mem_force_space(struct ttm_buffer_object *bo, man = ttm_manager_type(bdev, place->mem_type); ticket = dma_resv_locking_ctx(bo->base.resv); + + if (alloc_state) + alloc_state->in_evict = true; + do { - ret = ttm_resource_alloc(bo, place, mem); - if (likely(!ret)) + ret = ttm_resource_alloc(bo, place, mem, + alloc_state ? alloc_state->charge_pool : NULL); + if (likely(!ret)) { + if (alloc_state) + alloc_state->charge_pool = NULL; break; + } if (unlikely(ret != -ENOSPC)) - return ret; - ret = ttm_mem_evict_first(bdev, man, place, ctx, - ticket); + goto out; + if (alloc_state && man->cg) { + ret = ttm_mem_evict_first_dmem(bdev, man, place, ctx, + ticket, alloc_state); + } else { + ret = ttm_mem_evict_first(bdev, man, place, ctx, + ticket); + } if (unlikely(ret != 0)) - return ret; + goto out; } while (1); + if (alloc_state) + alloc_state->in_evict = false; return ttm_bo_add_move_fence(bo, man, *mem, ctx->no_wait_gpu); +out: + if (alloc_state) + alloc_state->in_evict = false; + return ret; } /** @@ -797,6 +1050,7 @@ int ttm_bo_mem_space(struct ttm_buffer_object *bo, for (i = 0; i < placement->num_placement; ++i) { const struct ttm_place *place = &placement->placement[i]; + struct ttm_bo_alloc_state alloc_state = {}; struct ttm_resource_manager *man; man = ttm_manager_type(bdev, place->mem_type); @@ -804,11 +1058,32 @@ int ttm_bo_mem_space(struct ttm_buffer_object *bo, continue; type_found = true; - ret = ttm_resource_alloc(bo, place, mem); - if (ret == -ENOSPC) + ret = ttm_bo_alloc_at_place(bo, place, ctx, false, mem, + &alloc_state); + if (ret == -ENOSPC) { + dmem_cgroup_uncharge(alloc_state.charge_pool, bo->base.size); + dmem_cgroup_pool_state_put(alloc_state.limit_pool); continue; - if (unlikely(ret)) + } + if (ret == -EBUSY) { + /* Allocation below protection limit or force_space needed */ + ret = ttm_bo_mem_force_space(bo, place, mem, ctx, + &alloc_state); + dmem_cgroup_pool_state_put(alloc_state.limit_pool); + if (ret) { + dmem_cgroup_uncharge(alloc_state.charge_pool, + bo->base.size); + if (ret == -EBUSY) + continue; + goto error; + } + return 0; + } + if (unlikely(ret)) { + dmem_cgroup_uncharge(alloc_state.charge_pool, bo->base.size); + dmem_cgroup_pool_state_put(alloc_state.limit_pool); goto error; + } ret = ttm_bo_add_move_fence(bo, man, *mem, ctx->no_wait_gpu); if (unlikely(ret)) { @@ -823,6 +1098,7 @@ int ttm_bo_mem_space(struct ttm_buffer_object *bo, for (i = 0; i < placement->num_busy_placement; ++i) { const struct ttm_place *place = &placement->busy_placement[i]; + struct ttm_bo_alloc_state alloc_state = {}; struct ttm_resource_manager *man; man = ttm_manager_type(bdev, place->mem_type); @@ -830,12 +1106,15 @@ int ttm_bo_mem_space(struct ttm_buffer_object *bo, continue; type_found = true; - ret = ttm_bo_mem_force_space(bo, place, mem, ctx); - if (likely(!ret)) + ret = ttm_bo_mem_force_space(bo, place, mem, ctx, &alloc_state); + dmem_cgroup_pool_state_put(alloc_state.limit_pool); + if (ret) { + dmem_cgroup_uncharge(alloc_state.charge_pool, bo->base.size); + if (ret != -EBUSY) + goto error; + } else { return 0; - - if (ret && ret != -EBUSY) - goto error; + } } ret = -ENOSPC; @@ -1184,7 +1463,7 @@ int ttm_bo_swapout(struct ttm_buffer_object *bo, struct ttm_operation_ctx *ctx, memset(&hop, 0, sizeof(hop)); place.mem_type = TTM_PL_SYSTEM; - ret = ttm_resource_alloc(bo, &place, &evict_mem); + ret = ttm_resource_alloc(bo, &place, &evict_mem, NULL); if (unlikely(ret)) goto out; diff --git a/drivers/gpu/drm/ttm/ttm_resource.c b/drivers/gpu/drm/ttm/ttm_resource.c index 774541948e024..0f12ff7ca57bb 100644 --- a/drivers/gpu/drm/ttm/ttm_resource.c +++ b/drivers/gpu/drm/ttm/ttm_resource.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -223,9 +224,43 @@ void ttm_resource_fini(struct ttm_resource_manager *man, } EXPORT_SYMBOL(ttm_resource_fini); +/** + * ttm_resource_try_charge - charge a resource manager's cgroup pool + * @bo: buffer for which an allocation should be charged + * @place: where the allocation is attempted to be placed + * @ret_pool: on charge success, the pool that was charged + * @ret_limit_pool: on charge failure, the pool responsible for the failure + * + * Should be used to charge cgroups before attempting resource allocation. + * When charging succeeds, the value of ret_pool should be passed to + * ttm_resource_alloc. + * + * Returns: 0 on charge success, negative errno on failure. + */ +int ttm_resource_try_charge(struct ttm_buffer_object *bo, + const struct ttm_place *place, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool) +{ + struct ttm_resource_manager *man = + ttm_manager_type(bo->bdev, place->mem_type); + + if (!man->cg) { + *ret_pool = NULL; + if (ret_limit_pool) + *ret_limit_pool = NULL; + return 0; + } + + return dmem_cgroup_try_charge(man->cg, bo->base.size, ret_pool, + ret_limit_pool); +} +EXPORT_SYMBOL(ttm_resource_try_charge); + int ttm_resource_alloc(struct ttm_buffer_object *bo, const struct ttm_place *place, - struct ttm_resource **res_ptr) + struct ttm_resource **res_ptr, + struct dmem_cgroup_pool_state *charge_pool) { struct ttm_resource_manager *man = ttm_manager_type(bo->bdev, place->mem_type); @@ -235,6 +270,8 @@ int ttm_resource_alloc(struct ttm_buffer_object *bo, if (ret) return ret; + (*res_ptr)->css = charge_pool; + spin_lock(&bo->bdev->lru_lock); ttm_resource_add_bulk_move(*res_ptr, bo); spin_unlock(&bo->bdev->lru_lock); @@ -244,6 +281,7 @@ int ttm_resource_alloc(struct ttm_buffer_object *bo, void ttm_resource_free(struct ttm_buffer_object *bo, struct ttm_resource **res) { struct ttm_resource_manager *man; + struct dmem_cgroup_pool_state *pool; if (!*res) return; @@ -251,9 +289,13 @@ void ttm_resource_free(struct ttm_buffer_object *bo, struct ttm_resource **res) spin_lock(&bo->bdev->lru_lock); ttm_resource_del_bulk_move(*res, bo); spin_unlock(&bo->bdev->lru_lock); + + pool = (*res)->css; man = ttm_manager_type(bo->bdev, (*res)->mem_type); man->func->free(man, *res); *res = NULL; + if (man->cg) + dmem_cgroup_uncharge(pool, bo->base.size); } EXPORT_SYMBOL(ttm_resource_free); diff --git a/include/drm/ttm/ttm_resource.h b/include/drm/ttm/ttm_resource.h index 799aa2a54d57b..df4cd9dd24b7a 100644 --- a/include/drm/ttm/ttm_resource.h +++ b/include/drm/ttm/ttm_resource.h @@ -39,6 +39,9 @@ #define TTM_MAX_BO_PRIORITY 4U #define TTM_NUM_MEM_TYPES 8 +struct dmem_cgroup_device; +struct dmem_cgroup_region; +struct dmem_cgroup_pool_state; struct ttm_device; struct ttm_resource_manager; struct ttm_resource; @@ -179,7 +182,11 @@ struct ttm_resource_manager { * bdev->lru_lock. */ uint64_t usage; - DEEPIN_KABI_RESERVE(1) + + /** + * @cg: &dmem_cgroup_region used for memory accounting, if not NULL. + */ + struct dmem_cgroup_region *cg; DEEPIN_KABI_RESERVE(2) }; @@ -223,11 +230,15 @@ struct ttm_resource { struct ttm_bus_placement bus; struct ttm_buffer_object *bo; + /** + * @css: cgroup state this resource is charged to + */ + struct dmem_cgroup_pool_state *css; + /** * @lru: Least recently used list, see &ttm_resource_manager.lru */ struct list_head lru; - DEEPIN_KABI_RESERVE(1) DEEPIN_KABI_RESERVE(2) DEEPIN_KABI_RESERVE(3) DEEPIN_KABI_RESERVE(4) @@ -370,9 +381,14 @@ void ttm_resource_init(struct ttm_buffer_object *bo, void ttm_resource_fini(struct ttm_resource_manager *man, struct ttm_resource *res); +int ttm_resource_try_charge(struct ttm_buffer_object *bo, + const struct ttm_place *place, + struct dmem_cgroup_pool_state **ret_pool, + struct dmem_cgroup_pool_state **ret_limit_pool); int ttm_resource_alloc(struct ttm_buffer_object *bo, const struct ttm_place *place, - struct ttm_resource **res); + struct ttm_resource **res, + struct dmem_cgroup_pool_state *charge_pool); void ttm_resource_free(struct ttm_buffer_object *bo, struct ttm_resource **res); bool ttm_resource_intersects(struct ttm_device *bdev, struct ttm_resource *res, From b1f5900bbe0a1c9d2c5a3e1639943bb0ace3da7f Mon Sep 17 00:00:00 2001 From: deepin-wm Date: Tue, 23 Jun 2026 13:43:07 +0800 Subject: [PATCH 5/8] fix: Address review feedback, NULL safety, and KABI issues Fix issues identified by Copilot code review: dmem.c: - Add NULL test pointer check in dmem_cgroup_below_min/low to prevent NULL dereference when called with uncharged pools - Fix dmem_cgroup_get_common_ancestor ERR_PTR handling: check for IS_ERR_OR_NULL return from get_cg_pool_unlocked and release css reference on failure - Fix kernel-doc: @b parameter was incorrectly described as 'First pool' - Fix get_cg_pool_unlocked infinite loop: return ERR_PTR(-ENOMEM) when fallback allocation fails instead of looping forever - Add NULL options check in dmemcg_limit_write to prevent crash when line contains only a region name - Fix %lld -> %llu format specifier for u64 values - Fix kernel-doc mismatch: dmem_cgroup_evict_valuable -> dmem_cgroup_state_evict_valuable cgroup_dmem.h: - Change license from MIT to GPL-2.0 for consistency with other internal cgroup headers ttm_resource.h: - Move css field to DEEPIN_KABI_RESERVE slot after lru to preserve struct layout and avoid breaking KABI for out-of-tree users ttm_bo.c: - Guard dmem_cgroup_below_min/low calls with charge_pool check - Only skip same-cgroup BOs during eviction when charge_pool is non-NULL - Handle NULL css in eviction: treat uncharged resources as evictable - Add charge_pool initialization in ttm_bo_mem_force_space for busy_placement path to prevent bypassing dmem cgroup limits --- drivers/gpu/drm/ttm/ttm_bo.c | 18 +++++++++++++++--- include/drm/ttm/ttm_resource.h | 10 ++++------ include/linux/cgroup_dmem.h | 2 +- kernel/cgroup/dmem.c | 23 +++++++++++++++++++---- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index 94414bc15baeb..f9062942c8916 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -722,7 +722,7 @@ static int ttm_bo_alloc_at_place(struct ttm_buffer_object *bo, * application buffers may be forced to go into GTT even though * usage is below the corresponding low/min limit. */ - if (!alloc_state->in_evict) { + if (!alloc_state->in_evict && alloc_state->charge_pool) { may_evict |= dmem_cgroup_below_min(NULL, alloc_state->charge_pool); alloc_state->may_try_low = may_evict; @@ -762,7 +762,8 @@ static bool ttm_bo_evict_valuable_dmem(struct ttm_buffer_object *bo, return true; /* Skip BOs from the same cgroup when not trying low-protected ones */ - if (!alloc_state->may_try_low && + if (alloc_state->charge_pool && + !alloc_state->may_try_low && bo->resource->css == alloc_state->charge_pool) return false; @@ -984,8 +985,19 @@ static int ttm_bo_mem_force_space(struct ttm_buffer_object *bo, man = ttm_manager_type(bdev, place->mem_type); ticket = dma_resv_locking_ctx(bo->base.resv); - if (alloc_state) + if (alloc_state) { alloc_state->in_evict = true; + /* Ensure we have a charge_pool for this allocation */ + if (man->cg && !alloc_state->charge_pool) { + ret = ttm_resource_try_charge(bo, place, + &alloc_state->charge_pool, + &alloc_state->limit_pool); + if (ret) { + alloc_state->in_evict = false; + return ret; + } + } + } do { ret = ttm_resource_alloc(bo, place, mem, diff --git a/include/drm/ttm/ttm_resource.h b/include/drm/ttm/ttm_resource.h index df4cd9dd24b7a..337ef458f10fd 100644 --- a/include/drm/ttm/ttm_resource.h +++ b/include/drm/ttm/ttm_resource.h @@ -230,16 +230,14 @@ struct ttm_resource { struct ttm_bus_placement bus; struct ttm_buffer_object *bo; - /** - * @css: cgroup state this resource is charged to - */ - struct dmem_cgroup_pool_state *css; - /** * @lru: Least recently used list, see &ttm_resource_manager.lru */ struct list_head lru; - DEEPIN_KABI_RESERVE(2) + /** + * @css: cgroup state this resource is charged to + */ + struct dmem_cgroup_pool_state *css; DEEPIN_KABI_RESERVE(3) DEEPIN_KABI_RESERVE(4) }; diff --git a/include/linux/cgroup_dmem.h b/include/linux/cgroup_dmem.h index 9d72457c4cb9d..ea7c11432f99a 100644 --- a/include/linux/cgroup_dmem.h +++ b/include/linux/cgroup_dmem.h @@ -1,4 +1,4 @@ -/* SPDX-License-Identifier: MIT */ +/* SPDX-License-Identifier: GPL-2.0 */ /* * Copyright © 2023-2024 Intel Corporation */ diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index f89a2a0cbcbbf..91ae3c351f2bb 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -552,6 +552,7 @@ get_cg_pool_unlocked(struct dmemcg_state *cg, struct dmem_cgroup_region *region) pool = NULL; continue; } + return ERR_PTR(-ENOMEM); } } @@ -592,7 +593,7 @@ EXPORT_SYMBOL_GPL(dmem_cgroup_uncharge); * * When this function fails with -EAGAIN and @ret_limit_pool is non-null, it * will be set to the pool for which the limit is hit. This can be used for - * eviction as argument to dmem_cgroup_evict_valuable(). This reference must be freed + * eviction as argument to dmem_cgroup_state_evict_valuable(). This reference must be freed * with @dmem_cgroup_pool_state_put(). * * Return: 0 on success, -EAGAIN on hitting a limit, or a negative errno on failure. @@ -652,6 +653,9 @@ EXPORT_SYMBOL_GPL(dmem_cgroup_try_charge); bool dmem_cgroup_below_min(struct dmem_cgroup_pool_state *root, struct dmem_cgroup_pool_state *test) { + if (!test) + return false; + if (root == test || !pool_parent(test)) return false; @@ -683,6 +687,9 @@ EXPORT_SYMBOL_GPL(dmem_cgroup_below_min); bool dmem_cgroup_below_low(struct dmem_cgroup_pool_state *root, struct dmem_cgroup_pool_state *test) { + if (!test) + return false; + if (root == test || !pool_parent(test)) return false; @@ -706,7 +713,7 @@ EXPORT_SYMBOL_GPL(dmem_cgroup_below_low); /** * dmem_cgroup_get_common_ancestor(): Find the first common ancestor of two pools. * @a: First pool to find the common ancestor of. - * @b: First pool to find the common ancestor of. + * @b: Second pool to find the common ancestor of. * * Return: The first pool that is a parent of both @a and @b, or NULL if either @a or @b are NULL, * or if such a pool does not exist. A reference to the returned pool is grabbed and must be @@ -717,6 +724,7 @@ struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgrou { struct cgroup *ancestor_cgroup; struct cgroup_subsys_state *ancestor_css; + struct dmem_cgroup_pool_state *pool; if (!a || !b) return NULL; @@ -728,7 +736,12 @@ struct dmem_cgroup_pool_state *dmem_cgroup_get_common_ancestor(struct dmem_cgrou ancestor_css = cgroup_e_css(ancestor_cgroup, &dmem_cgrp_subsys); css_get(ancestor_css); - return get_cg_pool_unlocked(css_to_dmemcs(ancestor_css), a->region); + pool = get_cg_pool_unlocked(css_to_dmemcs(ancestor_css), a->region); + if (IS_ERR_OR_NULL(pool)) { + css_put(ancestor_css); + return NULL; + } + return pool; } EXPORT_SYMBOL_GPL(dmem_cgroup_get_common_ancestor); @@ -787,6 +800,8 @@ static ssize_t dmemcg_limit_write(struct kernfs_open_file *of, continue; region_name = strsep(&options, " \t"); + if (!options) + return -EINVAL; if (!region_name[0]) continue; @@ -833,7 +848,7 @@ static int dmemcg_limit_show(struct seq_file *sf, void *v, val = fn(pool); if (val < PAGE_COUNTER_MAX) - seq_printf(sf, " %lld\n", val); + seq_printf(sf, " %llu\n", val); else seq_puts(sf, " max\n"); } From 2b2ced7ed537de4b2c6f9584785820ddf51a7bbf Mon Sep 17 00:00:00 2001 From: deepin-wm Date: Tue, 30 Jun 2026 12:14:21 +0800 Subject: [PATCH 6/8] fix: remove ttm_resource_cursor_fini and add kernel-doc for ttm_bo_pin --- drivers/gpu/drm/ttm/ttm_bo.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index f9062942c8916..ddbf00a0346bf 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -839,7 +839,6 @@ static int ttm_mem_evict_first_dmem(struct ttm_device *bdev, /* If we hit low-protected BOs and may_try_low, retry with try_low */ if (!bo && hit_low && alloc_state && alloc_state->may_try_low) { - ttm_resource_cursor_fini(&cursor); ttm_resource_manager_for_each_res(man, &cursor, res) { bool busy; @@ -897,6 +896,9 @@ static int ttm_mem_evict_first_dmem(struct ttm_device *bdev, ttm_bo_put(bo); return ret; } + +/** + * ttm_bo_pin - Pin the buffer object. * @bo: The buffer object to pin * * Make sure the buffer is not evicted any more during memory pressure. From 59910ca1a09c69165ceeeae42cc7a5db4bcad155 Mon Sep 17 00:00:00 2001 From: deepin-wm Date: Thu, 2 Jul 2026 13:43:11 +0800 Subject: [PATCH 7/8] fix: resolve i386_defconfig compilation errors - Move effective_protection() inside the #if IS_ENABLED(CONFIG_MEMCG) || IS_ENABLED(CONFIG_CGROUP_DMEM) guard in mm/page_counter.c to fix -Werror=unused-function when both configs are disabled (e.g. i386_defconfig) - Guard page_counter_calculate_protection() declaration in page_counter.h with the same #if to match its definition - Remove the 3rd argument from page_counter_init() call in dmem.c since page_counter_init() only takes 2 arguments in 6.6.y; the recursive_protection is passed directly to page_counter_calculate_protection() instead --- include/linux/page_counter.h | 2 ++ kernel/cgroup/dmem.c | 2 +- mm/page_counter.c | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/linux/page_counter.h b/include/linux/page_counter.h index e24e918146382..410dafa7990fe 100644 --- a/include/linux/page_counter.h +++ b/include/linux/page_counter.h @@ -81,8 +81,10 @@ static inline void page_counter_reset_watermark(struct page_counter *counter) counter->watermark = page_counter_read(counter); } +#if IS_ENABLED(CONFIG_MEMCG) || IS_ENABLED(CONFIG_CGROUP_DMEM) void page_counter_calculate_protection(struct page_counter *root, struct page_counter *counter, bool recursive_protection); +#endif #endif /* _LINUX_PAGE_COUNTER_H */ diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index 91ae3c351f2bb..2e3f09a68a8a8 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -339,7 +339,7 @@ alloc_pool_single(struct dmemcg_state *dmemcs, struct dmem_cgroup_region *region ppool = find_cg_pool_locked(parent, region); page_counter_init(&pool->cnt, - ppool ? &ppool->cnt : NULL, true); + ppool ? &ppool->cnt : NULL); reset_all_resource_limits(pool); list_add_tail_rcu(&pool->css_node, &dmemcs->pools); diff --git a/mm/page_counter.c b/mm/page_counter.c index 74f5c91877861..e88960c942513 100644 --- a/mm/page_counter.c +++ b/mm/page_counter.c @@ -307,6 +307,7 @@ int page_counter_memparse(const char *buf, const char *max, * against immediate siblings whereas 5. is about protecting against * neighboring subtrees. */ +#if IS_ENABLED(CONFIG_MEMCG) || IS_ENABLED(CONFIG_CGROUP_DMEM) static unsigned long effective_protection(unsigned long usage, unsigned long parent_usage, unsigned long setting, @@ -381,6 +382,7 @@ static unsigned long effective_protection(unsigned long usage, return ep; } +#endif /** * page_counter_calculate_protection - check if memory consumption is in the normal range From 42e9dcb092953ad156abf66f7afebf7c4fc88ed2 Mon Sep 17 00:00:00 2001 From: deepin-wm Date: Mon, 6 Jul 2026 13:33:17 +0800 Subject: [PATCH 8/8] fix: backport dmem pool refcount and css guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Backport pool-level refcount_t to fix dmem pool use-after-free 2. Add pool refcount helpers with RCU-delayed freeing 3. Balance pool refs across charge, uncharge, and region unregister 4. Add NULL css guard so uncharged BOs are always evictable Influence: 1. Test dmem region register/unregister with outstanding charges 2. Verify uncharged BO (css == NULL) is evictable 3. Build with CONFIG_CGROUP_DMEM enabled and disabled fix: backport dmem 池引用计数与 css 守卫 1. 从 6.18.y backport 池级 refcount_t,修复 use-after-free 2. 添加池引用计数辅助函数,采用 RCU 延迟释放 3. 在 charge/uncharge 和 region 注销路径中配平池引用 4. 添加 NULL css 守卫,未计费 BO 一律可驱逐 Influence: 1. 测试 dmem region 注册/注销时存在未释放计费的场景 2. 验证未计费 BO (css == NULL) 可被驱逐 3. 在 CONFIG_CGROUP_DMEM 启用和禁用两种配置下编译测试 --- drivers/gpu/drm/ttm/ttm_bo.c | 3 ++ kernel/cgroup/dmem.c | 67 ++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/ttm/ttm_bo.c b/drivers/gpu/drm/ttm/ttm_bo.c index ddbf00a0346bf..ec3a04c8a4ebf 100644 --- a/drivers/gpu/drm/ttm/ttm_bo.c +++ b/drivers/gpu/drm/ttm/ttm_bo.c @@ -761,6 +761,9 @@ static bool ttm_bo_evict_valuable_dmem(struct ttm_buffer_object *bo, if (!alloc_state) return true; + if (!bo->resource->css) + return true; + /* Skip BOs from the same cgroup when not trying low-protected ones */ if (alloc_state->charge_pool && !alloc_state->may_try_low && diff --git a/kernel/cgroup/dmem.c b/kernel/cgroup/dmem.c index 2e3f09a68a8a8..17bf6a4ff7134 100644 --- a/kernel/cgroup/dmem.c +++ b/kernel/cgroup/dmem.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -70,7 +71,9 @@ struct dmem_cgroup_pool_state { struct rcu_head rcu; struct page_counter cnt; + struct dmem_cgroup_pool_state *parent; + refcount_t ref; bool inited; }; @@ -87,6 +90,9 @@ struct dmem_cgroup_pool_state { static DEFINE_SPINLOCK(dmemcg_lock); static LIST_HEAD(dmem_cgroup_regions); +static void dmemcg_free_region(struct kref *ref); +static void dmemcg_pool_free_rcu(struct rcu_head *rcu); + static inline struct dmemcg_state * css_to_dmemcs(struct cgroup_subsys_state *css) { @@ -103,10 +109,38 @@ static struct dmemcg_state *parent_dmemcs(struct dmemcg_state *cg) return cg->css.parent ? css_to_dmemcs(cg->css.parent) : NULL; } +static void dmemcg_pool_get(struct dmem_cgroup_pool_state *pool) +{ + refcount_inc(&pool->ref); +} + +static bool dmemcg_pool_tryget(struct dmem_cgroup_pool_state *pool) +{ + return refcount_inc_not_zero(&pool->ref); +} + +static void dmemcg_pool_put(struct dmem_cgroup_pool_state *pool) +{ + if (!refcount_dec_and_test(&pool->ref)) + return; + + call_rcu(&pool->rcu, dmemcg_pool_free_rcu); +} + +static void dmemcg_pool_free_rcu(struct rcu_head *rcu) +{ + struct dmem_cgroup_pool_state *pool = container_of(rcu, typeof(*pool), rcu); + + if (pool->parent) + dmemcg_pool_put(pool->parent); + kref_put(&pool->region->ref, dmemcg_free_region); + kfree(pool); +} + static void free_cg_pool(struct dmem_cgroup_pool_state *pool) { list_del(&pool->region_node); - kfree(pool); + dmemcg_pool_put(pool); } static void @@ -341,6 +375,12 @@ alloc_pool_single(struct dmemcg_state *dmemcs, struct dmem_cgroup_region *region page_counter_init(&pool->cnt, ppool ? &ppool->cnt : NULL); reset_all_resource_limits(pool); + refcount_set(&pool->ref, 1); + kref_get(®ion->ref); + if (ppool && !pool->parent) { + pool->parent = ppool; + dmemcg_pool_get(ppool); + } list_add_tail_rcu(&pool->css_node, &dmemcs->pools); list_add_tail(&pool->region_node, ®ion->pools); @@ -388,6 +428,10 @@ get_cg_pool_locked(struct dmemcg_state *dmemcs, struct dmem_cgroup_region *regio /* Fix up parent links, mark as inited. */ pool->cnt.parent = &ppool->cnt; + if (ppool && !pool->parent) { + pool->parent = ppool; + dmemcg_pool_get(ppool); + } pool->inited = true; pool = ppool; @@ -422,7 +466,7 @@ static void dmemcg_free_region(struct kref *ref) */ void dmem_cgroup_unregister_region(struct dmem_cgroup_region *region) { - struct list_head *entry; + struct dmem_cgroup_pool_state *pool, *next; if (!region) return; @@ -432,11 +476,10 @@ void dmem_cgroup_unregister_region(struct dmem_cgroup_region *region) /* Remove from global region list */ list_del_rcu(®ion->region_node); - list_for_each_rcu(entry, ®ion->pools) { - struct dmem_cgroup_pool_state *pool = - container_of(entry, typeof(*pool), region_node); - + list_for_each_entry_safe(pool, next, ®ion->pools, region_node) { list_del_rcu(&pool->css_node); + list_del(&pool->region_node); + dmemcg_pool_put(pool); } /* @@ -517,8 +560,10 @@ static struct dmem_cgroup_region *dmemcg_get_region_by_name(const char *name) */ void dmem_cgroup_pool_state_put(struct dmem_cgroup_pool_state *pool) { - if (pool) + if (pool) { css_put(&pool->cs->css); + dmemcg_pool_put(pool); + } } EXPORT_SYMBOL_GPL(dmem_cgroup_pool_state_put); @@ -532,6 +577,8 @@ get_cg_pool_unlocked(struct dmemcg_state *cg, struct dmem_cgroup_region *region) pool = find_cg_pool_locked(cg, region); if (pool && !READ_ONCE(pool->inited)) pool = NULL; + if (pool && !dmemcg_pool_tryget(pool)) + pool = NULL; rcu_read_unlock(); while (!pool) { @@ -540,6 +587,8 @@ get_cg_pool_unlocked(struct dmemcg_state *cg, struct dmem_cgroup_region *region) pool = get_cg_pool_locked(cg, region, &allocpool); else pool = ERR_PTR(-ENODEV); + if (!IS_ERR(pool)) + dmemcg_pool_get(pool); spin_unlock(&dmemcg_lock); if (pool == ERR_PTR(-ENOMEM)) { @@ -576,6 +625,7 @@ void dmem_cgroup_uncharge(struct dmem_cgroup_pool_state *pool, u64 size) page_counter_uncharge(&pool->cnt, size); css_put(&pool->cs->css); + dmemcg_pool_put(pool); } EXPORT_SYMBOL_GPL(dmem_cgroup_uncharge); @@ -627,7 +677,9 @@ int dmem_cgroup_try_charge(struct dmem_cgroup_region *region, u64 size, if (ret_limit_pool) { *ret_limit_pool = container_of(fail, struct dmem_cgroup_pool_state, cnt); css_get(&(*ret_limit_pool)->cs->css); + dmemcg_pool_get(*ret_limit_pool); } + dmemcg_pool_put(pool); ret = -EAGAIN; goto err; } @@ -824,6 +876,7 @@ static ssize_t dmemcg_limit_write(struct kernfs_open_file *of, /* And commit */ apply(pool, new_limit); + dmemcg_pool_put(pool); out_put: kref_put(®ion->ref, dmemcg_free_region);