Linux3.5内核对路由子系统的重构对Redirect路由以及neighbour子系统的影响

几年前,我记得写过好几篇关于Linux去除对路由cache支持的文章,路由cache的下课来源于一次对路由子系统的重构,具体原因就不再重复说了,本文将介绍这次重构对Redirect路由以及neighbour子系统的影响。

事实上,直到最近3个月我才发现这些影响是如此之大,工作细节不便详述,这里只是对关于开放源代码Linux内核协议栈的一些实现上的知识进行一个汇总,以便今后查阅,如果有谁也因此获益,则不胜荣幸。

路由项rtable,dst_entry与neighbour

IP协议栈中,IP发送由两部分组成:

IP路由的查找


想成功发送一个数据包,必须要有响应的路由,这部分是由IP协议规范的路由查找逻辑完成的,路由查找细节并不是本文的要点,对于Linux系统,最终的查
找结果是一个rtable结构体对象,表示一个路由项,其内嵌的第一个字段是一个dst_entry结构体,因此二者可以相互强制转换,其中重要的字段就
是:rt_gateway
  rt_gateway只是要想把数据包发往目的地,下一跳的IP地址,这是IP逐跳转发的核心。到此为止,IP路由查找就结束了。

IP neighbour的解析


IP路由查找阶段已经知道了rt_gateway,那么接下来就要往二层落实了,这就是IP
neighbour解析的工作,我们知道rt_gateway就是neighbour,现在需要将它解析成硬件地址。所谓的neighbour就是逻辑上
与本机直连的所有网卡设备,“逻辑上直连”意味着,对于以太网而言,整个以太网上所有的设备都可以是本机的邻居,关键看谁被选择为发送当前包的下一跳,而
对于POINTOPOINT设备而言,则其邻居只有唯一的一个,即对端设备,唯一意味着不需要解析硬件地址!值得注意的是,无视这个区别将会带来巨大的性
能损失,这个我将在本文的最后说明。

声明:

为了描述方便,以下将不再提起rtable,将路由查找结果一律用
dst_entry代替!下面的代码并不是实际上的Linux协议栈的代码,而是为了表述方便抽象而成的伪代码,因此dst_entry并不是内核中的
dst_entry结构体,而只是代表一个路由项!这么做的理由是,dst_entry表示的是与协议无关的部分,本文的内容也是与具体协议无关的,因此
在伪代码中不再使用协议相关的rtable结构体表示路由项。

Linux内核对路由子系统的重构

在Linux内核
3.5版本之前,路由子系统存在一个路由cache哈希表,它缓存了最近最经常使用的一些dst_entry(IPv4即rtable)路由项,对数据包
首先以其IP地址元组信息查找路由cache,如果命中便可以直接取出dst_entry,否则再去查找系统路由表。
  在3.5内核中,路由
cache不见了,具体缘由不是本文的重点,已有其它文章描述,路由cache的去除引起了对neighbour子系统的副作用,这个副作用被证明是有益
的,下面的很大的篇幅都花在这个方面,在详细描述重构对neighbour子系统的影响之前,再简单说说另一个变化,就是Redirect路由的实现的变
化。
  所谓的Redirect路由肯定是对本机已经存在的路由项的Redirect,然而在早期的内核中,都是在不同的位置比如
inet_peer中保存重定向路由,这意味着路由子系统与协议栈其它部分发生了耦合。在早期内核中,其实不管Redirect路由项存在于哪里,最终它
都要进入路由cache才能起作用,可是在路由cache完全没有了之后,Redirect路由保存的位置问题才暴露出来,为了“在路由子系统内部解决
Redirect路由问题”,重构后的内核在路由表中为每一个路由项保存了一个exception哈希表,一个路由项Fib_info类似于下面的样子:

Fib_info {
  Address nexhop;
  Hash_list exception;
};

这个exception表的表项类似下面的样子:

Exception_entry {
  Match_info info;
  Address new_nexthop;
};

