[PATCH v2 04/11] drm, cgroup: Add total GEM buffer allocation stats

Kenny Ho
2020-02-26T19:01:45Z

The drm resource being measured here is the GEM buffer objects. User
applications allocate and free these buffers. In addition, a process
can allocate a buffer and share it with another process. The consumer
of a shared buffer can also outlive the allocator of the buffer.

For the purpose of cgroup accounting and limiting, ownership of the
buffer is deemed to be the cgroup for which the allocating process
belongs to. There is one cgroup stats per drm device. Each allocation
is charged to the owning cgroup as well as all its ancestors.

Similar to the memory cgroup, migrating a process to a different cgroup
does not move the GEM buffer usages that the process started while in
previous cgroup, to the new cgroup.

The following is an example to illustrate some of the operations. Given
the following cgroup hierarchy (The letters are cgroup names with R
being the root cgroup. The numbers in brackets are processes. The
processes are placed with cgroup's 'No Internal Process Constraint' in
mind, so no process is placed in cgroup B.)

R (4, 5) ------ A (6)
\
B ---- C (7,8)
\
D (9)

Here is a list of operation and the associated effect on the size
track by the cgroups (for simplicity, each buffer is 1 unit in size.)

== == == == == ===================================================
R A B C D Ops
== == == == == ===================================================
1 0 0 0 0 4 allocated a buffer
1 0 0 0 0 4 shared a buffer with 5
1 0 0 0 0 4 shared a buffer with 9
2 0 1 0 1 9 allocated a buffer
3 0 2 1 1 7 allocated a buffer
3 0 2 1 1 7 shared a buffer with 8
3 0 2 1 1 7 sharing with 9
3 0 2 1 1 7 release a buffer
3 0 2 1 1 7 migrate to cgroup D
3 0 2 1 1 9 release a buffer from 7
2 0 1 0 1 8 release a buffer from 7 (last ref to shared buf)
== == == == == ===================================================

gpu.buffer.stats
A read-only flat-keyed file which exists on all cgroups. Each
entry is keyed by the drm device's major:minor.

Total GEM buffer allocation in bytes.

Change-Id: Ibc1f646ca7dbc588e2d11802b156b524696a23e7
Signed-off-by: Kenny Ho <Kenny.Ho-5C7GfCeVMHo@public.gmane.org>
---
Documentation/admin-guide/cgroup-v2.rst | 50 +++++++++-
drivers/gpu/drm/drm_gem.c | 9 ++
include/drm/drm_cgroup.h | 16 +++
include/drm/drm_gem.h | 10 ++
include/linux/cgroup_drm.h | 6 ++
kernel/cgroup/drm.c | 126 ++++++++++++++++++++++++
6 files changed, 216 insertions(+), 1 deletion(-)

diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 7deff912185e..c041e672cc10 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -63,6 +63,7 @@ v1 is available under Documentation/admin-guide/cgroup-v1/.
5-7-1. RDMA Interface Files
5-8. GPU
5-8-1. GPU Interface Files
+ 5-8-2. GEM Buffer Ownership
5-9. Misc
5-9-1. perf_event
5-N. Non-normative information
@@ -2068,7 +2069,54 @@ of GPU-related resources.
GPU Interface Files
~~~~~~~~~~~~~~~~~~~~

