[转帖]你的第一个XDP BPF 程序

第一个,xdp,bpf,程序 · 浏览次数 : 0

小编点评

**XDP程序简介** XDP (X-Direct Programming) 是一种内核编程技术,允许内核直接与用户空间应用程序进行交互。通过 XDP,我们可以从内核空间中获取用户空间应用程序的运行数据,并进行各种编程操作。 **XDP程序加载** XDP程序加载到内核的XDP Hook 上。XDP Hook 是内核中一个专门的区域,用于处理 XDP 请求。当内核收到 XDP 请求时,会将请求传递到 XDP Hook 上。 **XDP程序验证** XDP程序通过验证器进行验证。验证器确保 XDP 程序加载到内核中并安全。验证器可以检查 XDP 程序的版本、运行数据和安全模式。 **XDP编程操作** XDP 程序可以通过各种方法访问用户空间应用程序的运行数据。例如,我们可以通过 XDP 访问用户空间应用程序的运行数据,或我们可以通过 XDP 访问用户空间应用程序的线程。 **XDP示例程序** 以下是一个 XDP 示例程序的链接: ``` https://elixir.bootlin.com/linux/latest/source/samples/bpf ``` **XDP示例程序代码** ```c #include // XDP 代码示例 int xdp_example(void) { // 初始化 XDP 上下文 struct xdp_ctx *ctx = xdp_create_ctx(); // 获取用户空间应用程序的运行数据 int data = xdp_get_user_data(); // 访问用户空间应用程序的线程 struct xdp_thread *thread = xdp_create_thread(); // 释放 XDP 上下文 xdp_destroy_ctx(ctx); // 返回运行数据 return data; } ``` **XDP 编程优势** * XDP 允许内核与用户空间应用程序进行交互。 * XDP 允许内核访问用户空间应用程序的运行数据。 * XDP 允许内核进行各种编程操作,例如访问用户空间应用程序的线程、执行内核函数等。

正文

https://cloud.tencent.com/developer/inventory/600/article/1626925

 


这是一个实战系列文章,它是eBPF学习计划里面的应用场景之网络部分,终极目标是源码级别学习云原生网络方案Cilium(声明:下文提到的BPF字样是泛指,包括cBPF和eBPF)。 Cilium方案中大量使用了XDP、TC等网络相关的BPF hook,以实现高性能的网络RX和TX。 今天我们先来讲讲XDP应用理论,再来学习编写第一个XDP BPF程序。

目录

  • TL;DR
  • 网络钩子(hook)
  • XDP使用介绍
  • 引入XDP之后网络Data Path
  • XDP 开发环境搭建
  • 设计你的第一个XDP程序
  • 编译XDP程序
  • 加载XDP程序
  • 验证第一个XDP程序的效果
  • Ingress or Egress
  • 推荐阅读Linux内核代码的利器
  • 特别通知

TL;DR

文章涉及的实验环境和代码可以到这个git repo获取。

https://github.com/nevermosby/linux-bpf-learning

网络钩子(hook)

在计算机网络中,Hook钩子在操作系统中用于在调用前或执行过程中拦截网络数据包。Linux内核中暴露了多个钩子,BPF程序可以连接到这些钩子上,实现数据收集和自定义事件处理。虽然Linux内核中的钩子点很多,但我们将重点关注网络子系统中存在的两个钩子:XDP)和TC。它们结合在一起,可以用来处理RX和TX上两个链路上靠近NIC的数据包,从而实现了许多网络应用的开发。今天我们先来讲讲XDP。

XDP使用介绍

XDP全称为eXpress Data Path,是Linux内核网络栈的最底层。它只存在于RX路径上,允许在网络设备驱动内部网络堆栈中数据来源最早的地方进行数据包处理,在特定模式下可以在操作系统分配内存(skb)之前就已经完成处理。

XDP暴露了一个可以加载BPF程序的网络钩子。在这个钩子中,程序能够对传入的数据包进行任意修改和快速决策,避免了内核内部处理带来的额外开销。这使得XDP在性能速度方面成为最佳钩子,例如缓解DDoS攻击等,相关背景知识可以看这篇文章

XDP输入参数

XDP暴露的钩子具有特定的输入上下文,它是单一输入参数。它的类型为 struct xdp_md,在内核头文件bpf.h 中定义,具体字段如下所示:

/* user accessible metadata for XDP packet hook
 * new fields must be added to the end of this structure
 */
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  */
};

程序执行时,datadata_end字段分别是数据包开始和结束的指针,它们是用来获取和解析传来的数据,第三个值是data_meta指针,初始阶段它是一个空闲的内存地址,供XDP程序与其他层交换数据包元数据时使用。最后两个字段分别是接收数据包的接口和对应的RX队列的索引。当访问这两个值时,BPF代码会在内核内部重写,以访问实际持有这些值的内核结构struct xdp_rxq_info

