eBPF 中的 XDP 程序允许进行非常高效的、自定义的数据包处理。eBPF XDP 程序在数据包到达内核网络堆栈之前运行。
翻译自 Catch Performance in eBPF with Rust: XDP Programs 。
这是五部分系列文章中的第二部分。在此阅读第一部分。
在这个系列中,我们学习了 eBPF 是什么,以及与之相关的工具,为什么 eBPF 性能很重要,以及如何使用连续基准测试来跟踪性能。在本系列的这一篇文章中,我们将讨论如何使用 Aya 在 Rust 中创建一个基本的 eBPF XDP 程序。该项目的所有源代码都是开源的,可以在 GitHub 上获取。
eBPF XDP 程序允许进行非常高效的、自定义的数据包处理。eBPF XDP 程序在数据包到达内核的网络堆栈之前运行。eBPF XDP 程序可以执行四种不同的操作:
XDP_PASS:将数据包传递给正常的网络堆栈进行处理。数据包内容可以被修改。XDP_DROP:丢弃数据包并不对其进行处理。这是最快的操作。XDP_TX:将数据包转发到它所在的相同网络接口。数据包内容可以被修改。XDP_ABORTED:在处理过程中出现错误,因此丢弃数据包并不进行处理。这表示 eBPF 程序中的错误。在我们的基本示例中,如果一切顺利,我们只会执行第一个操作 XDP_PASS ,因为我们更关注的是脚手架和进程间通信,而不是数据包处理逻辑。我们的初始版本的 eBPF XDP 应用程序只会记录接收到的每个数据包的 IPv4 源地址。

让我们首先看一下内核方面的代码:
#[xdp(name = fun_xdp)]
pub fn fun_xdp(ctx: XdpContext) -> u32 {
match try_fun_xdp(&ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}逐行解释:
fun_xdp 的 eBPF XDP 程序。try_fun_xdp 的辅助函数,我们将在下面讨论。根据它的返回值,如果返回 Ok ,则一切正常,我们返回给定的值。否则,如果得到一个 Err ,我们中止执行。现在让我们来看一下 try_fun_xdp 函数:
fn try_fun_xdp(ctx: &XdpContext) -> Result<u32, ()> {
let eth_hdr: *const EthHdr = unsafe { ptr_at(ctx, 0)? };
unsafe {
let EtherType::Ipv4 = (*eth_hdr).ether_type else {
return Ok(xdp_action::XDP_PASS);
};
}
let ipv4_hdr: *const Ipv4Hdr = unsafe { ptr_at(ctx, EthHdr::LEN)? };
let source_addr = unsafe { (*ipv4_hdr).src_addr };
info!(ctx, "IPv4 Source Address: {}", source_addr);
Ok(xdp_action::XDP_PASS)
}逐行解释:
try_fun_xdp 函数接受一个对上下文的引用,并返回一个 Result ,其中包含一个 Ok 的无符号 32 位整数值或一个空的 Err 。unsafe 的 ptr_at 辅助函数,我们接下来会讨论它。unsafe 的,因此我们必须显式地选择它们。unsafe 的 ptr_at 辅助函数。最后让我们看一下 ptr_at 辅助函数:
#[inline(always)]
unsafe fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
let start = ctx.data();
let end = ctx.data_end();
let len = core::mem::size_of::<T>();
if start + offset + len > end {
return Err(());
}
Ok((start + offset) as _)
}逐行解释:
T 的数据。对于成功读取, Result 是一个指向 T 的指针的 Ok 。否则,返回一个空的 Err 。T 的字节数。offset 处读取 T 。还有一个最后的函数:
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}逐行解释:
现在转到用户空间的部分。让我们看看我们的 main 函数:
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let opt = Opt::parse();
env_logger::init();
let mut bpf = Bpf::load(include_bytes_aligned!("../path/to/ebpf-bin"))?;
BpfLogger::init(&mut bpf)?;
let program: &mut Xdp = bpf.program_mut(fun_xdp).unwrap().try_into()?;
program.load()?;
program.attach(&opt.iface, XdpFlags::default())?;
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await?;
info!("Exiting...");
Ok(())
}
#[derive(clap::Parser)]
struct Opt {
#[clap(long)]
iface: String,
}逐行解释:
tokio 创建了一个异步运行时来运行我们的程序。main 函数。在 Rust 二进制文件中, main 函数是事实上的入口点。该函数的结果是一个空的 Ok 或使用 anyhow crate 捕获所有的 Err 。fun_xdp eBPF XDP 程序。fun_xdp eBPF XDP 程序加载到内核中,使用默认标志。fun_xdp eBPF XDP 程序附加到一个由 iface 命令行参数设置的网络接口上。Opt 结构中定义的命令行参数。Opt 。clap 这个字段应该作为长参数名进行解析,即 --iface 。iface ,其值为字符串。通过以上代码,我们已经创建了一个非常基本的 eBPF 程序。同样,该项目的所有源代码都是开源的,并且可在 GitHub 上获得。