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

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