关注了就能看到更多这么棒的文章哦~
January 31, 2020
This article was contributed by Marta Rybczyńska
原文来自:https://lwn.net/Articles/810663/
随着网络接口速度越来越快,用来处理每个packet的CPU时间就越来越少。好在,许多工作都可以offload给硬件去做,包括packet filtering。不过,Linux kernel还需要许多工作才能利用上这个功能。上一篇文章里综述了基于硬件的packet filtering的工作原理,kernel里已经有了相关的支持。本文则总结了一下packet filtering在netfilter subsystem里面是如何offload出去的,以及系统管理员该如何利用这个功能。
offload功能是Pablo Neira Ayuso的patch加入的,首先出现在5.3 release里面,后续也在持续更新。这组patch set的主要目标是允许用一组典型的配置来把netfilter rule的一部分给offload出去,对这些offload rule处理了的packet就不会再执行kernel里通用的packet处理代码。目前还不能把所有rule都offload出去,因为这会需要底层硬件支持,netfilter代码也需要改动。这个应用场景以及细节在Neira的 2019 Linux Plumbers Conference的pdf里面有介绍(https://linuxplumbersconf.org/event/4/contributions/463/attachments/286/485/2019-plumbers-lisboa.pdf )
Background work
这组patch set对代码进行了重构,从而让netflter offload机制可以重用那些此前跟traffic-control (tc) subsystem紧密绑定的一些功能。重构工作利用了现有driver里的callback。有些模块此前只能在tc subsystem里面用,现在变得更通用了。
第一个新增的subsystem是“flow block"架构,由2017年引入,用来支持filtering rule的共享,以及优化ternary content-addressable memory (TCAM) 条目的使用。它可以支持两个或者多个网络接口上共享一组rule规则,这样就能减少rule offloading所占用的硬件资源。因为包含有多条物理接口的网卡通常会在这些接口上公用TCAM条目。因此在交换机(switch)上,这个优化可以让系统管理员定义一组公共的filtering rule,然后在多个网络接口上应用。对这组公共rule的修改会在所有相关的网络接口上生效。netfilter offload patch把原来tc subsystem专用的flow block扩展了应用范围,这样它就可以用在所有需要对packet filtering进行offload的子系统里面。
flow block的核心功能是一系列driver callback函数,会在硬件里面写好的rule发生改变的时候被调用。通常每个设备(常见的网卡都是这样)只有一个条目。对switch(交换机)来说,它里面的所有网络接口都公用一个callback。如果一个平台上两个网络接口共用相同的一组规则,flow-block的列表里面会包含2个callback(每个网络接口都有一个)。flow-block架构并不限制filtering rule的数量。
这个patch set里面另一个重要改动,是修改了网卡驱动的一个callback函数。callback函数位于struct net_device_ops之中。netfilter offload patch set重复利用了ndo_setup_tc() callback,本来是用来为tc subsystem来配置scheduler, classifier和action的,原型如下:
int (*ndo_setup_tc)(struct net_device *dev, enum tc_setup_type type,
void *type_data);
它的一个参数是网卡设备dev,还有需要应用的配置信息(由enum tc_setup_type定义),以及一个数据值。这个enum定义了不同的action类型。netfilter并没有定义自己特有的类型,因此它会利用flower classifier已经定义好的TC_SETUP_CLSFLOWER。后面这里可能会变,因为会有驱动开始同时支持tc和netfilter两种offloading。
最后,在2019年2月引入了flow-rule API(第6版的patch set里面有个很长的引言,建议一读)。它实现了flow-filtering rule的一种中间表达方式(intermediate representation,有些人可能更熟悉IR),可以把驱动相关的实现部分从子系统对它的调用给区分开来。具体来说,它实现了一套代码路径,供驱动程序调用,来支持ethtool或者flower classifier所配置的access-control-list offload。
在这个flow-rule API里面,每个flow_rule对象都代表了一个过滤条件。它包含这个rule的匹配条件(struct flow_match),以及相应的action (struct flow_action)。在netfilter代码里面,每个flow_rule都代表了一条被offload给硬件的rule,它会被保存在flow-block list里面。当netfilter把一条rule offload给硬件的时候,会对flow block里面的callback list进行遍历,逐个调用每条callback函数来传入rule,这样就可以让驱动程序来处理了。
Driver API changes
因为tc特有的这些代码被改得更加通用了,所以有一些type和定义都被重命名或者重新组织过了。新加了一个type,flow_block_command,定义了驱动的flow-block setup函数里要用的命令。它主要包括两个定义,分别是TC_BLOCK_BIND和TC_BLOCK_UNBIND,分别被改名为FLOW_BLOCK_BIND和FLOW_BLOCK_UNBIND。这样kernel就可以把flow block给绑定到某个网络接口上,或者解绑。同样的,flow_block_binder_type定义了offload的类型(输入的话就是ingress,输出的话就是egress),它的成员从TCF_BLOCK_BINDER_TYPE_*改名成FLOW_BLOCK_BINDER_TYPE_*
现有的驱动程序都简单的配置好tc offloading功能了。所以Neira就增加了一个helper function供所有人来用:
int flow_block_cb_setup_simple(struct flow_block_offload *f,
struct list_head *driver_block_list,
flow_setup_cb_t *cb, void *cb_ident,
void *cb_priv, bool ingress_only);
这里f是offload context,driver_block_list是对于这个特定驱动程序所用的flow block的列表,cb是驱动程序的ndo_setup_tc() callback函数,cb_ident则是context的编号(identification),cb_priv则是要传递给cb的context(大多数情况下cb_ident和cb_priv是相同的),ingress_only如果是true则代表要配置的offload是只支持ingress类型(表示接收),这也是直到5.4 kernel为止的所有驱动的缺省配置,在5.5里面,cxgb4 driver开始支持双向了。flow_block_cb_setup_simple()会为每个network device来注册一个callback,这样大多数驱动程序就够用了。
每个驱动都应该要保存一组flow block,也就是flow_block_cb_setup_simple()的driver_block_list参数。如果驱动需要多个callback的话,例如一个用在ingress,另一个用在egress rules,那么这个列表是必须的。
每个驱动实现的callback函数,类型是flow_setup_cb_t,定义如下:
typedef int flow_setup_cb_t(enum tc_setup_type type, void *type_data,
void *cb_priv);
驱动程序里面相应的实现函数会利用提供进来的配置信息来设置好硬件的过滤功能。参数type定义了要用哪个classifier,type_data则是这个classifer所需的数据(通常是一个指向flow_rule结构的指针),cb_priv是callback函数的私有数据。
如果flow_block_cb_setup_simple()的功能无法满足这个驱动程序的要求(比如这个驱动是一个交换机上所用),那么就需要用到那些直接分配flow block的API了。分配和释放flow block是用这两个helper函数:flow_block_cb_alloc()和flow_block_cb_free(),原型如下:
struct flow_block_cb *flow_block_cb_alloc(flow_setup_cb_t *cb,
void *cb_ident, void *cb_priv,
void (*release)(void *cb_priv));
void flow_block_cb_free(struct flow_block_cb *block_cb);
这些回调函数都是由驱动程序定义的,然后通过flow-block的操作方式来传递给netfilter。netfilter会维护一个跟每个给定的rule关联起来的回调函数列表。
每个flow block都包含一个列表,其中是所有的driver offload回调函数。驱动可以把他们自己加到这个列表里或者从中删除掉,需要使用flow_block_cb_add()和flow_block_cb_remove()函数:
void flow_block_cb_add(struct flow_block_cb *block_cb,
struct flow_block_offload *offload);
void flow_block_cb_remove(struct flow_block_cb *block_cb,
struct flow_block_offload *offload);
驱动程序可以用flow_block_cb_lookup()函数来查找指定的回调函数。
struct flow_block_cb *flow_block_cb_lookup(struct flow_block *block,
flow_setup_cb_t *cb, void *cb_ident);
这个函数会在block context里面的list中搜索flow-block回调函数。如果cb callback和cb_ident值对应上了,就把相关的flow-block callback structure返回出来。这个函数主要是供交换机驱动来检查一个callback函数是否早就已经注册上来了(再强调一遍,交换机的一个callback会应用在所有网络接口上)。在配置第一个网络接口的时候,如果看到flow_block_cb_lookup()返回NULL,那么就分配资源并注册这个callback函数。相应的,其他的网络接口在调用这个函数的时候则会返回一个非NULL的值,也就可以直接使用现有的这个callback函数了,只不过需要增加一下引用计数。在unregister一个callback的时候,如果其他的用户还在使用这个callback户数,flow_block_cb_lookup()也会返回非NULL值,这样驱动程序就仅仅减少一下引用计数就好。
操作flow-block引用计数的函数是flow_block_cb_incref()和flow_block_cb_decref()。定义如下:
void flow_block_cb_incref(struct flow_block_cb *block_cb);
unsigned int flow_block_cb_decref(struct flow_block_cb *block_cb);
flow_block_cb_decref返回的值是做完这个操作之后的引用计数的值。
还有一个函数flow_block_cb_priv(),允许驱动访问它的private data。原型如下,非常简单:
void *flow_block_cb_priv(struct flow_block_cb *block_cb);
最后,驱动程序还可以使用flow_block_is_busy()来检查这个callback函数是否已经在使用中了(就是加到列表里面并且是active状态)。这个函数的原型如下:
bool flow_block_cb_is_busy(flow_setup_cb_t *cb, void *cb_ident,
struct list_head *driver_block_list);
此函数会如果看到driver_block_list里面存在一个条目,cb和cb_ident都符合要求,那么就返回true。这个函数的主要用途是在配置offload的时候,避免同时配置tc和netfilter callback。针对那些同时支持tc和ntetfilter的硬件来说,只要实现好了这两种功能,它们的驱动程序里面移除对这个函数的调用。
traffic classifier内部的实现已经被改为使用flow-block API里面存放的filtering,这是通过利用新加的tcf_block_setup()函数来实现的。
Callback list
驱动程序会配置好flow-block对象(flow_block_cb),然后把驱动程序的callback函数加入它们的列表中。每个驱动程序都会把自己的列表传递给网络处理核心代码去,完成注册(包括tc和netfilter),并且调用驱动程序的callback来真正完成硬件的配置。这个回调函数会利用参数中传递进来的classifier特有的数据,包括操作类型(例如增加或者删除一个offload)。
Edward Cree问过,为什么每个驱动程序只有一个列表,而不是每个设备一个。他说:“Pablo,能否解释一下(commit message里面没有提到)为什么需要这些per-driver list,有哪些数据、状态是属于module范围的(而不是例如netdevice范围的)?”
Neira解释说,这些驱动程序目前只支持一个flow block,后续计划扩展为针对每个subsystem(例如ethtook, tc等等)使用一个flow block。这里有两个原因:首先,目前的驱动程序只能支持一个subsystem,等这个限制去掉之后,还有另一个限制,就是为了支持共享功能,所以所有subsystem都必须使用相同的配置。这就意味着,例如eth0和eth1就需要用相同的配置用于tc,同时也还需要一个共同的配置用在netfilter上。Neira觉得今后应该不会需要做成这样。
The netfilter offload itself
最后一个patch里面引入了netfilter里面进行硬件offload的功能。目前只有基本支持,仅仅处理了ingress chain。并且只有在flow的5个元素(协议,源、目标地址,源、目标端口号)完全匹配上的时候才会应用rule。
patch中举了下面的例子来说明:
table netdev filter {
chain ingress {
type filter hook ingress device eth0 priority 0; flags offload;
ip daddr 192.168.0.10 tcp dport 22 drop
}
}
这条规则会把发往192.168.0.10端口22(一般是ssh)的所有TCP包都丢弃。比起那些没有被offload的rule来说,主要的区别是增加了flags offload参数。
因为对offload进行控制的权限释放给了administrator,所以有可能出现配置错误的情况。比如说,如果对一条不可以做offload的规则加上了offload flag,那么就会返回错误代码EOPNOTSUPP。假如驱动程序无法处理这条命令,例如TCAM已经满了,那么就需要返回驱动程序自己决定的错误代码。
这个接口给系统管理员赋予了更多能力,也同时需要他们负起责任来,仔细挑选那些能获得尽量大好处的规则来offload出去。所以,需要非常了解系统配置以及系统上主要的网络数据都是什么,这样才能更好地利用这个新功能。在撰写这篇文章的时候,还没有benchmark数据,也没有一些通用的建议文档。此外我们还需要慢慢观察这个offload功能有那些局限性,例如是否容易能调查驱动程序callback里传入的用户配置错误。
Summary
netfilter classification offloading功能可以用来打开硬件offload功能,这样应该能够在某些应用场景下得到非常显著的性能提升。这个工作引出了对现有代码的重构工作,为其他offloading用户趟平了道路。不过,驱动程序还需要修改之后才能完全利用这个优势,并且这组API还是比较复杂的,有多种callback函数。系统管理员算是得到了一个强大的工具,不过需要仔细小心地使用好它。。今后,这部分肯定还会有许多工作需要做。
[The author would like to thank Pablo Neira Ayuso for helpful comments]
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~