Linux内核工程导论——网络:Filter(LSF、BPF)

数据包过滤

LSF(Linux socket filter)起源于BPF(Berkeley Packet Filter),基础从架构一致,但使用更简单。其核心原理是对用户提供了一种SOCKET选项:SO_ATTACH_FILTER。允许用户在某个sokcet上添加一个自定义的filter,只有满足该filter指定条件的数据包才会上发到用户空间。因为sokket有很多种,你可以在各个维度的socket添加这种filter,如果添加在raw socket,就可以实现基于全部IP数据包的过滤(tcpdump就是这个原理),如果你想做一个http分析工具,就可以在基于80端口(或其他http监听端口)的socket添加filter。还有一种使用方式离线式的,使用libpcap抓包存储在本地,然后可以使用bpf代码对数据包进行离线分析,这对于实验新的规则和测试bpf程序非常有帮助。甚至更底层的用法,可以在内核模块中直接写eBPF(内核中的表达方法,后面介绍)程序,直接插入内核的执行流程。

echo 2 > /proc/sys/net/core/bpf_jit_enable

通过像这个写入0/1/2可以实现关闭、打开、调试日志等bpf模式。

在用户空间使用,最简单的办法是使用libpcap的引擎,由于bpf是一种汇编类型的语言,自己写难度比较高,所以libpcap提供了一些上层封装可以直接调用。然而libpcap并不能提供所有需求,比如bpf模块开发者的测试需求,还有高端的自定义bpf脚本的需求。这种情况下就需要自己编写bpf代码,然后使用内核tools/net/目录下的工具进行编译成bpf汇编代码,再使用socket接口传入这些代码即可。bpf引擎在内核中大部分以模块的形式提供,并且可以替换采用不同的引擎,常用的由netfilter自带的xt_bpf 、cls_bpf,

内核对bpf的完整支持是从3.9开始的,作为iptables的一部分存在,默认使用的是xt_bpf,用户端的库是libxt_bpf。iptables一开始对规则的管理方式是顺序的一条条的执行,这种执行方式难免在匹配数目多的时候带来性能瓶颈,添加了bpf支持后,灵活性大大提升。

其他的BPF程序

前面说的bpf程序是用来做包过滤的,那么bpf代码只能用来做包过滤吗?非也。内核的bpf支持是一种基础架构,只是一种中间代码的表达方式,是向用户空间提供一个向内核注入可执行代码的公共接口。只是目前的大部分应用是使用这个接口来做包过滤。其他的如seccomp BPF可以用来实现限制用户进程可使用的系统调用,cls_bpf可以用来将流量分类,PTP dissector/classifier(干啥的还不知道)等都是使用内核的eBPF语言架构来实现各自的目的,并不一定是包过滤功能。

用户空间bpf支持

工具:tcpdump、tools/net、cloudfare、seccomp BPF

用户空间bpf汇编架构分析

bpf中每一条汇编指令都是如下格式:

struct sock_filter {    /* Filter block */
    __u16   code;   /* Actual filter code */
    __u8    jt; /* Jump true */
    __u8    jf; /* Jump false */
    __u32   k;      /* Generic multiuse field */
};

一个列子:op:16, jt:8, jf:8, k:32

code是真实的汇编指令,jt是指令结果为true的跳转,jf是为false的跳转,k是指令的参数,根据指令不同不同。一个bpf程序编译后就是一个sock_filter的数组,而可以使用类似汇编的语法进行编程,然后使用内核提供的bpf_asm程序进行编译。

bpf在内核中实际上是一个虚拟机,有自己定义的虚拟寄存器组。和我们熟悉的java虚拟机的原理一致。这个虚拟机的设计是lsf的成功的所在。有3种寄存器:

  A           32位,所有加载指令的目的地址和所有指令运算结果的存储地址
  X           32位,二元指令计算A中参数的辅助寄存器(例如移位的位数,除法的除数)
  M[]         0-15共16个32位寄存器,可以自由使用

