在Linux内核观测领域,eBPF已经成为系统调用级追踪的首选手段。当我们需要分析系统中所有mkdir行为背后的目录创建细节时,传统的审计子系统(auditd)虽然能记录路径,但无法便捷地关联调用进程的内核任务状态,也难以在低开销下做实时过滤。bpf_get_current_task_mkdir作为eBPF辅助函数,允许我们在挂载到mkdir相关跟踪点时,直接从当前任务的进程描述符中拿到即将创建的目录名,而不必依赖寄存器或栈上参数的不稳定抓取。

bpf_get_current_task_mkdir的函数原理与数据结构
bpf_get_current_task_mkdir并不是标准上游内核长期稳定提供的辅助函数名,在多数发行版中它体现为通过bpf_get_current_task拿到task_struct指针后,由eBPF程序自行顺着fs指针读取fs_struct中的root与pwd,再结合系统调用参数中的路径名计算出绝对目录。理解这一点很关键:所谓“获取mkdir分析目录创建”,本质是借助当前任务上下文补全路径语义。task_struct中的fs字段指向fs_struct,其中保存了进程的根目录和当前工作目录的vfsmount与dentry,eBPF程序可以利用内核头文件生成的BTF信息直接偏移访问。
在mkdir系统调用入口处,用户传入的是相对或绝对路径字符串。如果直接读取该字符串,只能得到调用者视角下的路径片段,无法知道它映射到的真实目录树位置。通过当前任务的fs_struct,程序能把相对路径解析为挂载命名空间内的目标父目录。例如容器内的进程在/ tmp下创建目录,从宿主机命名空间看可能对应/var/lib/docker/overlay2/xxx/tmp,这种映射只有结合task的fs信息才能还原。
从性能角度看,在eBPF中直接读取task上下文比在用户态轮询/proc/PID/cwd要轻量得多。后者每次都要打开文件、读取链接、解析字符串,且容易遇到进程退出导致的竞态。而eBPF辅助调用在内核态同步完成,数据一致性由调度器保证。下面是一个简化版的BTF偏移读取示例,展示如何从当前任务提取父目录dentry名。
#include <linux/sched.h>
#include <linux/fs_struct.h>
#include <bpf/bpf_helpers.h>
SEC("tracepoint/syscalls/sys_enter_mkdir")
int trace_mkdir(struct trace_event_raw_sys_enter *ctx) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// 通过BTF生成的偏移访问task->fs->pwd.dentry
struct fs_struct *fs = task->fs;
struct dentry *pwd = fs->pwd.dentry;
char buf[64];
// 简化:读取dentry->d_name.name,实际需用bpf_probe_read_kernel
bpf_probe_read_kernel(buf, sizeof(buf), pwd->d_name.name);
bpf_printk("mkdir under cwd: %sn", buf);
return 0;
}
在mkdir跟踪点中落地目录创建分析
将上文原理应用到mkdir分析时,我们通常选择sys_enter_mkdir或sys_enter_mkdirat跟踪点。这两个点分别对应mkdir和mkdirat系统调用,后者多一个目录文件描述符参数。在eBPF程序中,除了利用当前任务上下文拿到cwd对应的目录项,还要结合第一个参数(路径指针)读取用户传入的目录名。对于mkdirat,若dirfd为AT_FDCWD,则基于cwd解析;否则需从dirfd对应的file结构取目录,这在eBPF里可通过bpf_task_get_pid等辅助结合fdtable查找,但复杂度较高,一般建议只关注AT_FDCWD场景。
为了输出结构化的目录创建事件,我们可以在eBPF中定义结构体,包含pid、cwd字符串、目标路径字符串、时间戳,然后通过perf array或ring buffer发送到用户态。用户态程序负责聚合,比如统计每秒哪个容器创建目录最多。这种方案相比纯审计日志,延迟更低,且能利用eBPF地图做去重。下面的代码展示如何把路径与cwd拼接后提交事件。
struct mkdir_event {
u32 pid;
char cwd[128];
char path[128];
};
SEC("tracepoint/syscalls/sys_enter_mkdir")
int trace_mkdir_enter(struct trace_event_raw_sys_enter *ctx) {
struct mkdir_event e = {};
e.pid = bpf_get_current_pid_tgid() >> 32;
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
struct fs_struct *fs = task->fs;
bpf_probe_read_kernel(&e.cwd, sizeof(e.cwd), fs->pwd.dentry->d_name.name);
// 参数1为路径名指针
bpf_probe_read_user(&e.path, sizeof(e.path), (void *)ctx->args[0]);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
return 0;
}
需要注意,读取用户态路径时必须使用bpf_probe_read_user系列函数,因为路径字符串位于进程地址空间。若进程在读取瞬间消亡,这类调用会安全返回错误而非崩溃内核。另外,dentry的d_name.name长度不固定,真实代码应循环读取或限制长度,避免缓冲区溢出。通过这些细节处理,目录创建分析才能稳定用于生产环境。
对比其他方案与常见误区
有些工程师习惯用kprobe挂载do_mkdirat函数,然后从寄存器提取struct path指针。这种方式在x86和arm架构下寄存器编号不同,且编译器优化可能把参数放进栈,导致eBPF读取失败。而基于tracepoint加bpf_get_current_task_mkdir式上下文读取,不依赖特定寄存器约定,可移植性更好。另外,auditd规则可能产生大量日志,磁盘IO反而成为瓶颈,eBPF在内核态过滤明显更优。
一个常见误区是认为“获取mkdir分析目录创建”只需要路径参数就够了。实际上,如果没有当前任务的fs上下文,短路径如“subdir”无法判断真实位置,尤其在chroot或容器环境下。另一个误区是直接在eBPF里做字符串拼接并printk,这会大量占用trace_pipe,正确做法是用地图批量传用户态处理。下面表格对比三种方案特点。
| 方案 | 上下文获取 | 开销 | 稳定性 |
|---|---|---|---|
| auditd | 用户态日志含cwd | 高(磁盘写) | 中(规则复杂漏报) |
| kprobe do_mkdirat | 需解析寄存器 | 低 | 低(架构相关) |
| tracepoint加task读取 | 直接task->fs | 极低 | 高(不依赖寄存器) |
综合来看,使用bpf_get_current_task_mkdir思路来追踪mkdir目录创建,既贴合内核数据结构真实布局,又能规避用户态轮询和寄存器解析的双重脆弱性。在编写这类程序时,应当充分借助libbpf与BTF,减少硬编码偏移,使观测工具能跨内核版本运行。
bpf_get_current_task_mkdireBPFmkdir修改时间:2026-08-18 00:22:33