这样的话,当收到Reidrect路由的时候,会初始化一个Exception_entry记录并且插入到相应的exception哈希
表,在查询路由的时候,比如说最终找到了一个Fib_info,在构建最终的dst_entry之前,要先用诸如源IP信息之类的Match_info去
查找exception哈希表,如果找到一个匹配的Exception_entry,则不再使用Fib_info中的nexhop构建
dst_entry,而是使用找到的Exception_entry中的new_nexthop来构建dst_entry。
    在对Redirect路由进行了简单的介绍之后,下面的篇幅将全部用于介绍路由与neighbour的关系。

重构对neighbour子系统的副作用

以下是网上摘录的关于在路由cache移除之后对neighbour的影响:
Neighbours
>Hold link-level nexthop information (for ARP, etc.)
>Routing cache pre-computed neighbours
>Remember: One “route” can refer to several nexthops
>Need to disconnect neighbours from route entries.
>Solution:
  Make neighbour lookups cheaper (faster hash, etc.)
  Compute neighbours at packet send time ...
  .. instead of using precomputed reference via route
>Most of work involved removing dependenies on old setup

事实上二者不该有关联的,路由子系统和neighbour子系统是两个处在上下不同层次的子系统,合理的方式是通过路由项的nexthop值来承上启下,通过一个唯一的neighbour查找接口关联即可:

dst_entry = 路由表查找(或者路由cache查找,通过skb的destination作键值)
nexthop = dst_entry.nexthop
neigh = neighbour表查找(通过nexthop作为键值)

然而Linux协议栈的实现却远远比这更复杂,这一切还得从3.5内核重构前开始说起。

重构前


重构前,由于存在路由cache,凡是在cache中可以找到dst_entry的skb,便不用再查找路由表,路由cache存在的假设是,对于绝大多
数的skb,都不需要查找路由表,理想情况下,都可以在路由cache中命中。对于neighbour而言,显而易见的做法是将neighbour和
dst_entry做绑定,在cache中找到了dst_entry,也就一起找到了neighbour。也就是说,路由cache不仅仅缓存
dst_entry,还缓存neighbour。
  事实上在3.5内核前,dst_entry结构体中有一个字段就是neighbour,表示与该路由项绑定的neighour,从路由cache中找到路由项后,直接取出neighbour就可以直接调用其output回调函数了。
  我们可以推导出dst_entry与neighbour的绑定时期,那就是查找路由表之后,即在路由cache未命中时,进而查找路由表完成后,将结果插入到路由cache之前,执行一个neighbour绑定的逻辑。
  和路由cache一样,neighbour子系统也维护着一张neighbour表,并执行着替换,更新,过期等状态操作,这个neighbour表和路由cache表之间存在着巨大的耦合,在描述这些耦合前,我们先看一下整体的逻辑:

func ip_output(skb):
        dst_entry = lookup_from_cache(skb.destination);
        if dst_entry == NULL
        then
                dst_entry = lookup_fib(skb.destination);
                nexthop = dst_entry.gateway?:skb.destination;
                neigh = lookup(neighbour_table, nexthop);
                if neigh == NULL
                then
                        neigh = create(neighbour_table, nexthop);
                        neighbour_add_timer(neigh);
                end
                dst_entry.neighbour = neigh;
                insert_into_route_cache(dst_entry);
        end
        neigh = dst_entry.neighbour;
        neigh.output(neigh, skb);
endfunc
---->TO Layer2

试看以下几个问题:
如果neighbour定时器执行时,某个neighbour过期了,可以删除吗?
如果路由cache定时器执行时,某条路由cache过期了,可以删除吗?

如果可以精确回答上述两个问题,便对路由子系统和neighbour子系统之间的关系足够了解了。我们先看第一个问题。
 
 如果删除了neighbour,由于此时与该neighbour绑定的路由cache项可能还在,那么在后续的skb匹配到该路由cache项时,便无
