关于 BPF 和 eBPF 的笔记 | LinuxCN Mirror
Skip to content
返回

关于 BPF 和 eBPF 的笔记

今天,我喜欢的 meetup 网站上有一篇我超爱的文章!Suchakra Sharma@tuxology 在 twitter/github)的一篇非常棒的关于传统 BPF 和在 Linux 中最新加入的 eBPF 的讨论文章,正是它促使我想去写一个 eBPF 的程序!

这篇文章就是 —— BSD 包过滤器:一个新的用户级包捕获架构

我想在讨论的基础上去写一些笔记,因为,我觉得它超级棒!

开始前,这里有个 幻灯片 和一个 pdf。这个 pdf 非常好,结束的位置有一些链接,在 PDF 中你可以直接点击这个链接。

什么是 BPF?

在 BPF 出现之前,如果你想去做包过滤,你必须拷贝所有的包到用户空间,然后才能去过滤它们(使用 “tap”)。

这样做存在两个问题:

  1. 如果你在用户空间中过滤,意味着你将拷贝所有的包到用户空间,拷贝数据的代价是很昂贵的。
  2. 使用的过滤算法很低效。

问题 #1 的解决方法似乎很明显,就是将过滤逻辑移到内核中。(虽然具体实现的细节并没有明确,我们将在稍后讨论)

但是,为什么过滤算法会很低效?

如果你运行 tcpdump host foo,它实际上运行了一个相当复杂的查询,用下图的这个树来描述它:

评估这个树有点复杂。因此,可以用一种更简单的方式来表示这个树,像这样:

然后,如果你设置 ether.type = IPip.src = foo,你必然明白匹配的包是 host foo,你也不用去检查任何其它的东西了。因此,这个数据结构(它们称为“控制流图” ,或者 “CFG”)是表示你真实希望去执行匹配检查的程序的最佳方法,而不是用前面的树。

为什么 BPF 要工作在内核中

这里的关键点是,包仅仅是个字节的数组。BPF 程序是运行在这些字节的数组之上。它们不允许有循环(loop),但是,它们 可以 有聪明的办法知道 IP 包头(IPv6 和 IPv4 长度是不同的)以及基于它们的长度来找到 TCP 端口:

x = ip_header_length
port = *(packet_start + x + port_offset) 

(看起来不一样,其实它们基本上都相同)。在这个论文/幻灯片上有一个非常详细的虚拟机的描述,因此,我不打算解释它。

当你运行 tcpdump host foo 后,这时发生了什么?就我的理解,应该是如下的过程。

  1. 转换 host foo 为一个高效的 DAG 规则
  2. 转换那个 DAG 规则为 BPF 虚拟机的一个 BPF 程序(BPF 字节码)
  3. 发送 BPF 字节码到 Linux 内核,由 Linux 内核验证它
  4. 编译这个 BPF 字节码程序为一个 原生 native 代码。例如,这是个ARM 上的 JIT 代码 以及 x86 的机器码
  5. 当包进入时,Linux 运行原生代码去决定是否过滤这个包。对于每个需要去处理的包,它通常仅需运行 100 - 200 个 CPU 指令就可以完成,这个速度是非常快的!

现状:eBPF

毕竟 BPF 出现已经有很长的时间了!现在,我们可以拥有一个更加令人激动的东西,它就是 eBPF。我以前听说过 eBPF,但是,我觉得像这样把这些片断拼在一起更好(我在 4 月份的 netdev 上我写了这篇 XDP & eBPF 的文章回复)

关于 eBPF 的一些事实是:

kprobes 和 eBPF

你可以在 Linux 内核中挑选一个函数(任意函数),然后运行一个你写的每次该函数被调用时都运行的程序。这样看起来是不是很神奇。

例如:这里有一个 名为 disksnoop 的 BPF 程序,它的功能是当你开始/完成写入一个块到磁盘时,触发它执行跟踪。下图是它的代码片断:

BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
    // stash start timestamp by request ptr
    u64 ts = bpf_ktime_get_ns();
    start.update(&req, &ts);
}
...
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")

本质上它声明一个 BPF 哈希(它的作用是当请求开始/完成时,这个程序去触发跟踪),一个名为 trace_start 的函数将被编译进 BPF 字节码,然后附加 trace_start 到内核函数 blk_start_request 上。

这里使用的是 bcc 框架,它可以让你写 Python 式的程序去生成 BPF 代码。你可以在 https://github.com/iovisor/bcc 找到它(那里有非常多的示例程序)。

uprobes 和 eBPF

因为我知道可以附加 eBPF 程序到内核函数上,但是,我不知道能否将 eBPF 程序附加到用户空间函数上!那会有更多令人激动的事情。这是 在 Python 中使用一个 eBPF 程序去计数 malloc 调用的示例

附加 eBPF 程序时应该考虑的事情

这个讨论超级棒

在幻灯片里有很多非常好的链接,并且在 iovisor 仓库里有个 LINKS.md。虽然现在已经很晚了,但是我马上要去写我的第一个 eBPF 程序了!


via: https://jvns.ca/blog/2017/06/28/notes-on-bpf---ebpf/

作者:Julia Evans 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出



Previous Post
强制关闭你的系统的内核模块
Next Post
如何使用 Vim 编辑器编辑多个文件