XDP输出参数

在处理完一个数据包后,XDP程序会返回一个动作(Action)作为输出,它代表了程序退出后对数据包应该做什么样的最终裁决,也是在内核头文件bpf.h 定义了以下5种动作类型:

enum xdp_action {
	XDP_ABORTED = 0, // Drop packet while raising an exception
	XDP_DROP, // Drop packet silently
	XDP_PASS, // Allow further processing by the kernel stack
	XDP_TX, // Transmit from the interface it came from
	XDP_REDIRECT, // Transmit packet from another interface
};

可以看出这个动作的本质是一个int值。前面4个动作是不需要参数的,最后一个动作需要额外指定一个NIC网络设备名称,作为转发这个数据包的目的地。

引入XDP之后网络Data Path

在没有引入XDP之前,原来是的网络数据包传输路径是这样的:

启用XDP后,网络包传输路径是这样的:

可以看到多了3个红色方框圈起来的新链路,我们来一一介绍:

  • offload模式,XDP程序直接hook到可编程网卡硬件设备上,与其他两种模式相比,它的处理性能最强;由于处于数据链路的最前端,过滤效率也是最高的。如果需要使用这种模式,需要在加载程序时明确声明。目前支持这种模式的网卡设备不多,有一家叫netronome
  • native模式,XDP程序hook到网络设备的驱动上,它是XDP最原始的模式,因为还是先于操作系统进行数据处理,它的执行性能还是很高的,当然你的网络驱动需要支持,目前已知的有i40e, nfp, mlx系列ixgbe系列
  • generic模式,这是操作系统内核提供的通用 XDP兼容模式,它可以在没有硬件或驱动程序支持的主机上执行XDP程序。在这种模式下,XDP的执行是由操作系统本身来完成的,以模拟native模式执行。好处是,只要内核够高,人人都能玩XDP;缺点是由于是仿真执行,需要分配额外的套接字缓冲区(SKB),导致处理性能下降,跟native模式在10倍左右的差距。

当前主流内核版本的Linux系统在加载XDP BPF程序时,会自动在native和generic这两种模式选择,完成加载后,可以使用ip命令行工具来查看选择的模式。

XDP 开发环境搭建

说完了理论,我们就要开始动手了。先来搭建实验环境。

  • 操作系统 建议使用Ubuntu 18.04 和CentOS 8系列最新稳定版,内核版本不要低于4.15,有条件的话,请选择最新稳定版。本次演示环境是Ubuntu 18.04 with Kernel 4.15。
  • 安装依赖的软件包

设计你的第一个XDP程序

通过上文的介绍,可以了解到XDP能够实现操控传入的数据包,它在业界最出名的一个应用场景就是Facebook基于XDP实现高效的防DDoS攻击,其本质上就是实现尽可能早地实现「丢包」,而不去消耗系统资源创建完整的网络栈链路,即「early drop」。

那么第一个XDP程序就来模拟防DDoS最重要的操作,取名为「丢掉你的整个世界」。如果你对第一个程序的设计有其他想法,建议可以去Linux源码里找找灵感:https://github.com/torvalds/linux/tree/master/samples/bpf,那里以xdp开头的文件都是很好的参考。

闲话不说,上代码(文件名为xdp-drop-world.c):

#include <linux/bpf.h>

/*
 * Comments from Linux Kernel:
 * Helper macro to place programs, maps, license in
 * different sections in elf_bpf file. Section names
 * are interpreted by elf_bpf loader.
 * End of comments

 * You can either use the helper header file below
 * so that you don't need to define it yourself:
 * #include <bpf/bpf_helpers.h> 
 */
#define SEC(NAME) __attribute__((section(NAME), used))

SEC("xdp")
int xdp_drop_the_world(struct xdp_md *ctx) {
    // drop everything
	// 意思是无论什么网络数据包,都drop丢弃掉
    return XDP_DROP;
}

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

