利用Linux NS和cGroups机制简单实现容器进程划分

2025-10-12 发布 0条评论

利用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: 用于编译和构建项目的脚本。
  1. 内核态 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";
  1. 用户态加载器 (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;
}
  1. 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

发表评论