AI智能摘要
深入探索eBPF技术,该方案通过非侵入式方式在Linux内核中高效运行自定义字节码,实现对kmalloc内存分配的实时监控,兼具安全性和灵活性。文章详细展示了内核态插桩与用户态应用的协同原理与代码实践,支持多维过滤和实用输出,适合于系统性能分析及生产环境部署,为内核行为追踪和资源优化提供了极具价值的案例
此摘要由AI分析文章内容生成,仅供参考。

最近也是因为一些灵感需要进行内核探测,从而了解到了eBPF。所以就开始了eBPF的学习之路,大体了解了一下太强大了!在不破坏内核代码的情况下,实现内核监控,性能开销极低,完全可以用于生产环境!这一篇也是我自己摸索实现的第一个功能,监控内核kmalloc的调用情况。

eBPF 技术概述

什么是 eBPF?

eBPF 是 Linux 内核的一个革命性技术,它允许用户空间程序在不修改内核源代码的情况下,安全、高效地在内核中运行自定义的字节码。eBPF 最初设计用于网络包过滤,现已扩展到系统监控、性能分析、安全等领域。

eBPF 的关键特性

  1. 安全性:所有 eBPF 程序必须通过内核验证器的严格检查,确保不会导致内核崩溃或数据损坏。

  2. 高性能:eBPF 程序在内核空间运行,避免了用户态和内核态之间的上下文切换开销。

  3. 灵活性:可以挂载到多种内核事件点(tracepoints、kprobes、uprobes 等)。

  4. 可编程性:支持复杂的数据结构和算法,包括循环(有限制)和分支。

kmalloc 监控

目标

创建一个能够实时监控和记录内核 kmalloc 调用的工具,需要捕获:

  • 调用进程的 PID 和名称

  • 请求分配的内存大小

  • 实际分配的内存大小

  • 分配标志(gfp_flags)

  • NUMA 节点信息

  • 时间戳

实例

内核态插桩

// kmalloc.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "kmalloc.h"

char LICENSE[] SEC("license") = "GPL";

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
    __uint(max_entries, 1024);
} kmalloc_events SEC(".maps");

// 定义一个结构体来匹配 tracepoint 参数
struct trace_kmalloc_args {
    unsigned long long unused;  
    unsigned long call_site;
    const void *ptr;
    size_t bytes_req;
    size_t bytes_alloc;
    unsigned long gfp_flags;
    int node;
};

static __always_inline bool comm_is_test_kmalloc(const char *comm)
{
    // 直接比较前11个字符("test_kmalloc" 的长度是11)
    // 这样可以避免循环,提高性能
    const char target[12] = "test_kmalloc";  // 11个字符 + 1个结束符
    
    // 使用内联汇编或直接比较
    if (comm[0] != 't') return false;
    if (comm[1] != 'e') return false;
    if (comm[2] != 's') return false;
    if (comm[3] != 't') return false;
    if (comm[4] != '_') return false;
    if (comm[5] != 'k') return false;
    if (comm[6] != 'm') return false;
    if (comm[7] != 'a') return false;
    if (comm[8] != 'l') return false;
    if (comm[9] != 'l') return false;
    if (comm[10] != 'o') return false;
    if (comm[11] != 'c') return false;
    if (comm[12] != '\0') return false;  // 确保后面是结束符
    
    return true;
}

// 使用原始 tracepoint 访问方式
SEC("tracepoint/kmem/kmalloc")
int trace_kmalloc(void *ctx)
{
    struct kmalloc_event event = {};
    struct trace_kmalloc_args *args = ctx;
    
    // 获取进程信息
    u64 pid_tgid = bpf_get_current_pid_tgid();
    event.pid = pid_tgid & 0xFFFFFFFF;
    event.tgid = pid_tgid >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    
    bpf_printk("Tracepoint triggered by: %s (PID: %d)", 
               event.comm, event.pid);

    if (!comm_is_test_kmalloc(event.comm)) {
        return 0;  // 不是 test_kmalloc 进程,直接返回
    }

    // 直接通过指针访问参数
    unsigned long long *raw_args = (unsigned long long *)((char *)ctx + 8);
    
    // 手动读取参数
    bpf_probe_read_kernel(&event.call_site, sizeof(event.call_site), &raw_args[0]);
    bpf_probe_read_kernel(&event.ptr, sizeof(event.ptr), &raw_args[1]);
    bpf_probe_read_kernel(&event.bytes_req, sizeof(event.bytes_req), &raw_args[2]);
    bpf_probe_read_kernel(&event.bytes_alloc, sizeof(event.bytes_alloc), &raw_args[3]);
    bpf_probe_read_kernel(&event.gfp_flags, sizeof(event.gfp_flags), &raw_args[4]);
    bpf_probe_read_kernel(&event.node, sizeof(event.node), &raw_args[5]);
    
    event.timestamp_ns = bpf_ktime_get_ns();
    
    // 发送事件到用户空间
    bpf_perf_event_output(ctx, &kmalloc_events, BPF_F_CURRENT_CPU, &event, sizeof(event));
    
    return 0;
}

