导读:本期聚焦于苏锦程创作的《如何使用 bpf_get_current_comm 获取进程名并关联网络活动?》,敬请观看详情。服务器上出现异常外联,却不知道是哪个进程发起的连接;或者想统计每个应用占用了多少网络流量,但又不愿意修改业务代码。eBPF 的 bpf_get_current_comm 指令可以提供低开销的观测能力。它能在内核网络路径的关键点上读取当前任务的进程名,从而把连接、报文与可执行程序对应起来。本文围绕 bpf_get_current_comm 的用法展开,介绍它的底层实现与使用限制,说明在 tcp_v4_connect、tcp_sendmsg 等挂载点收集进程名的方法,并给出一个完整的 kprobe 示例。你会看到如何定义 eBPF 映射、如何在挂载函数中提取四元组与进程名,以及验证输出时需要注意的 comm 截断、内核版本差异等实际问题。

在网络可观测性场景中,经常需要回答一个基础问题:当前这个 TCP 连接或数据包是哪个进程发起的?传统方式可能依赖 netstat、ss 等工具,但这些工具只能在用户态查询某一时刻的状态,无法在高频事件中做持续关联。eBPF 提供了一种更灵活的方式:在内核网络函数入口处通过 helper 直接读取当前任务的进程名,其中 bpf_get_current_comm 就是最常用的辅助函数之一。该函数在 eBPF 程序运行时返回调用进程的 comm 字段,也就是通常所说的程序名。

如何使用 bpf_get_current_comm 获取进程名并关联网络活动?

理解 bpf_get_current_comm 的工作时机很重要。eBPF 程序附着在 kprobe、tracepoint 或 socket filter 等不同挂载点时,其上下文中的当前进程含义可能不同。对于 kprobe/tcp_v4_connect 这类内核函数,运行上下文仍然属于发起连接的用户进程,因此能直接拿到准确的进程名。但如果换到软中断或 workqueue 上下文中,current 可能指向内核线程,此时进程名就不再代表原始用户进程了。因此,要用好这个 helper,需要选择合适的挂载点。

一、bpf_get_current_comm 的工作原理与限制

这个 helper 在内核中的实现并不复杂,它做的事情是从当前进程的 task_struct 中读取 comm 字段,复制到用户提供的缓冲区,并确保以空字符结尾。comm 字段由 TASK_COMM_LEN 宏定义,通常为 16 字节,所以最多容纳 15 个可见字符。这意味着像 python3.11 这样的进程名会被截断为 python3.1,在分析数据时需要注意这一点。截断后的名字仍然有一定参考价值,但不要把它当作完整可执行文件名。

进程名并不总等于可执行文件路径,也不具备唯一性。多个不同的进程可以共享同一个 comm,例如多个 Java 服务都显示为 java。如果只记录 comm,就无法区分具体是哪一个 Java 实例。更精细的方案需要结合 PID、命名空间、命令行参数等信息。但 bpf_get_current_comm 的优势在于速度极快,可以在性能敏感的网络收发路径中使用,适合先做粗粒度的进程归类。需要更精确信息时,再结合其他数据源做二次关联。

另一个容易忽略的点是上下文。eBPF 程序绑定到 kprobe/tcp_v4_connect 时,系统调用或网络栈的调用线程正是发起连接的进程,current 指向该用户进程,因此放在这里的 bpf_get_current_comm 结果准确。但如果把程序挂到 netif_receive_skb 等收包路径,数据包处理可能发生在软中断,此时 current 是 ksoftirqd 或其他内核线程,进程名就会变成内核线程名,不适合用于关联用户进程的网络活动。因此,选择挂载点必须优先考虑上下文是否仍属于目标用户进程。

二、在网络内核路径中关联进程名

对于 TCP 连接跟踪,最常见的挂载点是 tcp_v4_connect。这个函数在客户端发起 IPv4 连接时被调用,从参数中能拿到 struct sock *sk。通过读取 sk 的 __sk_common 结构中的地址和端口字段,可以获得源地址和目的地址以及端口。在同一个 kprobe 程序中先调用 bpf_get_current_comm,再读取这些字段写入 eBPF map,就实现了进程名到网络四元组的关联。服务端的被动连接可以在 tcp_v4_syn_recv_sock 或 inet_csk_accept 等函数处观测,但要注意此时当前进程可能是服务端监听线程。

