eBPF追踪中如何在kprobe:tcp_v4_connect获取文件描述符
问题解答
在tcp_v4_connect上下文获取对应文件描述符的方法
tcp_v4_connect本身的入参不直接携带文件描述符(fd),可以通过两种稳定方式拿到:
- 结构体顺藤摸瓜法(最准确,无兼容问题)
- 用
PT_REGS_PARM1(ctx)从寄存器拿到tcp_v4_connect的第一个参数,即struct sock *sk指针 - 顺着内核结构体偏移读:
sk->sk_socket拿到对应struct socket *sock,再读sock->file拿到struct file *file指针 - 通过
bpf_get_current_task_btf()拿到当前进程的task_struct,遍历进程files->fdt->fd数组,找到值等于上述file指针的数组下标,就是当前连接对应的fd。
遍历的时候可以加最大遍历长度限制(比如1024),客户端场景下连接用的fd普遍很小,遍历开销可以忽略。
- 用
- 双挂载点映射法(性能更高)
同时挂载tracepoint:syscalls:sys_enter_connect,这个tracepoint的参数直接携带fd、目的地址/端口信息。你可以创建一个以<tgid, pid>为key的临时map存当前线程正在发起connect的fd和五元组信息——线程执行connect的路径很短,从syscall入口到tcp_v4_connect的流程中不会出现跨线程的信息干扰,等执行到tcp_v4_connect时,直接用当前线程的tgid+pid查这个临时map,和当前sk读出的源/目的IP、端口做简单校验就能拿到对应fd,读取完成后删掉临时map的对应条目即可,省去遍历fd数组的开销。
客户端请求全链路追踪的更优实现方案
你当前用tracepoint:syscalls:write挂载的方案有两个明显缺陷:一是只能抓到write类调用,会漏掉send、sendmsg、writev等其他发送数据的系统调用;二是靠fd做关联键容易因为fd复用、多线程共享fd出现串流问题。更稳定的实现可以参考以下思路:
- 用
struct sock *sk指针作为连接的唯一关联键
内核中每个TCP连接对应的sk指针在连接生命周期内是唯一的,连接释放前不会被复用,比fd作为关联键可靠得多,完全不需要在各个追踪点之间传递、匹配fd。 - 选择覆盖全、兼容性好的挂载点
- 连接阶段:优先用
tracepoint:sock:inet_sock_set_state替代kprobetcp_v4_connect。这个tracepoint是TCP状态变更的统一钩子,同时覆盖IPv4/IPv6的主动连接、被动连接场景,参数直接携带sk指针、新旧TCP状态、源/目的IP和端口,不用分别适配不同版本的v4/v6 connect函数,跨内核版本兼容性更好。当连接状态转为TCP_SYN_SENT时记录连接发起事件,转为TCP_ESTABLISHED时记录连接建立成功事件。 - 数据收发阶段:直接挂kprobe
tcp_sendmsg和tcp_recvmsg,不要挂系统调用层的write/read。这两个函数是TCP协议栈收发数据的统一入口,上层不管用write、send、sendmsg还是writev,最终都会走到这两个函数,不会漏抓流量。而且这两个函数的第一个参数就是struct sock *sk,直接拿sk当key查连接信息,完全绕开fd关联的问题。 - 连接关闭阶段:还是复用
inet_sock_set_statetracepoint,当状态转为TCP_CLOSE时删除map中对应sk的条目,避免map无限膨胀、出现脏数据。
- 连接阶段:优先用
- 可选的用户态上下文关联
如果需要把内核侧的网络事件和用户态的全链路追踪ID(比如OpenTelemetry的TraceID)关联,可以配合uprobe挂载到用户态使用的网络库入口(比如libc的connect、send函数,或者语言自带的网络库实现),拿到用户态的trace上下文后,以<进程tgid, sk指针>为key存在map中,内核侧的各个追踪点可以直接读取对应上下文,实现端到端的链路串联。 - 避坑提示
- 不要用线程ID(pid)作为map key的组成部分,多线程进程内的线程共享文件描述符和sock结构,用进程tgid+sk指针作为key即可,避免多线程场景下匹配不到连接。
- 抓应用层载荷时,直接在
tcp_sendmsg/tcp_recvmsg中读取iov缓冲区的内容即可,比在系统调用层读取适配成本更低,覆盖更全。
内容的提问来源于stack exchange,提问作者dalek
相关产品推荐
相关产品推荐

