mirror of
https://github.com/torvalds/linux.git
synced 2026-09-13 06:23:02 +02:00
alloc_tag: add ioctl to /proc/allocinfo
Patch series "alloc_tag: introduce IOCTL-based filtering for MAP", v8. Currently, memory allocation profiling data is primarily exposed through /proc/allocinfo. While useful for manual inspection, this text-based interface poses challenges for production monitoring and large-scale analysis: 1. Userspace must parse large amounts of text to extract specific fields. 2. To find specific tags, userspace must read the entire dataset, requiring many context switches and high data copying. 3. The kernel currently aggregates per-CPU counters for every allocation size, even those the user intends to filter out immediately. This series introduces a new IOCTL-based binary interface for allocinfo that supports kernel-side filtering. By allowing the user to specify a filter mask, we significantly reduce the work performed in-kernel and the amount of data transferred to userspace. The IOCTL mechanism was chosen for allocinfo to address the per-CPU counter aggregation bottleneck. A traditional read() operation must report the total allocation count and sizes for every code tag in the system. Doing so requires iterating across all CPUs to sum their per-CPU counters for thousands of tags, which introduces substantial runtime overhead. The IOCTL interface allows userspace to push selective filtering criteria directly into the kernel before the per-CPU counter aggregation. The kernel aggregates per-CPU counters only for a small subset of tags that match the filter. This results in significant performance improvement. Beyond fast filtered retrieval, the IOCTL foundation allows introducing a context capture mechanism in the future to capture the context for specific allocations. Performance measurements were conducted on an Intel Xeon Platinum 8481C (224 CPUs) with caches dropped before each run. The IOCTL mechanism shows a ~20x performance improvement for filtered queries. The kernel avoids the expensive per-CPU counter aggregation (alloc_tag_read) for any tags that fail the initial string or location filters. Scenario 1: Specific File Filtering (arch/x86/events/rapl.c) 1. Traditional (cat /proc/allocinfo | grep): 22ms (sys) 2. IOCTL Interface: 1ms (sys) Scenario 2: Compound Filtering (Filename + Size) 1. Traditional: (cat ... | grep | awk): 21ms (sys) 2. IOCTL Interface: 1ms (sys) Scenario 3: Size-Based Filtering (min_size = 1MB) 1. Traditional: (cat ... | awk): 21ms (sys) 2. IOCTL Interface: 14ms (sys) This patch (of 6): Add the following ioctl commands for /proc/allocinfo file: ALLOCINFO_IOC_CONTENT_ID - gets content identifier which can be used to check whether the file content has changed specifically due to module load/unload. Every time a module is loaded / unloaded, the returned value will be different. By comparing the identifier value at the beginning and at the end of the content retrieval operation, users can validate retrieved information for consistency. ALLOCINFO_IOC_GET_AT - gets the record at the specified position. This is the position of a record in /proc/allocinfo. ALLOCINFO_IOC_GET_NEXT - gets the record next to the last retrieved one. If no records were previously retrieved, returns the first record. Note, function file and module names often have the same prefixes, therefore when filtering for them, we compare the last 64 characters to minimize the chances of name collisions. [akpm@linux-foundation.org: include compat.h, per Suren] Closes: https://lore.kernel.org/oe-kbuild-all/202607091820.qbjlGhKK-lkp@intel.com/ Link: https://lore.kernel.org/cover.1783532853.git.abhishekbapat@google.com Link: https://lore.kernel.org/15596de2607ef13e7c77c6d74763f4ae992ec475.1783532853.git.abhishekbapat@google.com Signed-off-by: Suren Baghdasaryan <surenb@google.com> Signed-off-by: Abhishek Bapat <abhishekbapat@google.com> Acked-by: Hao Ge <hao.ge@linux.dev> Cc: Jonathan Corbet <corbet@lwn.net> Cc: Kent Overstreet <kent.overstreet@linux.dev> Cc: Sourav Panda <souravpanda@google.com> Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
This commit is contained in:
parent
22709abff9
commit
1d581ab234
|
|
@ -57,6 +57,11 @@ sysctl:
|
|||
Runtime info:
|
||||
/proc/allocinfo
|
||||
|
||||
Profiling data can be retrieved either by reading `/proc/allocinfo` directly as
|
||||
text or programmatically via `ioctl()` calls defined in `<uapi/linux/alloc_tag.h>`.
|
||||
The ioctl interface supports structured binary data extraction as well as filtering
|
||||
by module name, function, file, line number, accuracy, or allocation size limits.
|
||||
|
||||
Example output::
|
||||
|
||||
root@moria-kvm:~# sort -g /proc/allocinfo|tail|numfmt --to=iec
|
||||
|
|
|
|||
|
|
@ -346,6 +346,8 @@ Code Seq# Include File Comments
|
|||
<mailto:luzmaximilian@gmail.com>
|
||||
0xA5 20-2F linux/surface_aggregator/dtx.h Microsoft Surface DTX driver
|
||||
<mailto:luzmaximilian@gmail.com>
|
||||
0xA6 00-0F uapi/linux/alloc_tag.h Memory allocation profiling
|
||||
<mailto:surenb@google.com>
|
||||
0xAA 00-3F linux/uapi/linux/userfaultfd.h
|
||||
0xAB 00-1F linux/nbd.h
|
||||
0xAC 00-1F linux/raw.h
|
||||
|
|
|
|||
|
|
@ -16940,6 +16940,7 @@ S: Maintained
|
|||
F: Documentation/mm/allocation-profiling.rst
|
||||
F: include/linux/alloc_tag.h
|
||||
F: include/linux/pgalloc_tag.h
|
||||
F: include/uapi/linux/alloc_tag.h
|
||||
F: mm/alloc_tag.c
|
||||
|
||||
MEMORY MANAGEMENT - BALLOON
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ struct codetag_iterator {
|
|||
void codetag_lock_module_list(struct codetag_type *cttype);
|
||||
bool codetag_trylock_module_list(struct codetag_type *cttype);
|
||||
void codetag_unlock_module_list(struct codetag_type *cttype);
|
||||
unsigned long codetag_get_content_id(struct codetag_type *cttype);
|
||||
unsigned int codetag_get_count(struct codetag_type *cttype);
|
||||
struct codetag_iterator codetag_get_ct_iter(struct codetag_type *cttype);
|
||||
struct codetag *codetag_next_ct(struct codetag_iterator *iter);
|
||||
|
||||
|
|
|
|||
65
include/uapi/linux/alloc_tag.h
Normal file
65
include/uapi/linux/alloc_tag.h
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
||||
/*
|
||||
* alloc_tag IOCTL API definition
|
||||
*
|
||||
* Copyright (C) 2026 Google, LLC. All rights reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2 as
|
||||
* published by the Free Software Foundation.
|
||||
*/
|
||||
|
||||
#ifndef _UAPI_ALLOC_TAG_H
|
||||
#define _UAPI_ALLOC_TAG_H
|
||||
|
||||
#include <linux/types.h>
|
||||
|
||||
/*
|
||||
* Function, file and module names often have the same prefixes, therefore
|
||||
* when filtering by these criteria, we compare the last 64 characters to
|
||||
* minimize the chances of name collisions
|
||||
*/
|
||||
#define ALLOCINFO_STR_SIZE 64
|
||||
|
||||
struct allocinfo_content_id {
|
||||
__u64 id;
|
||||
};
|
||||
|
||||
struct allocinfo_tag {
|
||||
/* Longer names are trimmed */
|
||||
char modname[ALLOCINFO_STR_SIZE];
|
||||
char function[ALLOCINFO_STR_SIZE];
|
||||
char filename[ALLOCINFO_STR_SIZE];
|
||||
__u64 lineno;
|
||||
};
|
||||
|
||||
/* The alignment ensures 32-bit compatible interfaces are not broken */
|
||||
struct allocinfo_counter {
|
||||
__u64 bytes;
|
||||
__u64 calls;
|
||||
__u8 accurate;
|
||||
} __attribute__((aligned(8)));
|
||||
|
||||
struct allocinfo_tag_data {
|
||||
struct allocinfo_tag tag;
|
||||
struct allocinfo_counter counter;
|
||||
};
|
||||
|
||||
struct allocinfo_get_at {
|
||||
__u64 pos; /* input */
|
||||
struct allocinfo_tag_data data;
|
||||
};
|
||||
|
||||
#define _ALLOCINFO_IOC_CONTENT_ID 0
|
||||
#define _ALLOCINFO_IOC_GET_AT 1
|
||||
#define _ALLOCINFO_IOC_GET_NEXT 2
|
||||
|
||||
#define ALLOCINFO_IOC_BASE 0xA6
|
||||
#define ALLOCINFO_IOC_CONTENT_ID _IOR(ALLOCINFO_IOC_BASE, _ALLOCINFO_IOC_CONTENT_ID, \
|
||||
struct allocinfo_content_id)
|
||||
#define ALLOCINFO_IOC_GET_AT _IOWR(ALLOCINFO_IOC_BASE, _ALLOCINFO_IOC_GET_AT, \
|
||||
struct allocinfo_get_at)
|
||||
#define ALLOCINFO_IOC_GET_NEXT _IOR(ALLOCINFO_IOC_BASE, _ALLOCINFO_IOC_GET_NEXT, \
|
||||
struct allocinfo_tag_data)
|
||||
|
||||
#endif /* _UAPI_ALLOC_TAG_H */
|
||||
|
|
@ -19,6 +19,8 @@ struct codetag_type {
|
|||
struct codetag_type_desc desc;
|
||||
/* generates unique sequence number for module load */
|
||||
unsigned long next_mod_seq;
|
||||
/* bumped on every module load and unload */
|
||||
unsigned long content_id;
|
||||
};
|
||||
|
||||
struct codetag_range {
|
||||
|
|
@ -50,6 +52,20 @@ void codetag_unlock_module_list(struct codetag_type *cttype)
|
|||
up_read(&cttype->mod_lock);
|
||||
}
|
||||
|
||||
unsigned long codetag_get_content_id(struct codetag_type *cttype)
|
||||
{
|
||||
lockdep_assert_held(&cttype->mod_lock);
|
||||
|
||||
return cttype->content_id;
|
||||
}
|
||||
|
||||
unsigned int codetag_get_count(struct codetag_type *cttype)
|
||||
{
|
||||
lockdep_assert_held(&cttype->mod_lock);
|
||||
|
||||
return cttype->count;
|
||||
}
|
||||
|
||||
struct codetag_iterator codetag_get_ct_iter(struct codetag_type *cttype)
|
||||
{
|
||||
struct codetag_iterator iter = {
|
||||
|
|
@ -204,6 +220,7 @@ static int codetag_module_init(struct codetag_type *cttype, struct module *mod)
|
|||
|
||||
down_write(&cttype->mod_lock);
|
||||
cmod->mod_seq = ++cttype->next_mod_seq;
|
||||
++cttype->content_id;
|
||||
mod_id = idr_alloc(&cttype->mod_idr, cmod, 0, 0, GFP_KERNEL);
|
||||
if (mod_id >= 0) {
|
||||
if (cttype->desc.module_load) {
|
||||
|
|
@ -368,6 +385,7 @@ void codetag_unload_module(struct module *mod)
|
|||
cttype->count -= range_size(cttype, &cmod->range);
|
||||
idr_remove(&cttype->mod_idr, mod_id);
|
||||
kfree(cmod);
|
||||
++cttype->content_id;
|
||||
}
|
||||
up_write(&cttype->mod_lock);
|
||||
if (found && cttype->desc.free_section_mem)
|
||||
|
|
|
|||
239
mm/alloc_tag.c
239
mm/alloc_tag.c
|
|
@ -5,6 +5,8 @@
|
|||
#include <linux/gfp.h>
|
||||
#include <linux/kallsyms.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/mutex.h>
|
||||
#include <linux/compat.h>
|
||||
#include <linux/page_ext.h>
|
||||
#include <linux/pgalloc_tag.h>
|
||||
#include <linux/proc_fs.h>
|
||||
|
|
@ -14,6 +16,7 @@
|
|||
#include <linux/string_choices.h>
|
||||
#include <linux/vmalloc.h>
|
||||
#include <linux/kmemleak.h>
|
||||
#include <uapi/linux/alloc_tag.h>
|
||||
|
||||
#include "internal.h"
|
||||
#include "page_alloc.h"
|
||||
|
|
@ -59,6 +62,10 @@ struct allocinfo_private {
|
|||
struct codetag_iterator iter;
|
||||
struct codetag_iterator reported_iter;
|
||||
bool print_header;
|
||||
/* ioctl uses a separate iterator not to interfere with reads */
|
||||
struct codetag_iterator ioctl_iter;
|
||||
bool positioned; /* seq_open_private() sets to 0 */
|
||||
struct mutex ioctl_lock;
|
||||
};
|
||||
|
||||
static void *allocinfo_start(struct seq_file *m, loff_t *pos)
|
||||
|
|
@ -142,6 +149,235 @@ static const struct seq_operations allocinfo_seq_op = {
|
|||
.show = allocinfo_show,
|
||||
};
|
||||
|
||||
/*
|
||||
* Initializes seq_file operations and allocates private state when opening
|
||||
* the /proc/allocinfo procfs entry.
|
||||
*/
|
||||
static int allocinfo_open(struct inode *inode, struct file *file)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = seq_open_private(file, &allocinfo_seq_op,
|
||||
sizeof(struct allocinfo_private));
|
||||
if (!ret) {
|
||||
struct seq_file *m = file->private_data;
|
||||
struct allocinfo_private *priv = m->private;
|
||||
|
||||
mutex_init(&priv->ioctl_lock);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Cleans up the seq_file state and frees up the private state allocated in
|
||||
* allocinfo_open() when closing the /proc/allocinfo file descriptor.
|
||||
*/
|
||||
static int allocinfo_release(struct inode *inode, struct file *file)
|
||||
{
|
||||
struct seq_file *m = file->private_data;
|
||||
struct allocinfo_private *priv = m->private;
|
||||
|
||||
mutex_destroy(&priv->ioctl_lock);
|
||||
return seq_release_private(inode, file);
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns a pointer to the suffix of a string so that its length fits within
|
||||
* ALLOCINFO_STR_SIZE, preserving the trailing characters.
|
||||
* Function, file and module names often have the same prefixes, therefore
|
||||
* when filtering by these criteria, we compare the last 64 characters to
|
||||
* minimize the chances of name collisions
|
||||
*/
|
||||
static const char *allocinfo_str(const char *str)
|
||||
{
|
||||
size_t len = strlen(str);
|
||||
|
||||
/* Keep an extra space for the trailing NULL. */
|
||||
if (len >= ALLOCINFO_STR_SIZE)
|
||||
str += (len - ALLOCINFO_STR_SIZE) + 1;
|
||||
return str;
|
||||
}
|
||||
|
||||
/* Copy a string and trim from the beginning if it's too long */
|
||||
static void allocinfo_copy_str(char *dest, const char *src)
|
||||
{
|
||||
strscpy_pad(dest, allocinfo_str(src), ALLOCINFO_STR_SIZE);
|
||||
}
|
||||
|
||||
/*
|
||||
* Populates the UAPI allocinfo_tag_data structure with active runtime
|
||||
* profiling counters extracted from the given kernel codetag.
|
||||
*/
|
||||
static void allocinfo_to_params(struct codetag *ct,
|
||||
struct allocinfo_tag_data *data)
|
||||
{
|
||||
struct alloc_tag *tag = ct_to_alloc_tag(ct);
|
||||
struct alloc_tag_counters counter = alloc_tag_read(tag);
|
||||
|
||||
if (ct->modname)
|
||||
allocinfo_copy_str(data->tag.modname, ct->modname);
|
||||
else
|
||||
data->tag.modname[0] = '\0';
|
||||
allocinfo_copy_str(data->tag.function, ct->function);
|
||||
allocinfo_copy_str(data->tag.filename, ct->filename);
|
||||
data->tag.lineno = ct->lineno;
|
||||
data->counter.bytes = counter.bytes;
|
||||
data->counter.calls = counter.calls;
|
||||
data->counter.accurate = !alloc_tag_is_inaccurate(tag);
|
||||
}
|
||||
|
||||
/*
|
||||
* Retrieves the unique content ID representing the current allocation tag module
|
||||
* layout, allowing userspace to detect if modules were loaded / unloaded.
|
||||
*/
|
||||
static int allocinfo_ioctl_get_content_id(struct seq_file *m, void __user *arg)
|
||||
{
|
||||
struct allocinfo_content_id params;
|
||||
|
||||
codetag_lock_module_list(alloc_tag_cttype);
|
||||
params.id = codetag_get_content_id(alloc_tag_cttype);
|
||||
codetag_unlock_module_list(alloc_tag_cttype);
|
||||
if (copy_to_user(arg, ¶ms, sizeof(params)))
|
||||
return -EFAULT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Seeks the ioctl iterator to the specified 0-indexed tag position, reads its
|
||||
* profiling data and returns it to userspace.
|
||||
*/
|
||||
static int allocinfo_ioctl_get_at(struct seq_file *m, void __user *arg)
|
||||
{
|
||||
struct allocinfo_private *priv;
|
||||
struct codetag *ct;
|
||||
__u64 pos;
|
||||
struct allocinfo_get_at params = {0};
|
||||
|
||||
if (copy_from_user(¶ms, arg, sizeof(params)))
|
||||
return -EFAULT;
|
||||
|
||||
priv = m->private;
|
||||
pos = params.pos;
|
||||
|
||||
mutex_lock(&priv->ioctl_lock);
|
||||
codetag_lock_module_list(alloc_tag_cttype);
|
||||
|
||||
if (pos >= codetag_get_count(alloc_tag_cttype)) {
|
||||
codetag_unlock_module_list(alloc_tag_cttype);
|
||||
mutex_unlock(&priv->ioctl_lock);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
/* Find the codetag */
|
||||
priv->ioctl_iter = codetag_get_ct_iter(alloc_tag_cttype);
|
||||
ct = codetag_next_ct(&priv->ioctl_iter);
|
||||
while (ct && pos--)
|
||||
ct = codetag_next_ct(&priv->ioctl_iter);
|
||||
if (ct) {
|
||||
allocinfo_to_params(ct, ¶ms.data);
|
||||
priv->positioned = true;
|
||||
}
|
||||
|
||||
codetag_unlock_module_list(alloc_tag_cttype);
|
||||
mutex_unlock(&priv->ioctl_lock);
|
||||
|
||||
if (!ct)
|
||||
return -ENOENT;
|
||||
|
||||
if (copy_to_user(arg, ¶ms, sizeof(params)))
|
||||
return -EFAULT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Advances the ioctl iterator to the next allocation tag in the sequence and
|
||||
* returns its profiling data to userspace.
|
||||
*/
|
||||
static int allocinfo_ioctl_get_next(struct seq_file *m, void __user *arg)
|
||||
{
|
||||
struct allocinfo_private *priv;
|
||||
struct codetag *ct;
|
||||
struct allocinfo_tag_data params;
|
||||
int ret = 0;
|
||||
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
priv = m->private;
|
||||
|
||||
mutex_lock(&priv->ioctl_lock);
|
||||
codetag_lock_module_list(alloc_tag_cttype);
|
||||
|
||||
if (!priv->positioned) {
|
||||
priv->ioctl_iter = codetag_get_ct_iter(alloc_tag_cttype);
|
||||
priv->positioned = true;
|
||||
}
|
||||
|
||||
ct = codetag_next_ct(&priv->ioctl_iter);
|
||||
if (ct)
|
||||
allocinfo_to_params(ct, ¶ms);
|
||||
|
||||
if (!ct) {
|
||||
priv->positioned = false;
|
||||
ret = -ENOENT;
|
||||
}
|
||||
codetag_unlock_module_list(alloc_tag_cttype);
|
||||
mutex_unlock(&priv->ioctl_lock);
|
||||
|
||||
if (ret == 0) {
|
||||
if (copy_to_user(arg, ¶ms, sizeof(params)))
|
||||
return -EFAULT;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Entry point ioctl function for /proc/allocinfo routing requests to fetch the
|
||||
* layout content ID, seek to a specific tag, or read sequential tags.
|
||||
*/
|
||||
static long allocinfo_ioctl(struct file *file, unsigned int cmd,
|
||||
unsigned long __arg)
|
||||
{
|
||||
void __user *arg = (void __user *)__arg;
|
||||
int ret;
|
||||
|
||||
switch (cmd) {
|
||||
case ALLOCINFO_IOC_CONTENT_ID:
|
||||
ret = allocinfo_ioctl_get_content_id(file->private_data, arg);
|
||||
break;
|
||||
case ALLOCINFO_IOC_GET_AT:
|
||||
ret = allocinfo_ioctl_get_at(file->private_data, arg);
|
||||
break;
|
||||
case ALLOCINFO_IOC_GET_NEXT:
|
||||
ret = allocinfo_ioctl_get_next(file->private_data, arg);
|
||||
break;
|
||||
default:
|
||||
ret = -ENOIOCTLCMD;
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_COMPAT
|
||||
static long allocinfo_compat_ioctl(struct file *file, unsigned int cmd,
|
||||
unsigned long arg)
|
||||
{
|
||||
return allocinfo_ioctl(file, cmd, (unsigned long)compat_ptr(arg));
|
||||
}
|
||||
#endif
|
||||
|
||||
static const struct proc_ops allocinfo_proc_ops = {
|
||||
.proc_open = allocinfo_open,
|
||||
.proc_read_iter = seq_read_iter,
|
||||
.proc_lseek = seq_lseek,
|
||||
.proc_release = allocinfo_release,
|
||||
.proc_ioctl = allocinfo_ioctl,
|
||||
#ifdef CONFIG_COMPAT
|
||||
.proc_compat_ioctl = allocinfo_compat_ioctl,
|
||||
#endif
|
||||
};
|
||||
|
||||
size_t alloc_tag_top_users(struct codetag_bytes *tags, size_t count, bool can_sleep)
|
||||
{
|
||||
struct codetag_iterator iter;
|
||||
|
|
@ -999,8 +1235,7 @@ static int __init alloc_tag_init(void)
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (!proc_create_seq_private(ALLOCINFO_FILE_NAME, 0400, NULL, &allocinfo_seq_op,
|
||||
sizeof(struct allocinfo_private), NULL)) {
|
||||
if (!proc_create(ALLOCINFO_FILE_NAME, 0400, NULL, &allocinfo_proc_ops)) {
|
||||
pr_err("Failed to create %s file\n", ALLOCINFO_FILE_NAME);
|
||||
shutdown_mem_profiling(false);
|
||||
return -ENOMEM;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user