利用Linux NS和cGroups机制简单实现容器进程划分
利用NS和cGroup区分容器进程和非容器进程
核心思路
在 Linux 系统中,容器技术的核心是 命名空间 (Namespace) 和 控制组 (Cgroups)。我们可以利用这些内核特性来识别一个进程是否运行在容器内。
PID 命名空间 (PID Namespace): 每个容器都有自己独立的进程ID空间。容器内的1号进程对应于宿主机上的一个普通进程。因此,一个进程的 PID 命名空间如果与宿主机 init 进程(PID为1)的 PID 命名空间不同,那么它极有可能是一个容器进程。
Cgroups: 容器运行时(如 Docker、containerd)会为每个容器创建特定的 Cgroup 路径,用于资源限制。通过检查进程的 /proc/[pid]/cgroup 文件,我们可以看到它所属的 Cgroup 路径。容器进程的 Cgroup 路径通常包含 docker、kubepods 或 container 等特征字符串。本项目将主要采用第一种方法(PID 命名空间),因为它是一种更通用和根本性的区分方式。
我们将通过 eBPF 实现以下逻辑:
内核态 (eBPF Program): 附加一个 eBPF 程序到内核的进程创建相关的跟踪点(例如 sched_process_exec)。当系统中有新进程执行时,该 eBPF 程序会被触发。程序会获取新进程的 task_struct 结构体,并从中读取其 PID 命名空间的 inode 号。
用户态 (User-space Application):
启动时,首先获取宿主机 init 进程(PID 1)的 PID 命名空间 inode 号,并将其作为“基准”写入一个 eBPF Map 中,供内核态的 eBPF 程序查询。
加载 eBPF 程序并将其附加到指定的内核跟踪点。
通过 eBPF 的 Perf Event Array 机制,接收内核态 eBPF 程序发送的事件。
内核态程序会将每个新进程的 PID、COMM(进程名)及其 PID 命名空间 inode 号与宿主机基准 inode 号的对比结果发送给用户态。
用户态程序接收到数据后,进行格式化输出,明确标识出哪些是容器进程,哪些是宿主机进程。系统架构
+————————————————-+
| User Space (C/Go/Rust) |
| +——————————————-+ |
| | Loader Application | |
| |——————————————-| |
| | 1. Get host init ns inode | |
| | 2. Load eBPF object file | |
| | 3. Write host ns inode to BPF_MAP <—-
| | 4. Attach eBPF program to tracepoint | | | BPF_MAP_TYPE_HASH
| | 5. Read events from Perf Buffer <—- (for host ns)
| +——————————————-+ | |
+———————-^————————–+
| Perf Buffer
| (BPF_MAP_TYPE_PERF_EVENT_ARRAY)
+———————-|————————–+
| v |
| Kernel Space |
| +——————————————-+ |
| | eBPF Program | |
| |——————————————-| |
| | TRACEPOINT(sched_process_exec) | |
| | { | |
| | task = bpf_get_current_task(); | |
| | process_ns_inode = task->nsproxy->…; | |
| | host_ns_inode = bpf_map_lookup(…); | | —-> Read host_ns_inode
| | | |
| | is_container = (process_ns_inode != | |
| | host_ns_inode); | |
| | | |
| | bpf_perf_event_output(…); | | —-> Send event data
| | } | |
| +——————————————-+ |
+————————————————-+
完整源码
本项目包含三个文件:
process_classifier.bpf.c: 内核态的 eBPF C 程序。
process_classifier.c: 用户态的加载器和事件处理 C 程序。
Makefile: 用于编译和构建项目的脚本。- 内核态 eBPF 程序 (process_classifier.bpf.c)
此文件定义了 eBPF 程序的核心逻辑。
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include "vmlinux.h"
// 定义发送给用户态的事件结构体
struct event {
u32 pid;
u32 ppid;
char comm[TASK_COMM_LEN];
bool is_container;
};
// Perf event map to send data to user space
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
// Map to store the host's init PID namespace inode number
// The user-space program will write to this map.
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} host_pid_ns_map SEC(".maps");
// Attach to the sched_process_exec tracepoint.
// This is not a raw tracepoint, so the context is task_struct.
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct task_struct *task;
struct event event = {};
u64 pid_tgid;
u32 key = 0;
u64 *host_pid_ns_inode;
u64 proc_pid_ns_inode;
// Get PID and TGID
pid_tgid = bpf_get_current_pid_tgid();
event.pid = pid_tgid >> 32;
// Get current task_struct
task = (struct task_struct *)bpf_get_current_task();
// Get parent PID
event.ppid = BPF_CORE_READ(task, real_parent, tgid);
// Get command name
bpf_get_current_comm(&event.comm, sizeof(event.comm));
// Get the PID namespace inode number for the current process
// task->nsproxy->pid_ns_for_children->ns.inum
proc_pid_ns_inode = BPF_CORE_READ(task, nsproxy, pid_ns_for_children, ns.inum);
// Look up the host PID namespace inode from the map
host_pid_ns_inode = bpf_map_lookup_elem(&host_pid_ns_map, &key);
if (host_pid_ns_inode) {
// If the process's PID namespace inode is different from the host's,
// it's a container process.
if (proc_pid_ns_inode != *host_pid_ns_inode) {
event.is_container = true;
} else {
event.is_container = false;
}
} else {
// If map lookup fails, we can't determine, assume not a container.
event.is_container = false;
}
// Send the event to user space via the perf buffer.
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
- 用户态加载器 (process_classifier.c)
此文件负责加载 eBPF 程序、与内核交互并显示结果。
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include "process_classifier.skel.h"
// 定义与内核态一致的事件结构体
struct event {
__u32 pid;
__u32 ppid;
char comm[16];
bool is_container;
};
// 获取指定PID的PID命名空间inode号
static unsigned long long get_pid_ns_inode(pid_t pid) {
char path[128];
struct stat s;
snprintf(path, sizeof(path), "/proc/%d/ns/pid", pid);
if (stat(path, &s) != 0) {
perror("Failed to stat pid namespace");
return 0;
}
return s.st_ino;
}
// Perf buffer 事件处理回调函数
static int handle_event(void *ctx, void *data, size_t data_sz) {
const struct event *e = data;
time_t t;
struct tm *tm;
char ts[32];
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
printf("%-8s %-7d %-7d %-16s %-10s\n",
ts, e->pid, e->ppid, e->comm,
e->is_container ? "Container" : "Host");
return 0;
}
int main(int argc, char **argv) {
struct process_classifier_bpf *skel;
struct perf_buffer *pb = NULL;
int err;
unsigned long long host_init_pid_ns;
int map_fd;
int key = 0;
// 提升资源限制
struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
if (setrlimit(RLIMIT_MEMLOCK, &r)) {
perror("setrlimit(RLIMIT_MEMLOCK)");
return 1;
}
// 1. 打开、加载并验证 eBPF 程序
skel = process_classifier_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// 2. 获取宿主机 init 进程 (PID=1) 的 PID Namespace Inode
host_init_pid_ns = get_pid_ns_inode(1);
if (host_init_pid_ns == 0) {
fprintf(stderr, "Failed to get host init pid namespace inode\n");
goto cleanup;
}
printf("Host init PID namespace inode: %llu\n", host_init_pid_ns);
// 3. 将宿主机 PID Namespace Inode 写入 eBPF map
map_fd = bpf_map__fd(skel->maps.host_pid_ns_map);
err = bpf_map_update_elem(map_fd, &key, &host_init_pid_ns, BPF_ANY);
if (err) {
fprintf(stderr, "Failed to update host_pid_ns_map: %s\n", strerror(errno));
goto cleanup;
}
// 4. 附加 eBPF 程序到跟踪点
err = process_classifier_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
printf("Successfully started! Please run processes on host or in containers to see events.\n");
printf("%-8s %-7s %-7s %-16s %-10s\n", "TIME", "PID", "PPID", "COMM", "TYPE");
// 5. 设置 Perf Buffer 来接收事件
pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 8, handle_event, NULL, NULL, NULL);
if (!pb) {
err = -errno;
fprintf(stderr, "Failed to create perf buffer: %d\n", err);
goto cleanup;
}
// 6. 轮询 Perf Buffer
while (true) {
err = perf_buffer__poll(pb, 100 /* timeout, ms */);
if (err < 0 && err != -EINTR) {
fprintf(stderr, "Error polling perf buffer: %s\n", strerror(-err));
break;
}
}
cleanup:
perf_buffer__free(pb);
process_classifier_bpf__destroy(skel);
return -err;
}
- Makefile
这个 Makefile 会处理所有编译步骤,包括使用 bpftool 生成 skeleton 文件。
# Dirs
SRC_DIR = .
OUT_DIR = .
# Files
BPF_SRC = $(SRC_DIR)/process_classifier.bpf.c
BPF_OBJ = $(OUT_DIR)/process_classifier.bpf.o
BPF_SKEL = $(OUT_DIR)/process_classifier.skel.h
USER_SRC = $(SRC_DIR)/process_classifier.c
USER_BIN = $(OUT_DIR)/process_classifier
# Tools
CC = gcc
CLANG = clang
BPFTOOL = bpftool
# Flags
CFLAGS = -g -Wall
LDFLAGS = -lbpf
.PHONY: all clean
all: $(USER_BIN)
# Generate BPF skeleton header
$(BPF_SKEL): $(BPF_OBJ)
$(BPFTOOL) gen skeleton $< > $@
# Compile BPF C code to object file
$(BPF_OBJ): $(BPF_SRC)
$(CLANG) -g -O2 -target bpf -c $< -o $@ -I/usr/include/bpf -I.
# Compile user-space C code
$(USER_BIN): $(USER_SRC) $(BPF_SKEL)
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
clean:
rm -f $(BPF_OBJ) $(BPF_SKEL) $(USER_BIN)
准备和编译环境
安装依赖:
你需要一个较新的 Linux 内核(推荐 5.8+),以及 clang, llvm, libbpf-dev, 和 bpftool。
在 Ubuntu/Debian 上:
sudo apt-get update
sudo apt-get install -y clang llvm libelf-dev libbpf-dev bpftool build-essential
获取 vmlinux.h:
eBPF 程序需要内核类型定义。bpftool 可以从你正在运行的内核生成此文件。
Bash
# 确保你的系统支持 BTF (BPF Type Format)
ls /sys/kernel/btf/vmlinux
# 如果上述文件存在,则可以生成 vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
如果 /sys/kernel/btf/vmlinux 不存在,你需要确保你的内核编译时开启了 CONFIG_DEBUG_INFO_BTF=y 选项。
编译项目:
将以上三个文件 (process_classifier.bpf.c, process_classifier.c, Makefile) 放在同一个目录下,然后运行 make。
make
如果一切顺利,你会在当前目录下看到一个名为 process_classifier 的可执行文件。运行和验证
运行程序:
因为 eBPF 程序需要加载到内核,所以需要 root 权限。
sudo ./process_classifier
程序启动后,会打印出宿主机的 init 进程 PID 命名空间 inode,然后开始监听新的进程执行事件。
验证:
打开第一个终端,运行程序:
sudo ./process_classifier
Host init PID namespace inode: 4026531836
Successfully started! Please run processes on host or in containers to see events.
TIME PID PPID COMM TYPE
打开第二个终端 (宿主机终端),执行一些命令:
ls -l
sleep 5
在第一个终端,你会看到类似这样的输出,TYPE 都是 Host:
20:42:10 12345 12300 ls Host
20:42:15 12346 12300 sleep Host打开第三个终端,启动一个 Docker 容器并执行命令:
# 启动一个交互式 busybox 容器
docker run --rm -it busybox
# 在容器内执行命令
/ # ls
/ # ps aux
/ # sleep 10
现在回到第一个终端,你会看到来自容器的进程事件,TYPE 被正确地标识为 Container:
20:45:30 13010 12990 ls Container
20:45:35 13011 12990 ps Container
20:45:40 13012 12990 sleep Container
发表评论