Published at 2021-07-04 | Last Update 2021-07-04
内核目前支持 30 来种 BPF 程序类型。对于主要的程序类型,本文将介绍其:
本文主要参考:
4.14
平时学习使用 BPF 时所整理。由于是笔记而非教程,因此内容不会追求连贯,有基础的 同学可作查漏补缺之用。
文中涉及的代码,如无特殊说明,均基于内核 5.8/5.10 版本。
BPF_PROG_TYPE_SOCKET_FILTER
BPF_PROG_TYPE_SOCK_OPS
BPF_PROG_TYPE_SK_SKB
BPF_PROG_TYPE_SCHED_CLS
BPF_PROG_TYPE_SCHED_ACT
BPF_PROG_TYPE_XDP
BPF_PROG_TYPE_CGROUP_SKB
BPF_PROG_TYPE_CGROUP_SOCK
BPF_PROG_TYPE_KPROBE
BPF_PROG_TYPE_TRACEPOINT
BPF_PROG_TYPE_PERF_EVENT
BPF_PROG_TYPE_LWT_IN
BPF_PROG_TYPE_LWT_OUT
BPF_PROG_TYPE_LWT_XMIT
Kernel 5.8 支持的 BPF 程序类型:
// include/uapi/linux/bpf.h
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC,
BPF_PROG_TYPE_SOCKET_FILTER,
BPF_PROG_TYPE_KPROBE,
BPF_PROG_TYPE_SCHED_CLS, // CLS: tc classifier,分类器
BPF_PROG_TYPE_SCHED_ACT, // ACT: tc action,动作
BPF_PROG_TYPE_TRACEPOINT,
BPF_PROG_TYPE_XDP,
BPF_PROG_TYPE_PERF_EVENT,
BPF_PROG_TYPE_CGROUP_SKB,
BPF_PROG_TYPE_CGROUP_SOCK,
BPF_PROG_TYPE_LWT_IN,
BPF_PROG_TYPE_LWT_OUT,
BPF_PROG_TYPE_LWT_XMIT,
BPF_PROG_TYPE_SOCK_OPS,
BPF_PROG_TYPE_SK_SKB,
BPF_PROG_TYPE_CGROUP_DEVICE,
BPF_PROG_TYPE_SK_MSG,
BPF_PROG_TYPE_RAW_TRACEPOINT,
BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
BPF_PROG_TYPE_LWT_SEG6LOCAL,
BPF_PROG_TYPE_LIRC_MODE2,
BPF_PROG_TYPE_SK_REUSEPORT,
BPF_PROG_TYPE_FLOW_DISSECTOR,
BPF_PROG_TYPE_CGROUP_SYSCTL,
BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE,
BPF_PROG_TYPE_CGROUP_SOCKOPT,
BPF_PROG_TYPE_TRACING,
BPF_PROG_TYPE_STRUCT_OPS,
BPF_PROG_TYPE_EXT,
BPF_PROG_TYPE_LSM,
};
通过 socket()
系统调用将 BPF 程序 attach 到 hook 点时用到,
定义:
// include/uapi/linux/bpf.h
enum bpf_attach_type {
BPF_CGROUP_INET_INGRESS,
BPF_CGROUP_INET_EGRESS,
BPF_CGROUP_INET_SOCK_CREATE,
BPF_CGROUP_SOCK_OPS,
BPF_SK_SKB_STREAM_PARSER,
BPF_SK_SKB_STREAM_VERDICT,
BPF_CGROUP_DEVICE,
BPF_SK_MSG_VERDICT,
BPF_CGROUP_INET4_BIND,
BPF_CGROUP_INET6_BIND,
BPF_CGROUP_INET4_CONNECT,
BPF_CGROUP_INET6_CONNECT,
BPF_CGROUP_INET4_POST_BIND,
BPF_CGROUP_INET6_POST_BIND,
BPF_CGROUP_UDP4_SENDMSG,
BPF_CGROUP_UDP6_SENDMSG,
BPF_LIRC_MODE2,
BPF_FLOW_DISSECTOR,
BPF_CGROUP_SYSCTL,
BPF_CGROUP_UDP4_RECVMSG,
BPF_CGROUP_UDP6_RECVMSG,
BPF_CGROUP_GETSOCKOPT,
BPF_CGROUP_SETSOCKOPT,
BPF_TRACE_RAW_TP,
BPF_TRACE_FENTRY,
BPF_TRACE_FEXIT,
BPF_MODIFY_RETURN,
BPF_LSM_MAC,
BPF_TRACE_ITER,
BPF_CGROUP_INET4_GETPEERNAME,
BPF_CGROUP_INET6_GETPEERNAME,
BPF_CGROUP_INET4_GETSOCKNAME,
BPF_CGROUP_INET6_GETSOCKNAME,
BPF_XDP_DEVMAP,
__MAX_BPF_ATTACH_TYPE
};
用于 过滤和重定向 socket 数据,或者监听 socket 事件。类型包括:
BPF_PROG_TYPE_SOCKET_FILTER
BPF_PROG_TYPE_SOCK_OPS
BPF_PROG_TYPE_SK_SKB
BPF_PROG_TYPE_SK_MSG
BPF_PROG_TYPE_SK_REUSEPORT
从名字 SOCKET_FILTER 可以看出,这种类型的 BPF 程序能对流量进行 过滤(filtering)。
仍然是对流量进行过滤,但只统计流量信息,不要包本身。
sock_queue_rcv_skb()
在 sock_queue_rcv_skb()
中触发执行:
// net/core/sock.c
// 处理 socket 入向流量,TCP/UDP/ICMP/raw-socket 等协议类型都会执行到这里
int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
err = sk_filter(sk, skb); // 执行 BPF 代码,这里返回的 err 表示对这个包保留前多少字节(trim)
if (err) // 如果字节数大于 0
return err; // 跳过接下来的处理逻辑,直接返回到更上层
// 如果字节数等于 0,继续执行内核正常的 socket receive 逻辑
return __sock_queue_rcv_skb(sk, skb);
}
struct __sk_buff *
上面可以看到,hook 入口 sk_filter(sk, skb)
传的是 struct sk_buff *skb
,
但经过层层传递,最终传递给 BPF 程序的其实是 struct __sk_buff *
。
这个结构体的定义见 include/uapi/linux/bpf.h,
// include/uapi/linux/bpf.h
// user accessible mirror of in-kernel sk_buff.
struct __sk_buff {
...
}
struct sk_buff
的用户可访问字段的镜像。struct __sk_buff
字段的访问,将会被 BPF 校验器转换成对相应的
struct sk_buff 字段的访问。再来看一下 hook 前后的逻辑:
// net/core/sock.c
// 处理 socket 入向流量,TCP/UDP/ICMP/raw-socket 等协议类型都会执行到这里
int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
err = sk_filter(sk, skb); // 执行 BPF 代码,这里返回的 err 表示对这个包保留前多少字节(trim)
if (err) // 如果字节数大于 0
return err; // 跳过接下来的处理逻辑,直接返回到更上层
// 如果字节数等于 0,继续执行内核正常的 socket receive 逻辑
return __sock_queue_rcv_skb(sk, skb);
}
如果 sk_filter()
的返回值 err
err != 0
:直接 return err
,返回到调用方,不再继续原来正常的内核处理逻辑 __sock_queue_rcv_skb()
;所以效果就是:将这个包过滤了出来(符合过滤条件);err == 0
:接下来继续执行正常的内核处理,也就是这个包不符合过滤条件;所以至此大概就知道要实现过滤和截断功能,程序应该返回什么了。要精确搞清楚,需要
看 sk_filter()
一直调用到 BPF 程序的代码,看中间是否对 BPF 程序的返回值做了封
装和转换。
这里给出结论:BPF 程序的返回值,
n
(n < pkt_size
):返回一个 截断的包(副本),只保留前面 n
个字节。0
:忽略这个包;需要说明:
setsockopt()
通过 setsockopt(fd, SO_ATTACH_BPF, ...)
系统调用,其中 fd 是
BPF 程序的文件描述符。
samples/bpf/sockex1
~ samples/bpf/sockex3
这三个例子都是用 BPF 程序 过滤网络设备设备上的包, 根据协议类型、IP、端口等信息统计流量。
源码 samples/bpf/。
$ cd samples/bpf/
$ make
$ ./sockex1
下面的例子根据包的协议类型对包进行截断。简单起见,不解析 IPv4 option 字段。
#include <uapi/linux/bpf.h>
#include <uapi/linux/in.h>
#include <uapi/linux/types.h>
#include <uapi/linux/string.h>
#include <uapi/linux/if_ether.h>
#include <uapi/linux/if_packet.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/tcp.h>
#include <uapi/linux/udp.h>
#include <bpf/bpf_helpers.h>
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
// We are only interested in TCP/UDP headers, so drop every other protocol
// and trim packets after the TCP/UDP header by returning eth_len + ipv4_hdr + TCP/UDP header
__section("socket")
int bpf_trim_skb(struct __sk_buff *skb)
{
int proto = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
int size = ETH_HLEN + sizeof(struct iphdr);
switch (proto) {
case IPPROTO_TCP: size += sizeof(struct tcphdr); break;
case IPPROTO_UDP: size += sizeof(struct udphdr); break;
default: size = 0; break; // drop this packet
}
return size;
}
char _license[] __section("license") = "GPL";
编译及测试:比较简单的方法是将源文件放到内核源码树中 samples/bpf/
目录下。
参考其中的 sockex1
来编译、加载和测试。
相关实现见 sk_filter_trim_cap()
,
它会进一步调用 bpf_prog_run_save_cb()
。
这里所说的 socket 事件包括建连(connection establishment)、重传(retransmit)、超时(timeout)等等。
这种场景只会拦截和解析系统事件,不会修改任何东西。
拦截到事件后,通过 bpf_setsockopt()
动态修改 socket 配置,
能够实现 per-connection 的优化,提升性能。例如,
BPF_PROG_TYPE_SK_SKB
程序配合)这个其实算是“动态修改”的特例。与 BPF_PROG_TYPE_SK_SKB
程序配合,通过
sockmap+redirection 实现 socket 重定向。这种情况下分为两段 BPF 程序,
BPF_PROG_TYPE_SOCK_OPS
程序,拦截 socket 事件,并从 struct bpf_sock_ops
中提取 socket 信息存储到 sockmap;BPF_PROG_TYPE_SK_SKB
类型程序,从拦截到的 socket message 提取
socket 信息,然后去 sockmap 查找对端 socket,然后通过 bpf_sk_redirect_map()
直接重定向过去。其他类型的 BPF 程序都是在某个特定的代码出执行的,但 SOCK_OPS 程序不同,它们
在多个地方执行,op 字段表示触发执行的地方。
op
字段是枚举类型,完整列表:
// include/uapi/linux/bpf.h
/* List of known BPF sock_ops operators. */
enum {
BPF_SOCK_OPS_VOID,
BPF_SOCK_OPS_TIMEOUT_INIT, // 初始化 TCP RTO 时调用 BPF 程序
// 程序应当返回希望使用的 SYN-RTO 值;-1 表示使用默认值
BPF_SOCK_OPS_RWND_INIT, // BPF 程序应当返回 initial advertized window (in packets);-1 表示使用默认值
BPF_SOCK_OPS_TCP_CONNECT_CB, // 主动建连 初始化之前 回调 BPF 程序
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB, // 主动建连 成功之后 回调 BPF 程序
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,// 被动建连 成功之后 回调 BPF 程序
BPF_SOCK_OPS_NEEDS_ECN, // If connection's congestion control needs ECN */
BPF_SOCK_OPS_BASE_RTT, // 获取 base RTT。The correct value is based on the path,可能还与拥塞控制
// 算法相关。In general it indicates
// a congestion threshold. RTTs above this indicate congestion
BPF_SOCK_OPS_RTO_CB, // 触发 RTO(超时重传)时回调 BPF 程序,三个参数:
// Arg1: value of icsk_retransmits
// Arg2: value of icsk_rto
// Arg3: whether RTO has expired
BPF_SOCK_OPS_RETRANS_CB, // skb 发生重传之后,回调 BPF 程序,三个参数:
// Arg1: sequence number of 1st byte
// Arg2: # segments
// Arg3: return value of tcp_transmit_skb (0 => success)
BPF_SOCK_OPS_STATE_CB, // TCP 状态发生变化时,回调 BPF 程序。参数:
// Arg1: old_state
// Arg2: new_state
BPF_SOCK_OPS_TCP_LISTEN_CB, // 执行 listen(2) 系统调用,socket 进入 LISTEN 状态之后,回调 BPF 程序
};
从以上注释可以看到,这些 OPS 分为两种类型:
通过 BPF 程序的返回值来动态修改配置,类型包括
BPF_SOCK_OPS_TIMEOUT_INIT
BPF_SOCK_OPS_RWND_INIT
BPF_SOCK_OPS_NEEDS_ECN
BPF_SOCK_OPS_BASE_RTT
在 socket/tcp 状态发生变化时,回调(callback)BPF 程序,类型包括
BPF_SOCK_OPS_TCP_CONNECT_CB
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
BPF_SOCK_OPS_RTO_CB
BPF_SOCK_OPS_RETRANS_CB
BPF_SOCK_OPS_STATE_CB
BPF_SOCK_OPS_TCP_LISTEN_CB
引入该功能的内核 patch 见 bpf: Adding support for sock_ops;
SOCK_OPS 类型的 BPF 程序都是从 tcp_call_bpf() 调用过来的,这个文件中多个地方都会调用到该函数。
struct bpf_sock_ops *
结构体 定义,
// include/uapi/linux/bpf.h
struct bpf_sock_ops {
__u32 op; // socket 事件类型,就是上面的 BPF_SOCK_OPS_*
union {
__u32 args[4]; // Optionally passed to bpf program
__u32 reply; // BPF 程序的返回值。例如,op==BPF_SOCK_OPS_TIMEOUT_INIT 时,
// BPF 程序的返回值就表示希望为这个 TCP 连接设置的 RTO 值
__u32 replylong[4]; // Optionally returned by bpf prog
};
__u32 family;
__u32 remote_ip4; /* Stored in network byte order */
__u32 local_ip4; /* Stored in network byte order */
__u32 remote_ip6[4]; /* Stored in network byte order */
__u32 local_ip6[4]; /* Stored in network byte order */
__u32 remote_port; /* Stored in network byte order */
__u32 local_port; /* stored in host byte order */
...
};
如前面所述,ops 类型不同,返回值也不同。
指定以 BPF_CGROUP_SOCK_OPS
类型,将 BPF 程序 attach 到某个 cgroup 文件描述符。
依赖 cgroupv2。
内核已经有了 BPF_PROG_TYPE_CGROUP_SOCK
类型的 BPF 程序,这里为什么又要引入一个
BPF_PROG_TYPE_SOCK_OPS
类型的程序呢?
BPF_PROG_TYPE_CGROUP_SOCK
类型的 BPF 程序:在一个连接(connection)的生命周期中只执行一次,BPF_PROG_TYPE_SOCK_OPS
类型的 BPF 程序:在一个连接的生命周期中,在不同地方被多次调用。这个功能依赖 sockmap,后者是一种特殊类型的 BPF map,其中存储的是 socket 引用(references)。
典型流程:
根据提取到的 socket 信息判断接下来应该做什么的过程称为 verdict(判决)。 verdict 类型可以是:
__SK_DROP
__SK_PASS
__SK_REDIRECT
这种程序的一个应用是 strparser framework。
它与上层应用配合,在内核中提供应用层消息解析的支持(provide kernel support for application layer messages)。两个使用了 strparser 框架的例子:TLS 和 KCM( Kernel Connection Multiplexor)。
TODO
smap_parse_func_strparser()
/ smap_verdict_func()
Socket receive 过程执行到 smap_parse_func_strparser()
时,触发 STREAM_PARSER BPF 程序执行。
执行到 smap_verdict_func()
时,触发 VERDICT BPF 程序执行。
struct __sk_buff *
见 前面 的介绍。
从中可以提取出 socket 信息(IP、port 等)。
TODO
这种程序需要指定 BPF_SK_SKB_STREAM_*
类型,将 BPF 程序 attach 到 sockmap:
BPF_SK_SKB_STREAM_VERDICT
加载到某个 sockmap。BPF_SK_SKB_STREAM_PARSER
加载到某个 sockmap。strparser
框架:解析消息流将 BPF 程序用作 tc 分类器(classifiers)和执行器(actions)。
BPF_PROG_TYPE_SCHED_CLS
:tc classifier,分类器BPF_PROG_TYPE_SCHED_ACT
:tc action,动作TC 是 Linux 的 QoS 子系统。帮助信息(非常有用):
tc(8)
manpage for a general introductiontc-bpf(8)
for BPF specificstc(8)
命令支持 eBPF,因此能直接将 BPF 程序作为
classifiers 和 actions 加载到 ingress/egress hook 点。
如何使用 tc BPF 提供的能力,参考 man8: tc-bpf。
sch_handle_ingress()
/sch_handle_egress()
sch_handle_ingress()/egress()
会调用到 tcf_classify()
,
struct __sk_buff *
见 前面 的介绍。
返回 TC verdict 结果。
tc
命令(背后使用 netlink)步骤:
例如,
$ tc qdisc add dev eth0 clsact
$ tc filter add dev eth0 egress bpf da obj toy-proxy-bpf.o sec egress
加载过程分为 tc 前端和内核 bpf 后端两部分,中间通过 netlink socket 通信,源码分析见 Firewalling with BPF/XDP: Examples and Deep Dive
使用方式与 BPF_PROG_TYPE_SCHED_CLS
类似,但用作 TC action。
tc
命令XDP 位于设备驱动中(在创建 skb 之前),因此能最大化网络处理性能, 而且可编程、通用(很多厂商的设备都支持)。
由于 XDP 程序执行时 skb 都还没创建,开销非常低,因此效率非常高。适用于 DDoS 防御、四层负载均衡等场景。
XDP 就是通过 BPF hook 对内核进行运行时编程(run-time programming),但基于内核而不是绕过(bypass)内核。
XDP 是在网络驱动中实现的,有专门的 TX/RX queue(native 方式)。
对于没有实现 XDP 的驱动,内核中实现了一个称为 “generic XDP” 的 fallback 实现, 见 net/core/dev.c。
struct xdp_md *
定义,非常轻量级:
// include/uapi/linux/bpf.h
/* user accessible metadata for XDP packet hook */
struct xdp_md {
__u32 data;
__u32 data_end;
__u32 data_meta;
/* Below access go through struct xdp_rxq_info */
__u32 ingress_ifindex; /* rxq->dev->ifindex */
__u32 rx_queue_index; /* rxq->queue_index */
__u32 egress_ifindex; /* txq->dev->ifindex */
};
enum xdp_action
// include/uapi/linux/bpf.h
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP_PASS,
XDP_TX,
XDP_REDIRECT,
};
通过 netlink socket 消息 attach:
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
NLA_F_NESTED | 43
类型的 netlink 消息,表示这是 XDP message。消息中包含 BPF fd, the interface index (ifindex) 等信息。用 tc
attach BPF 程序,其实背后使用的也是 netlink socket。
TODO
CGroups 用于对一组进程(a group of processes)进行控制,
CGroups 最典型的使用场景是容器(containers)。
具体到 eBPF 方面,可以用 CGroups 来控制访问权限(allow or deny),程序的返回结果只有两种:
例如,很多 hook 会执行到宏 __cgroup_bpf_run_filter_skb()
,它负责执行 cgroup BPF 程序:
// include/linux/bpf-cgroup.h
#define BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb) \
({ \
int __ret = 0; \
if (cgroup_bpf_enabled) \
__ret = __cgroup_bpf_run_filter_skb(sk, skb, \
BPF_CGROUP_INET_INGRESS); \
\
__ret; \
})
#define BPF_CGROUP_RUN_SK_PROG(sk, type) \
({ \
int __ret = 0; \
if (cgroup_bpf_enabled) { \
__ret = __cgroup_bpf_run_filter_sk(sk, type); \
} \
__ret; \
})
完整的 cgroups hook 列表见 前面 enum bpf_attach_type
列表,其中的 BPF_CGROUP_*
。
在 IP egress/ingress 层禁止或允许网络访问。
sk_filter_trim_cap()
对于 inet ingress,sk_filter_trim_cap()
会调用
BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb)
;如果返回值非零,错误信息会传递给调
用方(例如,__sk_receive_skb()
),随后包会被丢弃并释放(discarded and freed)
。
egress 是类似的,但在 ip[6]_finish_output()
中。
struct sk_buff *skb
1
:放行;__cgroup_bpf_run_filter_skb()
返回 -EPERM
,这会进一步返
回给调用方,告诉它们应该丢弃该包。根据 BPF attach 的 hook 位置,选择合适的 attach 类型:
BPF_CGROUP_INET_INGRESS
BPF_CGROUP_INET_EGRESS
这里的 socket 相关事件包括 BPF_CGROUP_INET_SOCK_CREATE、BPF_CGROUP_SOCK_OPS。
struct sk_buff *skb
跟前面一样,程序返回 1 表示允许访问。
返回其他值会导致 __cgroup_bpf_run_filter_sk()
返回 -EPERM
,调用方收到这个返回值会将包丢弃。
inet_create()
Socket 创建时会执行 inet_create()
,里面会调用
BPF_CGROUP_RUN_PROG_INET_SOCK()
,如果该函数执行失败,socket 就会被释放。
TODO
三者都用于 kernel instrumentation。简单对比:
数据源 | Type | Kernel/User space | |
---|---|---|---|
kprobes | Dynamic | Kernel | 观测内核函数的运行时(进入和离开函数)参数值等信息 |
uprobes | Dynamic | Userspace | 同上,但观测的是用户态函数 |
tracepoints | Static | Kernel | 将自定义 handler 编译并加载到某些内核 hook,能拿到更多观测信息 |
USDT | Static | Userspace |
kprobes:对特定函数进行 instrumentation。
kprobe
kretprobe
启用后,会 将 probe 位置的一段空代码替换为一个断点指令。 当程序执行到这个断点时,会触发一条 trap 指令,然后保存寄存器状态, 跳转到指定的处理函数(instrumentation handler)。
Tracepoints:内核中的轻量级 hook。
Tracepoints 与 kprobes 类似,但 前者是动态插入代码来完成的,后者显式地(静态地)写在代码中的。 启用之后,会从这些地方收集 debug 信息。
同一个 tracepoints 可能会在多个地方声明;例如,
trace_drv_return_int()
在 net/mac80211/driver-ops.c 中的多个地方被调用。
查看可用的 tracepoints 列表:ls /sys/kernel/debug/tracing/events
。
Perf events:是这里提到的几种 eBPF 程序的基础。
BPF 基于已有的基础设施来完成事件采样(event sampling),允许 attach 程序到 感兴趣的 perf 事件,包括kprobes, uprobes, tracepoints 以及软件和硬件事件。
这些 instrumentation points 使 BPF 成为了一个通用的跟踪工具, 超越了最初的网络范畴。
通过 kprobe
/kretprobe
观测内核函数。
k[ret]probe_perf_func()
会执行加载到 probe 点的 BPF 程序。
另外,这种程序也能 attach 到 u[ret]probes
,详情见
uprobetracer.txt。
k[ret]probe_perf_func()
/u[ret]probe_perf_func()
启用某个 probe 并执行到断点时,k[ret]probe_perf_func()
会通过
trace_call_bpf()
执行 attach 在这个 probe 位置的 BPF 程序。
u[ret]probe_perf_func()
也是类似的。
struct pt_regs *ctx
可以通过这个指针访问寄存器。
这个变量内的很多字段是平台相关的,但也有一些通用函数,例如
regs_return_value(regs)
,返回的是存储程序返回值的寄存器内的值(x86 上对应的是 ax
寄存器)。
/sys/fs/debug/tracing/
目录下的配置文件/sys/kernel/debug/tracing/events/[uk]probe/<probename>/id
/sys/kernel/debug/tracing/events/[uk]retprobe/<probename>/id
Documentation/trace/kprobetrace.txt 有详细的例子。例如,
# 创建一个名为 `myprobe` 的程序,attach 到进入函数 `tcp_retransmit_skb()` 的地方
$ echo 'p:myprobe tcp_retransmit_skb' > /sys/kernel/debug/tracing/kprobe_events
# 获取 probe id
$ cat /sys/kernel/debug/tracing/events/kprobes/myprobe/id
2266
用以上 id 打开一个 perf event,启用它,然后将这个 perf event 的 BPF 程序指定为我们的程序。 过程可参考 load_and_attach():
// samples/bpf/bpf_load.c
static int load_and_attach(const char *event, struct bpf_insn *prog, int size)
{
struct perf_event_attr attr;
/* Load BPF program and assign programfd to it; and get probeid of probe from sysfs */
attr.type = PERF_TYPE_TRACEPOINT;
attr.sample_type = PERF_SAMPLE_RAW;
attr.sample_period = 1;
attr.wakeup_events = 1;
attr.config = probeid; // /sys/kernel/debug/tracing/events/kprobes/<probe>/id
eventfd = sys_perf_event_open(&attr, -1, 0, programfd, 0);
ioctl(eventfd, PERF_EVENT_IOC_ENABLE, 0);
ioctl(eventfd, PERF_EVENT_IOC_SET_BPF, programfd);
...
}
TODO
启用方式和上面的 kprobe 类似:
$ echo 1 > /sys/kernel/xxx/enable
可跟踪的事件都在 /sys/kernel/debug/tracing/events
目录下面。
perf_trace_<event_class>()
相应的 tracepoint 启用并执行到之后,perf_trace_<event_class>()
(定义见 include/trace/perf.h)
调用 perf_trace_run_bpf_submit()
,后者通过 trace_call_bpf()
触发 BPF 程序执行。
传入的参数和类型因 tracepoint 而异,见其定义。
见 /sys/kernel/debug/tracing/events/<tracepoint>/format
。例如,
$ sudo cat /sys/kernel/debug/tracing/events/net/netif_rx/format
name: netif_rx
ID: 1457
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:void * skbaddr; offset:8; size:8; signed:0;
field:unsigned int len; offset:16; size:4; signed:0;
field:__data_loc char[] name; offset:20; size:4; signed:1;
print fmt: "dev=%s skbaddr=%p len=%u", __get_str(name), REC->skbaddr, REC->len
顺便看一下这个 tracepoint 在内核中的实现,
// net/core/dev.c
static int netif_rx_internal(struct sk_buff *skb)
{
net_timestamp_check(netdev_tstamp_prequeue, skb);
trace_netif_rx(skb);
...
}
/sys/fs/debug/tracing/
目录下的配置文件例如,
# 启用 `net/net_dev_xmit` tracepoint as "myprobe2"
$ echo 'p:myprobe2 trace:net/net_dev_xmit' > /sys/kernel/debug/tracing/kprobe_events
# 获取 probe ID
$ cat /sys/kernel/debug/tracing/events/kprobes/myprobe2/id
2270
过程加载代码可参考 load_and_attach()。
包括系统调用事件、定时器超时事件、硬件采样事件等。硬件事件包括 PMU(processor monitoring unit)事件,它告诉我们已经执行了多少条指令之类的信息。
Perf 事件监控能具体到某个进程、组、处理器,也可以指定采样频率。
ioctl()
ioctl(fd, PERF_EVENT_IOC_SET_BPF)
设置 BPF程序,struct bpf_perf_event_data *
定义:
// include/uapi/linux/bpf_perf_event.h
struct bpf_perf_event_data {
bpf_user_pt_regs_t regs;
__u64 sample_period;
__u64 addr;
};
取决于 perf event firing 和选择的采样频率。
Lightweight tunnels 提供了对内核路 由子系统的编程能力,据此可以实现轻量级隧道。
举个例子,下面是没有 BPF 编程能力时,如何(为不同协议)添加路由:
# VXLAN:
$ ip route add 40.1.1.1/32 encap vxlan id 10 dst 50.1.1.2 dev vxlan0
$ MPLS:
$ ip route add 10.1.1.0/30 encap mpls 200 via inet 10.1.1.1 dev swp1
有了 BPF 可编程性之后,能为出向流量(入向是只读的)做封装。 详见 BPF for lightweight tunnel encapsulation。
与 tc
类似,ip route
支持直接将 BPF 程序 attach 到网络设备:
$ ip route add 192.168.253.2/32 \
encap bpf out obj lwt_len_hist_kern.o section len_hist \
dev veth0
Examine inbound packets for lightweight tunnel de-encapsulation.
lwtunnel_input()
该函数支持多种封装类型。 The BPF case runs bpf_input in net/core/lwt_bpf.c with redirection disallowed.
struct sk_buff *
ip route add
$ ip route add <route+prefix> encap bpf in obj <bpf obj file.o> section <ELF section> dev <device>
TODO
ip route add
$ ip route add <route+prefix> encap bpf out obj <bpf object file.o> section <ELF section> dev <device>
struct __sk_buff *
lwtunnel_output()
lwtunnel_xmit()
struct __sk_buff *
定义见 前面。
ip route add
$ ip route add <route+prefix> encap bpf xmit obj <bpf obj file.o> section <ELF section> dev <device>