法取出和使用neighbour,由于dst_entry和neighbour的绑定仅仅发生在路由cache未命中的时候,此时无法执行重新绑定,事实
上,由于路由项和neighbour是一个多对一的关系,因此neighbour中无法反向引用路由cache项,通过
dst_entry.neighbour引用的一个删除后的neighbour就是一个野指针从而引发oops最终内核panic。因此,显而易见的答案
就是即便neighbour过期了,也不能删除,只能标记为无效,这个通过引用计数可以做到。现在看第二个问题。
  路由cache过期了,可以
删除,但是要记得递减与该路由cache项绑定的neighbour的引用计数,如果它为0,把neighbour删除,这个neighbour就是第一
个问题中在neighbour过期时无法删除的那类neighbour。由此我们可以看到,路由cache和neighbour之间的耦合关系导致与一个
dst_entry绑定的neighbour的过期删除操作只能从路由cache项发起,除非一个neighbour没有同任何一个dst_entry绑
定。现修改整体的发送逻辑如下:

func ip_output(skb):
        dst_entry = lookup_from_cache(skb.destination);
        if dst_entry == NULL
        then
                dst_entry = lookup_fib(skb.destination);
                nexthop = dst_entry.gateway?:skb.destination;
                neigh = lookup(neighbour_table, nexthop);
                if neigh == NULL
                then
                        neigh = create(neighbour_table, nexthop);
                        neighbour_add_timer(neigh);
                end
                inc(neigh.refcnt);
                dst_entry.neighbour = neigh;
                insert_into_route_cache(dst_entry);
        end
        neigh = dst_entry.neighbour;
        # 如果是INVALID状态的neigh,需要在output回调中处理
        neigh.output(neigh, skb);
endfunc
   
func neighbour_add_timer(neigh):
        inc(neigh.refcnt);
        neigh.timer.func = neighbour_timeout;
        timer_start(neigh.timer);
endfunc

func neighbour_timeout(neigh):
        cnt = dec(neigh.refcnt);
        if cnt == 0
        then
                free_neigh(neigh);
        else
                neigh.status = INVALID;
        end
endfunc

func dst_entry_timeout(dst_entry):
        neigh = dst_entry.neighbour;
        cnt = dec(neigh.refcnt);
        if cnt == 0
        then
                free_neigh(neigh);
        end
        free_dst(dst_entry);
endfunc

我们最后看看这会带来什么问题。
  如果neighbour表的gc参数和路由cache表的gc参数不同步,比如
neighbour过快到期,而路由cache项到期的很慢,则会有很多的neighbour无法删除,造成neighbour表爆满,因此在这种情况
下,需要强制回收路由cache,这是neighbour子系统反馈到路由子系统的一个耦合,这一切简直太乱了:

func create(neighbour_table, nexthop):
retry:
        neigh = alloc_neigh(nexthop);
        if neigh == NULL or neighbour_table.num > MAX
        then
                shrink_route_cache();
                retry;
        end
endfunc

关于路由cache的gc定时器与neighbour子系统的关系,有一篇写得很好的关于路由cache的文章《Tuning Linux IPv4 route cache》 如下所述:
You may find documentation about those obsolete sysctl values:
net.ipv4.route.secret_interval has been removed in Linux 2.6.35; it was used to trigger an asynchronous flush at fixed interval to avoid to fill the cache.
net.ipv4.route.gc_interval
has been removed in Linux 2.6.38. It is still present until Linux 3.2
but has no effect. It was used to trigger an asynchronous cleanup of the
route cache. The garbage collector is now considered efficient enough
for the job.
UPDATED: net.ipv4.route.gc_interval
is back for Linux 3.2. It is still needed to avoid exhausting the
neighbour cache because it allows to cleanup the cache periodically and
not only above a given threshold. Keep it to its default value of 60.

这一切在3.5内核之后发生了改变!!

重构后