-TODO
+ gpu.buffer.stats
+ A read-only flat-keyed file which exists on all cgroups. Each
+ entry is keyed by the drm device's major:minor.
+
+ Total GEM buffer allocation in bytes.
+
+GEM Buffer Ownership
+~~~~~~~~~~~~~~~~~~~~
+
+For the purpose of cgroup accounting and limiting, ownership of the
+buffer is deemed to be the cgroup for which the allocating process
+belongs to. There is one cgroup stats per drm device. Each allocation
+is charged to the owning cgroup as well as all its ancestors.
+
+Similar to the memory cgroup, migrating a process to a different cgroup
+does not move the GEM buffer usages that the process started while in
+previous cgroup, to the new cgroup.
+
+The following is an example to illustrate some of the operations. Given
+the following cgroup hierarchy (The letters are cgroup names with R
+being the root cgroup. The numbers in brackets are processes. The
+processes are placed with cgroup's 'No Internal Process Constraint' in
+mind, so no process is placed in cgroup B.)
+
+R (4, 5) ------ A (6)
+ \
+ B ---- C (7,8)
+ \
+ D (9)
+
+Here is a list of operation and the associated effect on the size
+track by the cgroups (for simplicity, each buffer is 1 unit in size.)
+
+== == == == == ===================================================
+R A B C D Ops
+== == == == == ===================================================
+1 0 0 0 0 4 allocated a buffer
+1 0 0 0 0 4 shared a buffer with 5
+1 0 0 0 0 4 shared a buffer with 9
+2 0 1 0 1 9 allocated a buffer
+3 0 2 1 1 7 allocated a buffer
+3 0 2 1 1 7 shared a buffer with 8
+3 0 2 1 1 7 sharing with 9
+3 0 2 1 1 7 release a buffer
+3 0 2 1 1 7 migrate to cgroup D
+3 0 2 1 1 9 release a buffer from 7
+2 0 1 0 1 8 release a buffer from 7 (last ref to shared buf)
+== == == == == ===================================================

Misc
diff --git a/drivers/gpu/drm/drm_gem.c b/drivers/gpu/drm/drm_gem.c
index a9e4a610445a..589f8f6bde2c 100644
--- a/drivers/gpu/drm/drm_gem.c
+++ b/drivers/gpu/drm/drm_gem.c
@@ -38,6 +38,7 @@
#include <linux/dma-buf.h>
#include <linux/mem_encrypt.h>
#include <linux/pagevec.h>
+#include <linux/cgroup_drm.h>

#include <drm/drm.h>
#include <drm/drm_device.h>
@@ -46,6 +47,7 @@
#include <drm/drm_gem.h>
#include <drm/drm_print.h>
#include <drm/drm_vma_manager.h>
+#include <drm/drm_cgroup.h>

#include "drm_internal.h"

@@ -164,6 +166,9 @@ void drm_gem_private_object_init(struct drm_device *dev,
obj->resv = &obj->_resv;

drm_vma_node_reset(&obj->vma_node);
+
+ obj->drmcg = drmcg_get(current);
+ drmcg_chg_bo_alloc(obj->drmcg, dev, size);
}
EXPORT_SYMBOL(drm_gem_private_object_init);

@@ -957,6 +962,10 @@ drm_gem_object_release(struct drm_gem_object *obj)
fput(obj->filp);

dma_resv_fini(&obj->_resv);
+
+ drmcg_unchg_bo_alloc(obj->drmcg, obj->dev, obj->size);
+ drmcg_put(obj->drmcg);
+
drm_gem_free_mmap_offset(obj);
}
EXPORT_SYMBOL(drm_gem_object_release);
diff --git a/include/drm/drm_cgroup.h b/include/drm/drm_cgroup.h
index fda426fba035..1eb3012e16a1 100644
--- a/include/drm/drm_cgroup.h
+++ b/include/drm/drm_cgroup.h
@@ -26,6 +26,12 @@ void drmcg_unregister_dev(struct drm_device *dev);

void drmcg_device_early_init(struct drm_device *device);

+void drmcg_chg_bo_alloc(struct drmcg *drmcg, struct drm_device *dev,
+ size_t size);
+
+void drmcg_unchg_bo_alloc(struct drmcg *drmcg, struct drm_device *dev,
+ size_t size);
+
#else