这是C语言代码,应该不会太难看懂吧。其中定义了一个函数,里面就一行代码,返回一个int类型,最后是代码许可证声明。做个简单的解释:

  1. 第一部分是第一行的头文件linux/bpf.h,它包含了BPF程序使用到的所有结构和常量的定义(除了一些特定的子系统,如TC,它需要额外的头文件)。理论上来说,所有的eBPF程序第一行都是这个头文件。
  2. 第二部分是第二行的宏定义,它的作用是赋予了SEC(NAME)这一串字符具有意义,即可以被编译通过。我截取了Linux内核代码里的注释,可以看出这段宏定义是为了ELF格式添加Section信息的。ELF全称是Executable and Linkable Format,就是可执行文件的一种主流格式(详细介绍点这里),广泛用于Linux系统,我们的BPF程序一旦通过编译后,也会是这种格式。下面代码中的SEC("xdp")SEC("license")都是基于这个宏定义。
  3. 第三部分,也就是我们的代码主体,它是一个命名为xdp_drop_the_world函数,,返回值为int类型,接受一个参数,类型为xdp_md结构,上文已经介绍过,这个例子没有使用到这个参数。函数内的就是一行返回语句,使用XDP_DROP,也就是1,意思就是丢弃所有收到的数据包。
  4. 第四部分是最后一行的许可证声明。这行其实是给程序加载到内核时BPF验证器看的,因为有些eBPF函数只能被具有GPL兼容许可证的程序调用。因此,验证器会检查程序所使用的函数的许可证和程序的许可证是否兼容,如果不兼容,则拒绝该程序。
  5. 还有一点,大家是否注意到整个程序是没有main入口的,事实上,程序的执行入口可以由前面提到的ELF格式的对象文件中的Section来指定。入口也有默认值,它是ELF格式文件中.text这个标识的内容,程序编译时会将能看到的函数放到.text里面。

以上是对我们第一个XDP程序的简要介绍,很多相关的背景知识没有一一说明,大家可移步Linux内核源码一探究竟。

编译XDP程序

设计并编写完我们的程序代码后,接下来就是编译工作了。可以利用clang命令行工具配合后端编译器LLVM来进行操作(相关介绍看这里):

# -02: Moderate level of optimization which enables most optimizations,对生成的执行文件进行中等程度的优化
> clang -O2 -target bpf -c xdp-drop-world.c -o xdp-drop-world.o

# 查看生成的elf格式的可执行文件的相关信息
# 能看到上文提到的Section信息
> readelf -a xdp-drop-world.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          216 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         6
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  000000a0
       0000000000000037  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] xdp               PROGBITS         0000000000000000  00000040
       0000000000000010  0000000000000000  AX       0     0     8
  [ 4] license           PROGBITS         0000000000000000  00000050
       0000000000000004  0000000000000000  WA       0     0     1
  [ 5] .symtab           SYMTAB           0000000000000000  00000058
       0000000000000048  0000000000000018           1     1     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 3 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    4 _license
     2: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    3 xdp_drop_the_world

No version information found in this file.

还可以通过llvm-objdump这个工具来分析下这个可执行文件的反汇编指令信息:

> llvm-objdump -S xdp-drop-world.o
xdp-world-drop.o:	file format ELF64-BPF

Disassembly of section xdp:
xdp_drop_the_world:
       0:	b7 00 00 00 01 00 00 00 	r0 = 1 # 1代表XDP_DROP,这句指令表示赋值1到代表返回值的寄存器r0
       1:	95 00 00 00 00 00 00 00 	exit

加载XDP程序

由于我们的实验环境配置的网卡和网卡驱动都不支持XDP hook,所以肯定是用了 xdpgeneric模式,我们不进行性能对比,因此不影响我们演示效果。加载XDP程序就要用到ip这个命令行工具了,它能帮助我们将程序加载到内核的XDP Hook上。上命令:

ip link set dev [device name] xdp obj xdp-drop-world.o sec [section name]

简单解释下:

  1. sec [section name]就是上文提到的通过Section来指定程序入口
  2. device name是本机某个网卡设备的名称,可以通过ip a查看本机所有的网卡设备。一般可以选取本机对外的IP所在的网卡设备。

因此完整的命令为:

# 查看主机的网卡设备列表,选取本机IP所在的网卡设备
> ip a
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:5c:7d:8f brd ff:ff:ff:ff:ff:ff
    inet 192.168.58.112/24 brd 192.168.58.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe5c:7d8f/64 scope link
       valid_lft forever preferred_lft forever
# 加载XDP程序到这个网卡设备上
> ip link set dev enp0s8 xdp obj xdp-drop-world.o sec xdp verbose
# 如下信息就是没有报错,说明已经通过BPF验证器并attach到内核XDP hook上了
Prog section 'xdp' loaded (5)!
 - Type:         6
 - Instructions: 2 (0 over limit)
 - License:      GPL

Verifier analysis:

0: (b7) r0 = 1
1: (95) exit
processed 2 insns, stack depth 0

验证第一个XDP程序的效果

测试场景很简单:

  1. 从外部ping作为实验环境的虚拟机IP(就是上文中的192.168.58.112),期望是无法ping通。
  2. 然后通过以下命令把XDP卸载掉,即detach from XDP hook,发现又能ping通:

那么就能说明我们的第一个XDP程序就工作了!期间,可以tcpdump来观察网络联通情况。下面是完整的Demo视频,结果符合我们的期望。