过了重构,3.5以及此后的内核去除了对路由cache的支持,也就是说针对每一个数据包都要去查询路由表(暂不考虑在socket缓存
dst_entry的情形),不存在路由cache也就意味着不需要处理cache的过期和替换问题,整个路由子系统成了一个完全无状态的系统,因
此,dst_entry再也无需和neighbour绑定了,既然每次都要重新查找路由表开销也不大,每次查找少得多的neighbour表的开销更是可
以忽略(虽然查表开销无法避免),因此dst_entry去除了neighbour字段,IP发送逻辑如下:

func ip_output(skb):
        dst_entry = lookup_fib(skb.destination);
        nexthop = dst_entry.gateway?:skb.destination;
        neigh = lookup(neighbour_table, nexthop);
        if neigh == NULL
        then    
                neigh = create(neighbour_table, nexthop);
        end
        neigh.output(skb);
endfunc

路由项不再和neighbour关联,因此neighbour表就可以独立执行过期操作了,neighbour表由于路由cache的gc过慢而导致频繁爆满的情况也就消失了。
  不光如此,代码看上去也清爽了很多。

一个细节:关于POINTOPOINT和LOOPBACK设备的neighbour


很多讲述Linux
neighbour子系统的资料,但是几乎无一例外都是在说ARP的,各种复杂的ARP协议操作,队列操作,状态机等,但是几乎没有描述ARP之外的关于
neighbour的资料,因此本文在最后这个小节中准备补充关于这方面的一个例子。还是从问题开始:
一个NOARP的设备,比如POINTOPOINT设备发出的skb,其neighbour是谁?

广播式以太网情况下,要发数据包到远端,需要解析“下一跳”地址,即每一个发出的数据包都要经由一个gateway发出去,这个gateway被抽象为一
个同网段的IP地址,因此需要用ARP协议落实到确定的硬件地址。但是对于pointopoint设备而言,与该设备对连的只有固定的一个,它并没有一个
广播或者多播的二层,因此也就没有gateway的概念了,或者换句话说,其下一跳就是目标IP地址本身。
  根据上述的ip_output函数
来看,在查找neighbour表之前,使用的键值是nexthop,对于pointopoint设备而言,nexthop就是skb的目标地址本身,如
果找不到将会以此为键值进行创建,那么试想使用pointopint设备发送的skb的目标地址空间十分海量的情况,将会有海量的neighbour在同
一时间被创建,这些neighbour将会同时插入到neighbour表中,而这必然要遭遇到锁的问题,事实上,它们的插入操作将全部自旋在
neighbour表读写锁的写锁上!!
  neigh_create的逻辑如下:

struct neighbour *neigh_create(struct neigh_table *tbl, const void *pkey,
                   struct net_device *dev)
{
    struct neighbour *n1, *rc, *n = neigh_alloc(tbl);
  ......
    write_lock_bh(&tbl->lock);
  // 插入hash表
    write_unlock_bh(&tbl->lock);
    .......
}

在海量目标IP的skb通过pointopoint设备发送的时候,这是一个完全避不开的瓶颈!然而内核没有这么傻。它采用了以下的方式进行了规避:

__be32 nexthop = ((struct rtable *)dst)->rt_gateway?:ip_hdr(skb)->daddr;
if (dev->flags&(IFF_LOOPBACK|IFF_POINTOPOINT))
  nexthop = 0;

这就意味着只要发送的pointopint设备相同,且伪二层(比如IPGRE的情况)信息相同,所有的skb
将使用同一个neighbour,不管它们的目标地址是否相同。在IPIP
Tunnel的情形下,由于这种设备没有任何的二层信息,这更是意味着所有的通过IPIP
Tunnel设备的skb将使用一个单一的neighbour,即便是使用不同的IPIP Tunnel设备进行发送。
但是在3.5内核重构之后,悲剧了!
  我们直接看4.4的内核吧!