我们最常见的用法莫过于从数据包中取某个字的数据内来做判断。按照bpf的规定,我们可以使用偏移来指定数据包的任何位置,而很多协议很常用并且固定,例如端口和ip地址等,bpf就为我们提供了一些预定义的变量,只要使用这个变量就可以直接取值到对应的数据包位置。例如:

  len                                   skb->len
  proto                                 skb->protocol
  type                                  skb->pkt_type
  poff                                  Payload start offset
  ifidx                                 skb->dev->ifindex
  nla                                   Netlink attribute of type X with offset A
  nlan                                  Nested Netlink attribute of type X with offset A
  mark                                  skb->mark
  queue                                 skb->queue_mapping
  hatype                                skb->dev->type
  rxhash                                skb->hash
  cpu                                   raw_smp_processor_id()
  vlan_tci                              skb_vlan_tag_get(skb)
  vlan_avail                            skb_vlan_tag_present(skb)
  vlan_tpid                             skb->vlan_proto
  rand                                  prandom_u32()

更可贵的是这个列表还可以由用户自己去扩展。各种bpf引擎的具体实现还会定义各自的扩展。

内核的BPF支持

我们可以看到,用户端即使经过编译的bpf代码也只是内核的一个结构体数组,与具体的可执行的实际汇编代码还是有差距的。要想得到可以直接执行的二进制代码,还需要在内核中进行编译。首先是将用户提交来的结构体数组进行编译成eBPF代码。这个代码是内核层级的虚拟汇编代码,这套汇编代码不用用户自己写,而是用户需要完成的只是sock_filter结构体数组,后面的转换为eBPF代码是内核自己完成的。然后再将eBPF代码转变为可直接执行的二进制。在eBPF之前的内核表达方法叫classic BPF format,这在很多平台还在使用,这个代码就和用户空间使用的那种汇编是一样的,但是在X86架构,现在在内核态已经都切换到使用eBPF作为中间语言了。也就是说x86在用户空间使用的汇编和在内核空间使用的并不一样。但是内核在定义eBPF的时候已经尽量的复用bpf的编码,有的指令的编码和意义,如BPF_LD都是完全一样的。

所以可以看出,eBPF的野心绝不止于此, 作为一种内核中存在的平台中间语言,他希望将所有用户希望在内核中执行的代码逻辑编译成eBPF。

内核eBPF汇编架构分析

`

* R0 - return value from in-kernel function, and exit value for eBPF program

* R1 - R5 - arguments from eBPF program to in-kernel function

* R6 - R9 - callee saved registers that in-kernel function will preserve

* R10 - read-only frame pointer to access stack

`

为了配合更强大的功能,eBPF汇编架构使用的寄存器有所增加,上述的寄存器的存在,充分体现了函数调用的概念,而不再是加载处理的原始逻辑。有了函数调用的逻辑设置可以直接调用内核内部的函数(这是一个安全隐患,但是内部有规避机制)。不但如此,由于这种寄存器架构与x86等CPU的真实寄存器架构非常像,实际的实现正是实行了直接的寄存器映射,也就是说这些虚拟的寄存器实际上是使用的同功能的真实的寄存器,这无疑是对效率的极大提高。而且,在64位的计算机上这些计算机将会有64位的宽度,完美的发挥硬件能力。但是目前的64位支持还不太完善,但已经可用。

目前的内核实现,只可以在eBPF程序中调用预先定义好的内核函数,不可以调用其他的eBPF程序(因为eBPF本身不是函数的概念)。这看起来无关紧要,但是却是一个极大的能力,这就意味着你可以使用C语言来实现eBPF程序逻辑,eBPF只需要调用这个C函数就好了。

eBPF的数据交互:map

eBPF不但是程序,还可以访问外部的数据,重要的是这个外部的数据可以在用户空间管理。这个k-v格式的map数据体是通过在用户空间调用bpf系统调用创建、添加、删除等操作管理的。

eBPF的直接编程方法

除了在用户空间通过nettable和tcpdump来使用bpf,在内核中或者在其他通用的编程中可以直接使用C写eBPF代码,但是需要LLVM支持,例子

在用户空间通过使用bpf系统调用的BPF_PROG_LOAD方法,就可以发送eBPF的代码进内核,如此发送的代码不需要再做转换,因为其本身就是eBPF格式的。如果要在内核空间模块使用eBPF,可以直接使用对应的函数接口插入eBPF程序到sk_buff,提供强大的过滤能力。

