导读:本期聚焦于兔子创作的《如何使用bpf_get_current_pidns_info获取eBPF程序中的PID命名空间信息?》,敬请观看详情。容器环境中传统PID信息已经无法准确标识进程身份,同一台宿主机上的不同容器可能存在相同的PID号,这给基于eBPF的系统监控和安全审计带来了很大困扰。本文围绕eBPF辅助函数bpf_get_current_pidns_info展开,详细讲解它的结构定义、调用方式、返回字段含义,以及如何借助它区分容器内PID与宿主机全局PID,实现真正意义上的观测隔离。文中还会给出内核版本要求、典型的kprobe和tracepoint代码示例、映射表设计思路,以及使用过程中常见的编译报错和内核不支持等问题的排查办法,帮助你在生产环境中稳定落地基于命名空间维度的可观测性方案。

在传统的物理机或虚拟机环境里,用PID标识进程几乎不会产生歧义。但容器普及之后,情况变了:每个容器都有自己的PID命名空间,容器内部看到的PID 1可能是宿主机上的第3827号进程,而两个不同容器里也可能同时存在PID为42的进程。如果你正在用eBPF编写安全审计、系统监控或者网络排查工具,仅靠bpf_get_current_pid_tgid拿到的全局PID去关联容器内进程,日志对不上号是常态。这时就需要bpf_get_current_pidns_info登场了。

如何使用bpf_get_current_pidns_info获取eBPF程序中的PID命名空间信息?

bpf_get_current_pidns_info是什么,为什么需要它

先看它的函数原型。这个辅助函数的作用是把当前进程所在的PID命名空间的关键信息,连同进程在该命名空间内的PID一起填充到一个结构体中,供eBPF程序读取:

struct bpf_pidns_info {
    __u32 pid;
    __u32 tgid;
};

long bpf_get_current_pidns_info(struct bpf_pidns_info *pidns_info, u32 size);

结构体里的pid是当前任务在其所属PID命名空间内的进程ID,tgid则是线程组ID,也就是用户态常见的 getpid 眼中的那个值。对比之下,bpf_get_current_pid_tgid返回的是全局初始命名空间下的ID。两者结合使用,你就能同时拿到“容器内视角”和“宿主机视角”两套ID,监控数据可以准确落到具体容器上。

这个辅助函数并不是内核一开始就提供的,它由Cilium团队提交,进入主线内核的版本大约在4.18之后才逐步可用。如果你的目标机器内核较老,加载器会直接报 invalid instruction 或 unknown func 字样的错误。所以在动手写代码之前,务必确认/proc/kallsyms里是否能找到对应的符号,或者在BCC环境里查看/sys/kernel/debug/tracing/available_filter_functions。生产环境建议用libbpf探测辅助函数ID是否存在,做好降级方案。

在kprobe和tracepoint中的实战用法

最常见的场景是在进程创建、执行命令的挂钩点抓取命名空间信息。下面用一个BCC风格的例子演示,挂钩security_bprm_check,记录每个新程序启动时的容器内PID:

from bcc import BPF

bpf_src = r'''
#include <linux/sched.h>
#include <uapi/linux/bpf.h>

struct data_t {
    u32 ns_pid;
    u32 ns_tgid;
    u32 global_pid;
    char comm[16];
};

BPF_PERF_OUTPUT(events);

int trace_exec(struct pt_regs *ctx, struct linux_binprm *bprm)
{
    struct data_t data = {};
    struct bpf_pidns_info ns = {};

    // 获取当前进程所属PID命名空间内的pid/tgid
    if (bpf_get_current_pidns_info(&ns, sizeof(ns)) != 0)
        return 0;

    // 同时取全局视角的pid,方便关联宿主机侧数据
    u64 pid_tgid = bpf_get_current_pid_tgid();
    data.global_pid = pid_tgid >> 32;

    data.ns_pid = ns.pid;
    data.ns_tgid = ns.tgid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
'''

bpf = BPF(text=bpf_src)
bpf.attach_kprobe(event="security_bprm_check", fn_name="trace_exec")
print("tracing exec events, ctrl+c to exit")

这个程序的关键点在于错误处理:bpf_get_current_pidns_info在当前任务不处于任何PID命名空间(例如中断上下文里没有明确关联任务)时会返回失败,直接返回0跳过是安全的做法。另外要注意参数必须传栈上的结构体指针,并且size参数必须与结构体大小一致,否则验证器会拒绝。

如果你偏好原始的libbpf加C的路线,写法类似,只是需要自己在内核态把数据写进BPF映射表,再由用户态程序读取。无论哪种方式,都建议把ns_pid和global_pid一起上报。只上报容器内PID,运维同学在宿主机上排查时找不到进程;只上报全局PID,容器平台的同学又对不上容器里的ps输出。两套ID配合Cgroup ID或者挂载信息,才能构建完整的容器画像。

进阶:结合命名空间ID构建容器监控方案

单纯拿到ns_pid还不够,实际系统中往往需要回答“这条事件来自哪个容器”。PID命名空间在内核中对应一个struct pid_namespace实例,虽然没有专门的辅助函数直接返回其编号,但可以结合进程的/proc/self/ns/pid链接,或者从task_struct里的nsproxy指针出发,在kprobe中读取结构体成员获取命名空间标识。把这些信息和ns_pid组合起来写入哈希映射表,就形成了“命名空间ID + 容器内PID”的唯一键。

struct container_key {
    u32 ns_id;
    u32 ns_pid;
};

struct container_value {
    u64 exec_count;
    u64 last_seen_ns;
};

BPF_HASH(container_stats, struct container_key, struct container_value);

int trace_exec_ns(struct pt_regs *ctx)
{
    struct bpf_pidns_info ns = {};
    if (bpf_get_current_pidns_info(&ns, sizeof(ns)) != 0)
        return 0;

    struct container_key key = {};
    key.ns_pid = ns.pid;
    // ns_id 可从task_struct->nsproxy->pid_ns_for_children派生
    // 此处省略具体读取逻辑,避免依赖不同内核版本的成员偏移

    struct container_value *val;
    struct container_value zero = {};
    val = container_stats.lookup_or_try_init(&key, &zero);
    if (val) {
        __sync_fetch_and_add(&val->exec_count, 1);
        val->last_seen_ns = bpf_ktime_get_ns();
    }
    return 0;
}

使用这种聚合结构时,验证器对结构体指针的访问检查比较严格。不同内核版本中nsproxy的成员布局存在差异,直接硬编码偏移很容易在新内核上崩溃。更稳妥的方案是用BTF和CO-RE机制,声明struct pid_namespace的重定位信息,让libbpf在加载时根据目标内核自动修正偏移。这也是Falco、Tetragon等安全工具普遍采用的做法。

还有几个容易踩的坑值得提醒。第一,该辅助函数只能获取“当前任务”的命名空间信息,不能在perf事件或socket过滤器的软中断上下文中对任意进程调用,如果你需要观测其他进程,只能通过映射表把上下文传进去。第二,PID命名空间是分层的,容器里再跑一层嵌套容器时,返回的是程序执行时所在的那一层命名空间的PID,而不是最外层的,做多层容器编排监控时要考虑这一点。第三,配合PID回收问题:容器退出后PID可能被复用,长期统计时最好把启动时间戳一起作为键的一部分,避免数据串扰。掌握这些细节之后,基于eBPF的容器级监控才能真正做到既精确又稳定。

eBPFPID命名空间bpf_get_current_pidns_info修改时间:2026-09-05 08:06:34

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260905/50778.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。