static inline __be32 rt_nexthop(const struct rtable *rt, __be32 daddr)
{
    if (rt->rt_gateway)
        return rt->rt_gateway;
    return daddr;
}
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
  ......
    nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
    neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
    if (unlikely(!neigh))
        neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
    if (!IS_ERR(neigh)) {
        int res = dst_neigh_output(dst, neigh, skb);
        return res;
    }
  ......
}

可以看到,dev->flags&(IFF_LOOPBACK|IFF_POINTOPOINT)这个判断消失了!这意味着内核变傻了。上一段中分析的那种现象在3.5之后的内核中将会发生,事实上也一定会发生。
  遭遇这个问题后,在没有详细看3.5之前的内核实现之前,我的想法是初始化一个全局的dummy neighbour,它就是简单的使用dev_queue_xmit进行direct out:

static const struct neigh_ops dummy_direct_ops = {
    .family =        AF_INET,
    .output =        neigh_direct_output,
    .connected_output =    neigh_direct_output,
};
struct neighbour dummy_neigh;
void dummy_neigh_init()
{
    memset(&dummy_neigh, 0, sizeof(dummy_neigh));
    dummy_neigh.nud_state = NUD_NOARP;
    dummy_neigh.ops = &dummy_direct_ops;
    dummy_neigh.output = neigh_direct_output;
    dummy_neigh.hh.hh_len = 0;
}

static inline int ip_finish_output2(struct sk_buff *skb)
 {
  ......
     nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
    if (dev->type == ARPHRD_TUNNEL) {
        neigh = &dummy_neigh;
    } else {
        neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
    }
     if (unlikely(!neigh))
         neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
  ......
 }

后来看了3.5内核之前的实现,发现了:

if (dev->flags&(IFF_LOOPBACK|IFF_POINTOPOINT))
  nexthop = 0;

于是决定采用这个,代码更少也更优雅!然后就产生了下面的patch:

diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c
--- a/net/ipv4/ip_output.c
+++ b/net/ipv4/ip_output.c
@@ -202,6 +202,8 @@ static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *s

        rcu_read_lock_bh();
        nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
+       if (dev->flags & (IFF_LOOPBACK | IFF_POINTOPOINT))
+               nexthop = 0;
        neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
        if (unlikely(!neigh))
                neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
时间: 2024-10-13 00:59:04

Linux3.5内核对路由子系统的重构对Redirect路由以及neighbour子系统的影响的相关文章

Linux3.5内核以后的路由下一跳缓存

在Linux3.5版本号(包括)之前.存在一个路由cache.这个路由cache的初衷是美好的,可是现实往往是令人遗憾的.下面是陈列得出的两个问题:1.面临针对hash算法的ddos问题(描写叙述该问题的文章已经汗牛充栋,不再赘述):2.缓存出口设备是p2p设备的路由项会降低性能.这些问题本质上是由于路由cache的查找方式和路由表的查找方式互不相容引起的.路由cache必须是精确的元组匹配,因此它必须设计成一维的hash表,而路由表查找算法是最前前缀匹配.因此它能够是多维的. 路由查找终于会找

深入理解Linux网络技术内幕——路由子系统的概念与高级路由

本文讨论IPv4的路由子系统.(IPv6对路由的处理不同). 基本概念 路由子系统工作在三层,用来转发入口流量. 路由子系统主要设计 路由器.路由.路由表等概念. 路由器: 配备多个网络接口卡(NIC),并且能利用自身网络信息进行入口流量转发的设备. 路由: 流量转发,决定目的地的过程 路由表:转发信息库,该库中储存路由需要本地接收还是转发的信息, 以及转发流量时所需要的信息.(即,信息库用来判断,要不要转发,如果要转发,向哪里转发). 我们了解,路由器有多个网卡,但是多个NIC的设备不一定就是

Linux3.4内核的基本配置和编译