struct drmcg_props {
@@ -53,5 +59,15 @@ static inline void drmcg_device_early_init(struct drm_device *device)
{
}

+static inline void drmcg_chg_bo_alloc(struct drmcg *drmcg,
+ struct drm_device *dev, size_t size)
+{
+}
+
+static inline void drmcg_unchg_bo_alloc(struct drmcg *drmcg,
+ struct drm_device *dev, size_t size)
+{
+}
+
#endif /* CONFIG_CGROUP_DRM */
#endif /* __DRM_CGROUP_H__ */
diff --git a/include/drm/drm_gem.h b/include/drm/drm_gem.h
index 0b375069cd48..9c588c329da0 100644
--- a/include/drm/drm_gem.h
+++ b/include/drm/drm_gem.h
@@ -310,6 +310,16 @@ struct drm_gem_object {
*
*/
const struct drm_gem_object_funcs *funcs;
+
+ /**
+ * @drmcg:
+ *
+ * DRM cgroup this GEM object belongs to.
+ *
+ * This is used to track and limit the amount of GEM objects a user
+ * can allocate.
+ */
+ struct drmcg *drmcg;
};

/**
diff --git a/include/linux/cgroup_drm.h b/include/linux/cgroup_drm.h
index ff94b48aa2dc..34b0aec7c964 100644
--- a/include/linux/cgroup_drm.h
+++ b/include/linux/cgroup_drm.h
@@ -11,6 +11,11 @@
/* limit defined per the way drm_minor_alloc operates */
#define MAX_DRM_DEV (64 * DRM_MINOR_RENDER)

+enum drmcg_res_type {
+ DRMCG_TYPE_BO_TOTAL,
+ __DRMCG_TYPE_LAST,
+};
+
#ifdef CONFIG_CGROUP_DRM

/**
@@ -18,6 +23,7 @@
*/
struct drmcg_device_resource {
/* for per device stats */
+ s64 bo_stats_total_allocated;
};

/**
diff --git a/kernel/cgroup/drm.c b/kernel/cgroup/drm.c
index 351df517d5a6..addb096edac5 100644
--- a/kernel/cgroup/drm.c
+++ b/kernel/cgroup/drm.c
@@ -13,6 +13,7 @@
#include <drm/drm_file.h>
#include <drm/drm_drv.h>
#include <drm/drm_device.h>
+#include <drm/drm_ioctl.h>
#include <drm/drm_cgroup.h>

static struct drmcg *root_drmcg __read_mostly;
@@ -26,6 +27,18 @@ static struct drm_minor (*(*acquire_drm_minor)(unsigned int minor_id));

static void (*put_drm_dev)(struct drm_device *dev);

+#define DRMCG_CTF_PRIV_SIZE 3
+#define DRMCG_CTF_PRIV_MASK GENMASK((DRMCG_CTF_PRIV_SIZE - 1), 0)
+#define DRMCG_CTF_PRIV(res_type, f_type) ((res_type) <<\
+ DRMCG_CTF_PRIV_SIZE | (f_type))
+#define DRMCG_CTF_PRIV2RESTYPE(priv) ((priv) >> DRMCG_CTF_PRIV_SIZE)
+#define DRMCG_CTF_PRIV2FTYPE(priv) ((priv) & DRMCG_CTF_PRIV_MASK)
+
+
+enum drmcg_file_type {
+ DRMCG_FTYPE_STATS,
+};
+
/**
* drmcg_bind - Bind DRM subsystem to cgroup subsystem
* @acq_dm: function pointer to the drm_minor_acquire function
@@ -252,7 +265,66 @@ drmcg_css_alloc(struct cgroup_subsys_state *parent_css)
return &drmcg->css;
}

+static void drmcg_print_stats(struct drmcg_device_resource *ddr,
+ struct seq_file *sf, enum drmcg_res_type type)
+{
+ if (ddr == NULL) {
+ seq_puts(sf, "\n");
+ return;
+ }
+
+ switch (type) {
+ case DRMCG_TYPE_BO_TOTAL:
+ seq_printf(sf, "%lld\n", ddr->bo_stats_total_allocated);
+ break;
+ default:
+ seq_puts(sf, "\n");
+ break;
+ }
+}
+
+static int drmcg_seq_show_fn(int id, void *ptr, void *data)
+{
+ struct drm_minor *minor = ptr;
+ struct seq_file *sf = data;
+ struct drmcg *drmcg = css_to_drmcg(seq_css(sf));
+ enum drmcg_file_type f_type =
+ DRMCG_CTF_PRIV2FTYPE(seq_cft(sf)->private);
+ enum drmcg_res_type type =
+ DRMCG_CTF_PRIV2RESTYPE(seq_cft(sf)->private);
+ struct drmcg_device_resource *ddr;
+
+ if (minor->type != DRM_MINOR_PRIMARY)
+ return 0;
+
+ ddr = drmcg->dev_resources[minor->index];
+
+ seq_printf(sf, "%d:%d ", DRM_MAJOR, minor->index);
+
+ switch (f_type) {
+ case DRMCG_FTYPE_STATS:
+ drmcg_print_stats(ddr, sf, type);
+ break;
+ default:
+ seq_puts(sf, "\n");
+ break;
+ }
+
+ return 0;
+}
+
+int drmcg_seq_show(struct seq_file *sf, void *v)
+{
+ return drm_minor_for_each(&drmcg_seq_show_fn, sf);
+}
+
struct cftype files[] = {
+ {
+ .name = "buffer.total.stats",
+ .seq_show = drmcg_seq_show,
+ .private = DRMCG_CTF_PRIV(DRMCG_TYPE_BO_TOTAL,
+ DRMCG_FTYPE_STATS),
+ },
{ } /* terminate */
};