用于内核TRACING

我们知道eBPF有map数据结构,有程序执行能力。那么这就是完美的跟踪框架。比如通过kprobe将一个eBPF程序插入IO代码,监控IO次数,然后通过map向用户空间汇报具体的值。用户端只需要每次使用bpf系统调用查看这个map就可以得到想要统计的内容了。那么为何要用eBPF,而不是直接使用kprobe的c代码本身呢?这就是eBPF的安全性,其机制设计使其永远不会crash掉内核,不会与正常的内核逻辑发生交叉影响。可以说,通过工具选择避免了可能发生的很多问题。更可贵的是eBPF是原生的支持tracepoint,这就为kprobe不稳定的情况提供了可用性。

业界对eBPF的使用

Brendan Gregg’s Blog描述了一个使用eBPF进行kprobe测试的例子。

ktap创造性的使用eBPF机制实现了内核模块的脚本化,使用ktap,你可以直接使用脚本编程,无需要编译内核模块,就可以实现内核代码的追踪和插入。这背后就是eBPF和内核的tracing子系统。

bpf subcommand to perf:华为也在为bpf添加perf脚本的支持能力。

可以看出来,eBPF起源于包过滤,但是目前在trace市场得到越来越广泛的应用。

意义和总结

也就是说目前使用传统的bpf语法和寄存器在用户空间写bpf代码,代码在内核中会被编译成eBPF代码,然后编译为二进制执行。传统的bpf语法和寄存器简单,更面向业务,类似于高层次的编程语言,而内核的eBPF语法和寄存器复杂,类似于真实的汇编代码。

那么为何内核要大费周章的实现如此一个引擎呢?因为轻量级、安全性和可移植性。由于是中间代码,可移植性不必说,但是使用内核模块调用内核的函数接口一般也是可移植的,所以这个并不是很重要的理由。eBPF代码在执行的过程中被严格的限制了禁止循环和安全审查,使得eBPF被严格的定位于提供过程式的执行语句块,甚至连函数都算不上,并且限制同时只有一个eBPF程序,最大不超过4096个指令。所以这就是其定位:轻量级、安全、不循环。

上面说了几个bpf的用途,但远不至于此。

http://www.tcpdump.org/papers/bpf-usenix93.pdf

http://lwn.net/Articles/498231/

https://www.kernel.org/doc/Documentation/networking/filter.txt

时间: 2024-12-27 01:35:35

Linux内核工程导论——网络:Filter(LSF、BPF)的相关文章

Linux内核工程导论——网络:Netfilter概览

简介 最早的内核包过滤机制是ipfwadm,后来是ipchains,再后来就是iptables/netfilter了.再往后,也就是现在是nftables.不过nftables与iptables还处于争雄阶段,谁能胜出目前还没有定论.但是他们都属于netfilter项目的子成员. 钩子 netfilter基于钩子,在内核网络协议栈的几个固定的位置由netfilter的钩子.我们知道数据包有两种流向,一种是给本机的:驱动接收-->路由表-->本机协议栈-->驱动发送.一种是要转发给别人的:

linux内核工程导论-网络:tcp拥塞控制

这篇文章本来是在tcp那篇里面的,但是那篇太长了,不专一.就完善了一下提取出来了. TCP拥塞控制 拥塞控制讨论的是很多个同时存在的tcp连接应该怎么规划自己的数据包发送和接收速度,以在彼此之间共享带宽,同时与其他实体的机器公平的竞争带宽,而不是自己全占. 拥塞控制的核心是AIMD(additive-increase/multiplicative-decrease ),线性增加乘性减少.为啥不用线性增加线性减少,或者是乘性增加乘性减少呢?这个有人专门研究过,只有AIMD可以收敛聚合使得链路公平.

Linux内核project导论——网络:Filter(LSF、BPF、eBPF)