转载自:http://www.embedu.org/Column/Column634.htm 作者:李昕,华清远见研发中心讲师. 了解Linux3.4内核的特性及新增功能,掌握Linux内核的编译过程及Linux内核配置选项的内容. [实验环境] ●主机:Ubuntu 10.10 (64bit):                ●目标机:FS_S5PC100平台:                ●交叉编译工具链:arm-eabi-4.4.0(Android4.0.4自带交叉工具链): [实验步骤

【FL2400】Linux3.0 内核移植 一

接触arm + Linux已经将近两年了,之前都是站在大神的肩膀上来移植linux内核,对很对要求移植的东西都不是很懂!为了进一步深入对内核的了解,我决定重新从头开始对linux内核进行移植.这次移植完全是从一个新手的角度进行移植,包括可能出现的问题,以及出现的问题如何解决. 环境: 操作系统: CentOS 6.2 编译环境:gcc version 4.3.6 (Buildroot 2011.11) 开发板    : 飞凌2440(s3c2440)(arm920t) u-boot    :u-

基于OMAPL:Linux3.3内核的编译

基于OMAPL:Linux3.3内核的编译 OMAPL对应3个版本的linux源代码,分别是:Linux-3.3.Linux-2.6.37.Linux2.6.33,这里的差距在于Linux2,缺少SYSLINK支持组件. 这里我们选择Linux-3.3版本进行开发. 开发前准备 mkimage的工具:sudo apt-get install u-boot-tools menuconfig组件库安装:apt-get install libncurses5-dev 正确配置arm-none-linu

关于ICMP Redirect路由的一个不是bug的bug

在新公司碰到的第一个网络问题竟然是关于重定向路由的,这个不常被关注的问题竟然花费了我整整一下午时间来整理,本文介绍Linux协议栈是如何对待重定向路由的. 路由项的生成方式 任何具备网络功能的设备在内部都有有一张路由表,该表指示数据包如何从该设备发出以及下一站发送到哪里.路由表由一项一项组成,每一项称为一条路由项,这些路由项有以下的生成方式: 1.自动发现的路由项 网卡启动并且配置上IP地址的时候,会自动生成与该IP地址以及前缀相匹配的链路层路由,由于是链路可达的,所有不需要nexthop,所有

AngularJS路由系列(5)-- UI-Router的路由约束、Resolve属性、路由附加数据、路由进入退出事件

本系列探寻AngularJS的路由机制,在WebStorm下开发.主要包括: ● UI-Router约束路由参数● UI-Router的Resolve属性● UI-Router给路由附加数据● UI-Router的onEnter和onExit事件 AngularJS路由系列包括: 1.AngularJS路由系列(1)--基本路由配置2.AngularJS路由系列(2)--刷新.查看路由,路由事件和URL格式,获取路由参数,路由的Resolve3.AngularJS路由系列(3)-- UI-Rou

AngularJS路由系列(2)--刷新、查看路由,路由事件和URL格式,获取路由参数,路由的Resolve

本系列探寻AngularJS的路由机制,在WebStorm下开发.主要包括: ● 刷新路由● 查看当前路由以及所有路由● 路由触发事件● 获取路由参数 ● 路由的resolve属性● 路由URL格式 项目文件结构 node_modules/ public/.....app/..........bower_components/...............toastr/....................toastr.min.css....................toastr.min

路由基础(二)静态路由

1.静态路由 静态路由是由网络管理员手动进行配置,当网络拓扑产生变化,这时候需要手动配置. 1.1静态路由 静态路由的分为:静态路由.默认路由.指向null0的静态路由. 1.1.1静态路由是什么? 在现网中很多时候使用静态路由觉的简单才使用,或者是小型的网络才使用,因为静态路由简单,没有太复杂的选路原则. 事实上静态路由一些复杂的网络中使用也非常普遍,很多时候内网和外网对接的时候都会使用静态路由进行控制. 静态路由怎么配置? 首先要写网段=网络地址+掩码组成. 然后跟上下一跳的接口或IP地址.