我的建站学习

linux · observability · c/c++

这里记录的是在老内核环境(2.6 / 3.x)上做无侵入可观测的实践笔记。 工具链以 C/C++ 为主:procfs、sysfs、perf、kprobes、netlink—— 不依赖 eBPF 作为首选,也不靠改应用源码或 LD_PRELOAD 来「偷看」进程。

老内核上,什么叫「无侵入」

procfs sysfs perf kprobes netlink

无侵入不是「完全零成本」,而是不修改被观测应用的二进制或源码。 在老内核上,可用的观测面主要来自内核已暴露的接口: procfs(/proc)、sysfs、perf_event_open、内核探针(kprobes / ftrace)、 以及 netlink 套接字。

LD_PRELOAD 和 ptrace 虽然灵活,但会改变进程行为或需要附加调试器, 在生产环境应作为最后手段。优先走内核提供的只读或事件通道, 采集器自身用 C 写成独立守护进程,与业务进程隔离。

procfs 采集器 — /proc/stat CPU delta

procfs

2.6.x 内核没有 perf 事件接口时,/proc/stat 是最可靠的 CPU 计数来源。 采集器周期性读取各 CPU 行的 user/nice/system/idle 字段,做差分得到利用率。

/* 2.6.x — read /proc/stat, delta idle vs total */
static unsigned long read_cpu_jiffies(int cpu) {
    char path[32], line[256];
    snprintf(path, sizeof(path), "/proc/stat");
    FILE *f = fopen(path, "r");
    if (!f) return 0;
    /* skip to cpuN line, sum fields 1-4 */
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "cpu", 3) == 0 && line[3] == ' ') {
            unsigned long u,n,s,i;
            sscanf(line+5, "%lu %lu %lu %lu", &u, &n, &s, &i);
            fclose(f);
            return u + n + s + i;
        }
    }
    fclose(f);
    return 0;
}

两次采样间隔内的 idle 增量除以 total 增量,即该窗口的 CPU 使用率。 无需 root,无需注入,适合边缘设备上的轻量监控。

perf + uprobes — 不改应用源码

perf uprobes

3.x 起 perf_event_open 可用。uprobes 在用户态函数入口设探针, 采集器通过 mmap 的 perf ring buffer 读事件,应用本身无需链接任何库。

/* 3.x — open perf event on a symbol via uprobes */
int fd = syscall(SYS_perf_event_open,
    &(struct perf_event_attr){
        .type = PERF_TYPE_TRACEPOINT,
        .size = sizeof(struct perf_event_attr),
        .sample_type = PERF_SAMPLE_IP,
        .disabled = 0,
    }, -1, 0, -1, 0);
/* attach uprobe to /path/to/app:symbol via perf_event_open attr */

符号地址可通过 /proc/pid/maps 或 ELF 解析获得。 相比插桩,uprobes 的侵入仅限于内核在函数入口插入一条跳转, 业务逻辑和编译产物保持不变。

kprobes 与 ftrace — trace_pipe 兜底

kprobes

2.6.28+ 支持 kprobes。可在内核函数入口/返回设探针, 配合 ftrace 的 trace_pipe 做实时文本流输出,无需自定义驱动。

/* enable kprobe via debugfs, read trace_pipe */
echo 'p:vfs_read vfs_read' > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/vfs_read/enable
/* collector opens /sys/kernel/debug/tracing/trace_pipe */

当 perf 事件类型不足或需要内核路径级追踪时,kprobes 是标准备选。 trace_pipe 阻塞读即可,采集器用 C 写成单线程循环,开销可控。

网络观测 netlink — RTNETLINK vs /proc/net/tcp

netlink

/proc/net/tcp 适合快照式连接枚举,但无法感知实时变更。 RTNETLINK(AF_NETLINK, SOCK_RAW)可在链路/地址/路由变化时推送事件, 采集器注册 RTMGRP_LINK | RTMGRP_IPV4_IFADDR 即可。

/* open RTNETLINK socket for link/addr events */
int nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
struct sockaddr_nl sa = { .nl_family = AF_NETLINK,
    .nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR };
bind(nl, (struct sockaddr *)&sa, sizeof(sa));
/* recv loop: struct nlmsghdr + ifinfomsg / ifaddrmsg */

老环境若 netlink 权限受限,可退化为定时轮询 /proc/net/tcp; 两者组合能覆盖「当前状态」与「变更通知」两类需求。

为什么老环境慎用 eBPF

procfs perf kprobes

eBPF 程序加载需要内核 ≥ 4.x 且启用 CONFIG_BPF_SYSCALL; 3.10 及更早版本通常不可用。即便 4.x 边缘版本, verifier 能力和 map 类型也远不如现代内核,移植成本高。

务实的组合是:procfs + sysfs 做基线指标, perf 做采样与 uprobes,必要时加 kprobes 补内核路径。 这套栈在 2.6 / 3.x 上经过验证,不依赖新特性,也不绑死某一发行版。