Ingress or Egress

上面的测试场景是验证了经过目标网络设备的Ingress流量被我们的XDP程序drop了,专业术语叫RX流向。那么Egress流量是否也会被drop掉呢?

答案是,不会。XDP hook不会作用到Egress流量,也就是TX流向。读者可以自行在已经attach XDP程序的实验环境中,ping一个外部地址,请保证这次请求会经过被attach XDP程序的网络设备。其结果就是请求没有收到任何影响。

那么谁能帮我们解决Egress流量控制的问题呢?

那就是TC。下篇文章我们来讨论它。

推荐阅读Linux内核代码的利器

这款是一个在线查看Linux内核代码的网站,支持快速查看某个变量和结构的引用和定义。给大家放一个BPF示例程序的链接:

https://elixir.bootlin.com/linux/latest/source/samples/bpf

特别通知

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1zc1v65a3om8c

与[转帖]你的第一个XDP BPF 程序相似的内容:

[转帖]你的第一个XDP BPF 程序

https://cloud.tencent.com/developer/inventory/600/article/1626925 这是一个实战系列文章,它是eBPF学习计划里面的应用场景之网络部分,终极目标是源码级别学习云原生网络方案Cilium(声明:下文提到的BPF字样是泛指,包括cBPF和e

[转帖]docker 搭建 redis 伪分布式集群

https://www.jianshu.com/p/453a2d70a5de 建议阅读方式 可前往语雀阅读,体验更好:docker 搭建 redis 伪分布式集群 背景介绍 该实验主要来源于《Docker 容器与容器云 第2版》一书的 2.3 节:“搭建你的第一个 Docker 应用栈”中的一小步,

[转帖]带你重走 TiDB TPS 提升 1000 倍的性能优化之旅

https://tidb.net/blog/29074d86#TiDB%20%E6%80%A7%E8%83%BD%E5%92%8C%E7%A8%B3%E5%AE%9A%E6%80%A7%E7%9A%84%E6%8C%91%E6%88%98 今天我们来聊一下数据库的性能优化,第一部分简单介绍一下性能优

[转帖]TiDB 数据库统计表的大小方法

简介:TiDB统计表的大小,列出了一些方法: 1、第一种的统计方式: 基于统计表 METRICS_SCHEMA.store_size_amplification 要预估 TiDB 中一张表的大小,你可以参考使用以下查询语句: SELECT db_name, table_name, ROUND(SUM

[转帖]帮你精通Linux:Find命令高阶操作4项动作

八列属性 本文将继续探讨其高级查询功能,将分为四个方面展开讨论: 预定义动作 Predefined-Actions自定义动作 User-defined Actions与grep协同动作Operator逻辑操作 一、预定义动作 Predefined Actions 搜索是第一步,第二步是处理搜索的结果

[转帖]Brendan Gregg: 一个实战派大神

第一次知道Brendan Gregg,是我还在Juniper的时候。插点花絮,关于Juniper,你可以百度下“程序员薪水最高的25家公司”,那就是因为这条新闻才打定了主意去的Juniper, 只能说,Juniper的HR们很优秀。 言归正传,我那会在Juniper主要是研究网络性能优化的一些东西,

[转帖]高性能网络 | 你所不知道的TIME_WAIT和CLOSE_WAIT

https://zhuanlan.zhihu.com/p/528747315 你遇到过TIME_WAIT的问题吗? 我相信很多都遇到过这个问题。一旦有用户在喊:网络变慢了。第一件事情就是,netstat -a | grep TIME_WAIT | wc -l 一下。哎呀妈呀,几千个TIME_WAIT

[转帖]掌握Linux中的12个grep命令

http://blog.itpub.net/70023145/viewspace-2924123/ 你是否遇到过需要在文件中查找一个特定的字符串或者样式,但是不知道从哪儿开始?那么,就请grep来帮你吧。 grep是每个Linux发行版都预装的一个强有力的文件模式搜索工具。无论何种原因,如果你的系统

[转帖]你真的了解nf_conntrack么?

https://blog.51cto.com/u_15293891/3290242 女主宣言 该文章出自HULK虚拟化团队(网络小分队),主要是基于在奥创版本升级过程中遇到的一个nf_conntrack问题展开的。该问题在日常开启了iptables的高并发运维场景中也会经常出现。该文章主要是结合实际

[转帖]一文带你玩转 Redis 的 RESP 协议 !

https://zhuanlan.zhihu.com/p/384251739 RESP 是 Redis 客户端与 Redis 服务器相互通信时使用的一个协议, 全称 REdis Serialization Protocol ,即 redis 串行协议,通俗易懂,也表明了 redis 的特点,串行化(