概览 LSF(Linux socket filter)起源于BPF(Berkeley Packet Filter).基础从架构一致.但使用更简单.LSF内部的BPF最早是cBPF(classic).后来x86平台首先切换到eBPF(extended).但因为非常多上层应用程序仍然使用cBPF(tcpdump.iptables),而且eBPF还没有支持非常多平台,所以内核提供了从cBPF向eBPF转换的逻辑,而且eBPF在设计的时候也是沿用了非常多cBPF的指令编码. 可是在指令集合寄存器.还有架

Linux内核工程导论——基础架构

基础功能元素 workqueue linux下的工作队列时一种将工作推后执行的方式,其可以被睡眠.调度,与内核线程表现基本一致,但又比内核线程使用简单,一般用来处理任务内容比较动态的任务链.workqueue有个特点是自动的根据CPU不同生成不同数目的队列.每个workqueue都可以添加多个work(使用queue_work函数). 模块支持 模块概述 可访问地址空间,可使用资源, 模块参数 用户空间通过"echo-n ${value} > /sys/module/${modulenam

Linux内核工程导论——进程

进程 进程调度 概要 linux是个多进程的环境,不但用户空间可以有多个进程,而且内核内部也可以有内核进程.linux内核中线程与进程没有区别,因此叫线程和进程都是一样的.调度器调度的是CPU资源,按照特定的规则分配给特定的进程.然后占有CPU资源的资源去申请或使用硬件或资源.因此这里面涉及到的几个问题: 对于调度器来说: l  调度程序在运行时,如何确定哪一个程序将被调度来使用CPU资源? n  如何不让任何一个进程饥饿? n  如何更快的定位和响应交互式进程? l  单个CPU只有一个流水线

Linux内核工程导论——用户空间设备管理

用户空间设备管理 用户空间所能见到的所有设备都放在/dev目录下(当然,只是一个目录,是可以变化的),文件系统所在的分区被当成一个单独的设备也放在该目录下.以前的2.4版本的曾经出现过devfs,这个思路非常好,在内核态实现对磁盘设备的动态管理.可以做到当用户访问一个设备的设备的时候,devfs驱动才会去加载该设备的驱动.甚至每个节点的设备号都是动态获得的.但是该机制的作者不再维护他的代码,linux成员经过讨论,使用用户态的udev代替内核态的devfs,所以现在的devfs已经废弃了.用户态

Linux内核工程导论——内存管理(一)

Linux内存管理 概要 物理地址管理 很多小型操作系统,例如eCos,vxworks等嵌入式系统,程序中所采用的地址就是实际的物理地址.这里所说的物理地址是CPU所能见到的地址,至于这个地址如何映射到CPU的物理空间的,映射到哪里的,这取决于CPU的种类(例如mips或arm),一般是由硬件完成的.对于软件来说,启动时CPU就能看到一片物理地址.但是一般比嵌入式大一点的系统,刚启动时看到的已经映射到CPU空间的地址并不是全部的可用地址,需要用软件去想办法映射可用的物理存储资源到CPU地址空间.

Linux内核工程导论——内核为何使用C语言

C与C++的对比无数人说过,都说C效率高,但很多人做过实验如果C++不使用RTTI,C++的效率也不会低太多(25%左右).还有人说C++强大的STL,但是对效率讲究点的话那个真的不能用,具体我后面说.一般大部分人的心态是,学C++出身的,就经常吐槽linux的C代码乱的一塌糊涂,各种敏捷,面向对象原则,代码不如C++精简,连个STL或者boost都用不上,等软件工程相关问题都是被他们吐槽的重灾区,这些人肯定没有给内核贡献过代码.做嵌入式一直用C的,接触内核后的反应是:啊,内核真大,啊,内核真难

Linux内核工程导论——用户空间进程使用内核资源

本文大部分转载和组装,只是觉得这些知识应该放到一起比较好. 进程系统资源的使用原理 大部分进程通过glibc申请使用内存,但是glibc也是一个应用程序库,它最终也是要调用操作系统的内存管理接口来使用内存.大部分情况下,glibc对用户和操作系统是透明的,所以直接观察操作系统记录的进程对内存的使用情况有很大的帮助.但是glibc自己的实现也是有问题的,所以太特殊情况下追究进程的内存使用也要考虑glibc的因素.其他操作系统资源使用情况则可以直接通过proc文件系统查看. 进程所需要的系统资源种类