@@ -276,3 +348,57 @@ void drmcg_device_early_init(struct drm_device *dev)
drmcg_update_cg_tree(dev);
}
EXPORT_SYMBOL(drmcg_device_early_init);
+
+/**
+ * drmcg_chg_bo_alloc - charge GEM buffer usage for a device and cgroup
+ * @drmcg: the DRM cgroup to be charged to
+ * @dev: the device the usage should be charged to
+ * @size: size of the GEM buffer to be accounted for
+ *
+ * This function should be called when a new GEM buffer is allocated to account
+ * for the utilization. This should not be called when the buffer is shared (
+ * the GEM buffer's reference count being incremented.)
+ */
+void drmcg_chg_bo_alloc(struct drmcg *drmcg, struct drm_device *dev,
+ size_t size)
+{
+ struct drmcg_device_resource *ddr;
+ int devIdx = dev->primary->index;
+
+ if (drmcg == NULL)
+ return;
+
+ mutex_lock(&dev->drmcg_mutex);
+ for ( ; drmcg != NULL; drmcg = drmcg_parent(drmcg)) {
+ ddr = drmcg->dev_resources[devIdx];
+
+ ddr->bo_stats_total_allocated += (s64)size;
+ }
+ mutex_unlock(&dev->drmcg_mutex);
+}
+EXPORT_SYMBOL(drmcg_chg_bo_alloc);
+
+/**
+ * drmcg_unchg_bo_alloc -
+ * @drmcg: the DRM cgroup to uncharge from
+ * @dev: the device the usage should be removed from
+ * @size: size of the GEM buffer to be accounted for
+ *
+ * This function should be called when the GEM buffer is about to be freed (
+ * not simply when the GEM buffer's reference count is being decremented.)
+ */
+void drmcg_unchg_bo_alloc(struct drmcg *drmcg, struct drm_device *dev,
+ size_t size)
+{
+ int devIdx = dev->primary->index;
+
+ if (drmcg == NULL)
+ return;
+
+ mutex_lock(&dev->drmcg_mutex);
+ for ( ; drmcg != NULL; drmcg = drmcg_parent(drmcg))
+ drmcg->dev_resources[devIdx]->bo_stats_total_allocated
+ -= (s64)size;
+ mutex_unlock(&dev->drmcg_mutex);
+}
+EXPORT_SYMBOL(drmcg_unchg_bo_alloc);
--
2.25.0