应用层监控

// kmalloc.c - 用户态程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "kmalloc.skel.h"
#include "kmalloc.h"

static volatile bool exiting = false;

// 信号处理函数
static void sig_handler(int sig)
{
    fprintf(stderr, "\nSignal received, exiting...\n");
    exiting = true;
}

// 处理从内核传来的 kmalloc 事件
static void handle_kmalloc_event(void *ctx, int cpu, void *data, __u32 size)
{
    struct kmalloc_event *e = data;
    struct tm *tm;
    char ts[32];
    time_t t;
    
    // 获取当前时间
    time(&t);
    tm = localtime(&t);
    strftime(ts, sizeof(ts), "%H:%M:%S", tm);
    
    // 格式化输出事件信息
    printf("[%s] PID: %-6d | TGID: %-6d | COMM: %-16s | "
           "SIZE_REQ: %-8llu | SIZE_ALLOC: %-8llu | "
           "GFP_FLAGS: 0x%-8llx | NODE: %d | ADDR: 0x%llx\n",
           ts, e->pid, e->tgid, e->comm, 
           e->bytes_req, e->bytes_alloc,
           e->gfp_flags, e->node, e->ptr);
}

// 打印使用说明
static void print_usage(const char *prog_name)
{
    printf("Usage: %s [options]\n", prog_name);
    printf("Options:\n");
    printf("  -h, --help     Show this help message\n");
    printf("  -f, --filter   Filter by process name\n");
    printf("  -p, --pid      Filter by process ID\n");
    printf("  -s, --size     Only show allocations larger than SIZE bytes\n");
    printf("  -t, --top      Show top N processes by allocation count\n");
    printf("\nExample:\n");
    printf("  %s -f chrome         # Monitor only Chrome processes\n", prog_name);
    printf("  %s -s 1024          # Show only allocations > 1KB\n", prog_name);
    printf("  %s -p 1234          # Monitor only PID 1234\n", prog_name);
}

// 提升内存限制
static void bump_memlock_rlimit(void)
{
    struct rlimit rlim_new = {
        .rlim_cur = RLIM_INFINITY,
        .rlim_max = RLIM_INFINITY,
    };
    
    if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
        fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit: %s\n", strerror(errno));
        exit(1);
    }
    
    printf("RLIMIT_MEMLOCK set to unlimited\n");
}

// 初始化信号处理
static void init_signals(void)
{
    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);
    signal(SIGHUP, sig_handler);
}

