老内核上,什么叫「无侵入」
无侵入不是「完全零成本」,而是不修改被观测应用的二进制或源码。 在老内核上,可用的观测面主要来自内核已暴露的接口: procfs(/proc)、sysfs、perf_event_open、内核探针(kprobes / ftrace)、 以及 netlink 套接字。
LD_PRELOAD 和 ptrace 虽然灵活,但会改变进程行为或需要附加调试器, 在生产环境应作为最后手段。优先走内核提供的只读或事件通道, 采集器自身用 C 写成独立守护进程,与业务进程隔离。
linux · observability · c/c++
这里记录的是在老内核环境(2.6 / 3.x)上做无侵入可观测的实践笔记。 工具链以 C/C++ 为主:procfs、sysfs、perf、kprobes、netlink—— 不依赖 eBPF 作为首选,也不靠改应用源码或 LD_PRELOAD 来「偷看」进程。
无侵入不是「完全零成本」,而是不修改被观测应用的二进制或源码。 在老内核上,可用的观测面主要来自内核已暴露的接口: procfs(/proc)、sysfs、perf_event_open、内核探针(kprobes / ftrace)、 以及 netlink 套接字。
LD_PRELOAD 和 ptrace 虽然灵活,但会改变进程行为或需要附加调试器, 在生产环境应作为最后手段。优先走内核提供的只读或事件通道, 采集器自身用 C 写成独立守护进程,与业务进程隔离。
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,无需注入,适合边缘设备上的轻量监控。
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 的侵入仅限于内核在函数入口插入一条跳转, 业务逻辑和编译产物保持不变。
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 写成单线程循环,开销可控。
/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 程序加载需要内核 ≥ 4.x 且启用 CONFIG_BPF_SYSCALL; 3.10 及更早版本通常不可用。即便 4.x 边缘版本, verifier 能力和 map 类型也远不如现代内核,移植成本高。
务实的组合是:procfs + sysfs 做基线指标, perf 做采样与 uprobes,必要时加 kprobes 补内核路径。 这套栈在 2.6 / 3.x 上经过验证,不依赖新特性,也不绑死某一发行版。