除了连接建立,很多场景还关心流量大小。这时可以挂载 tcp_sendmsg 和 tcp_cleanup_rbuf 之类的函数,它们分别在发送和接收数据时触发。在这些函数中,current 仍然可能是用户进程,但对异步写入或零拷贝路径要小心,部分操作可能被延后到内核工作队列。更稳妥的方式是使用 tracepoint,例如 sock:inet_sock_set_state,该 tracepoint 在 TCP 状态变化时触发,上下文通常比较稳定,并且参数中直接包含 protocol、saddr、daddr、sport、dport 等信息,省去手动解析 sock 结构的麻烦。

如果目标只是回答哪个进程建立了哪些连接,kprobe/tcp_v4_connect 足够简单高效。若还要关联每个连接的关闭时间或状态变化,可以在 tcp_set_state 或 tcp_close 再挂一个程序,通过 map 共享连接信息。需要注意的是,map 的 key 设计很关键。常见做法是用源 IP、目标 IP、源端口、目标端口四元组拼接成哈希键,但端口可能复用,短时间内可能出现碰撞。更严谨的做法是加入进程 PID 或启动时间,或者使用内核分配的 socket cookie 作为 key。

三、完整 eBPF 程序实现与验证

下面这段 eBPF C 代码演示了在 tcp_v4_connect 挂载点获取进程名和目标地址并写入哈希 map。代码使用了 BTF 风格的 BPF_CORE_READ 来读取 sock 结构字段,避免了依赖特定内核头文件的完整结构定义。实际运行时需要内核开启 CONFIG_DEBUG_INFO_BTF。

#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_endian.h>

struct conn_key {
    __u32 pid;
    __u32 saddr;
    __u32 daddr;
    __u16 sport;
    __u16 dport;
};

struct conn_value {
    char comm[16];
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, struct conn_key);
    __type(value, struct conn_value);
} conn_map SEC(".maps");

SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
{
    struct conn_key key = {};
    struct conn_value val = {};

    bpf_get_current_comm(&val.comm, sizeof(val.comm));
    key.pid = bpf_get_current_pid_tgid() >> 32;

    // 从 sock 结构中读取 IPv4 地址和端口
    key.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    key.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    key.sport = __bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_num));
    key.dport = __bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));

    bpf_map_update_elem(&conn_map, &key, &val, BPF_ANY);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

这段代码首先定义了一个哈希 map,key 由 PID 和四元组组成,value 保存进程名。kprobe 程序在 tcp_v4_connect 入口执行时,先调用 bpf_get_current_comm 拿到进程名,再通过 bpf_get_current_pid_tgid 获取 PID,然后利用 BPF_CORE_READ 从 sock 结构中提取源地址、目的地址和端口。使用 CO-RE 方式读取字段的好处是,当内核结构布局发生细微变化时,eBPF 程序经过 BTF 重定位后仍然可以正确运行,不需要针对每个内核版本重新编译。

编译时可以使用 clang 生成 BPF 目标文件,命令为 clang -O2 -target bpf -g -c conn_track.bpf.c -o conn_track.bpf.o。如果内核支持 BTF,CO-RE 重定位会自动完成。加载时可以使用 bpftool prog load 或者通过 libbpf 编写用户态加载器。验证数据时用 bpftool map dump 即可查看类似 pid=1234 comm=curl dst=93.184.216.34 port=80 的记录。需要注意的是,comm 可能被截断,因此输出时最好保留 16 字节缓冲区并做右对齐处理。

实际使用中还会遇到一些常见问题。比如某些发行版默认限制非特权 eBPF,需要 root 权限或 CAP_BPF 能力;map 的 max_entries 设置过小会导致新连接无法记录;如果同一个进程短时间内建立大量连接,哈希表可能出现较多冲突,此时可以增加 map 大小或使用 LRU 类型。此外,对于 IPv6 连接,需要另写 tcp_v6_connect 挂载点,并把 key 中的地址字段扩展为 16 字节。总之,bpf_get_current_comm 为进程级网络观测提供了一个轻量入口,配合合适的挂载点和数据结构,可以稳定地完成网络活动关联任务。

bpf_get_current_commeBPF网络活动关联修改时间:2026-10-02 02:38:18

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