// 程序主函数
int main(int argc, char **argv)
{
    struct kmalloc_bpf *skel = NULL;
    struct perf_buffer *pb = NULL;
    struct perf_buffer_opts pb_opts = {};
    int err;
    
    // 解析命令行参数
    int opt;
    char *filter_comm = NULL;
    pid_t filter_pid = 0;
    size_t min_size = 0;
    int top_n = 0;
    
    while ((opt = getopt(argc, argv, "hf:p:s:t:")) != -1) {
        switch (opt) {
            case 'h':
                print_usage(argv[0]);
                return 0;
            case 'f':
                filter_comm = optarg;
                printf("Filtering by process name: %s\n", filter_comm);
                break;
            case 'p':
                filter_pid = atoi(optarg);
                printf("Filtering by PID: %d\n", filter_pid);
                break;
            case 's':
                min_size = atoll(optarg);
                printf("Minimum allocation size: %zu bytes\n", min_size);
                break;
            case 't':
                top_n = atoi(optarg);
                printf("Will show top %d processes\n", top_n);
                break;
            default:
                print_usage(argv[0]);
                return 1;
        }
    }
    
    // 初始化
    init_signals();
    bump_memlock_rlimit();
    
    printf("========================================\n");
    printf("KMALLOC Monitor - Tracing Memory Allocations\n");
    printf("========================================\n");
    printf("Kernel Version: ");
    fflush(stdout);
    system("uname -r");
    printf("\n");
    
    // 1. 打开并加载 BPF 程序
    printf("[1/3] Opening and loading BPF skeleton...\n");
    skel = kmalloc_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to open and load BPF skeleton\n");
        return 1;
    }
    
    // 2. 附加 BPF 程序到 tracepoint
    printf("[2/3] Attaching BPF programs...\n");
    err = kmalloc_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
        goto cleanup;
    }
    
    // 3. 设置 perf buffer 接收事件
    printf("[3/3] Setting up perf buffer...\n");
    
    pb_opts.sz = sizeof(pb_opts);
    pb = perf_buffer__new(bpf_map__fd(skel->maps.kmalloc_events), 128,
                          handle_kmalloc_event, NULL, NULL, &pb_opts);
    if (!pb) {
        err = -errno;
        fprintf(stderr, "Failed to create perf buffer: %s\n", strerror(-err));
        goto cleanup;
    }
    
    printf("\nBPF program loaded successfully!\n");
    printf("========================================\n");
    printf("Monitoring kmalloc calls in real-time...\n");
    printf("Press Ctrl+C to stop\n");
    printf("========================================\n\n");
    
    // 打印表头
    printf("%-8s %-6s %-6s %-16s %-10s %-10s %-10s %-4s\n",
           "TIME", "PID", "TGID", "COMMAND", "REQ_B", "ALLOC_B", "GFP_FLAGS", "NODE");
    printf("%-8s %-6s %-6s %-16s %-10s %-10s %-10s %-4s\n",
           "--------", "------", "------", "----------------", 
           "----------", "----------", "----------", "----");
    
    // 4. 主事件循环
    while (!exiting) {
        err = perf_buffer__poll(pb, 100);
        if (err < 0 && err != -EINTR) {
            fprintf(stderr, "Error polling perf buffer: %d\n", err);
            break;
        }
        
        // 定期输出状态(每5秒)
        static time_t last_status = 0;
        time_t now = time(NULL);
        if (now - last_status >= 5) {
            printf("\n[%s] Monitoring... (Press Ctrl+C to stop)\n", ctime(&now));
            last_status = now;
        }
    }
    
    printf("\n========================================\n");
    printf("Shutting down...\n");
    
cleanup:
    // 5. 清理资源
    if (pb) {
        perf_buffer__free(pb);
    }
    
    if (skel) {
        kmalloc_bpf__destroy(skel);
    }
    
    printf("Cleanup complete. Goodbye!\n");
    return err < 0 ? 1 : 0;
}

模拟测试

// test_kmalloc.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <signal.h>

static volatile int running = 1;

void handle_signal(int sig)
{
    running = 0;
    printf("\nSignal received, stopping...\n");
}

// 简单方法:通过文件操作触发内核 kmalloc
void trigger_kmalloc(int size)
{
    char filename[256];
    static int counter = 0;
    int fd;
    char *buffer;
    
    // 创建唯一的文件名
    snprintf(filename, sizeof(filename), "/tmp/kmalloc_test_%d_%d.tmp", getpid(), counter++);
    
    // 打开文件
    fd = open(filename, O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd < 0) {
        return;
    }
    
    // 分配内存并写入文件
    buffer = malloc(size);
    if (buffer) {
        memset(buffer, 'X', size);
        write(fd, buffer, size);
        free(buffer);
    }
    
    close(fd);
    
    // 删除文件
    unlink(filename);
}

int main(int argc, char *argv[])
{
    int count = 100;       // 默认执行100次
    int size = 4096;       // 默认4KB
    int delay = 100000;    // 默认100ms
    
    // 解析参数
    if (argc > 1) count = atoi(argv[1]);
    if (argc > 2) size = atoi(argv[2]);
    if (argc > 3) delay = atoi(argv[3]);
    
    // 设置信号处理
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);
    
    printf("KMALLOC Test Program - PID: %d\n", getpid());
    printf("Will execute %d times, size=%d bytes, delay=%d us\n", 
           count, size, delay);
    printf("Press Ctrl+C to stop\n\n");
    
    for (int i = 0; i < count && running; i++) {
        printf("Triggering kmalloc #%d/%d (size=%d)\n", i+1, count, size);
        
        // 触发 kmalloc
        trigger_kmalloc(size);
        
        // 延迟
        usleep(delay);
    }
    
    printf("\nTest completed!\n");
    return 0;
}

测试结果

启动上层监控程序:

此时会一直轮询监控

执行测试程序

总结

在不修改内核的情况下实现深度监控,很有意思,会继续学习这块!