第二十七章:邻居子系统:基础结构

主要的数据结构:

struct neighbour:存储邻居有关的信息,例如,L2和L3地址,NUD状态等。注意,一个neighbour项不是与一台主机关联,而是和一个L3地址关联。

struct neigh_table:描述一种邻居协议的参数和所用函数。每个邻居协议都有该结构的一个实例。

struct neigh_parms:对每个设备上邻居协议行为进行调整的一组参数。由于在大部分接口上可以启动多个协议如ipv4和ipv6,因此,一个net_device结构可以关联多个neigh_parms。

struct neigh_ops:一组函数,用来表示L3协议和dev_queue_xmit之间的接口。

struct hh_cache:缓存链路层头部信息用于加快传输速度。并不是所有的网络设备驱动都支持缓存头部信息。

struct rtable:

struct dst_entry:这两个结构和路由相关

in_device和inet6_dev:分别用于保存某个设备上的ipv4和ipv6配置信息。

struct pneigh_entry:用于基于目的地址的代理。

struct neigh_statistics:用于收集邻居协议的统计数据。

对图27-2的一些说明:

在图的中间,每个网络设备(net_device)都有两个指针,指向L3协议配置,分别是ipv4(in_device)和ipv6(inet6_dev)。而在in_device和inet6_dev中,又有指针指向各自的邻居层协议,即ARP和ND。每个协议所用的全部neigh_parms结构都链接到一个单向链表中,其根节点保存在neigh_table结构中。

在图的上部和底部显示每个协议有两个hash表。其中,hash_buckets用于缓存协议解析过对L3到L2的映射或静态配置。第二个是phash_bucket,保存着需要被代理的ip地址。每个pneigh_entry包含一个指向对应net_device结构的指针。

若网络设备支持缓存链路层头部信息,那么每个neighbour实例与一个多多个hh_cache关联。

L3协议和邻居协议间的通用接口:

Linux有个通用邻居层,通过一个虚拟函数表VFT,将L3协议和主要的L2传输函数连接起来。邻居子系统的VFT由neigh_ops结构实现。当创建一个邻居时,其neighbour->ops被初始化为合适的neigh_ops结构。

上图中,neigh->output实际上就是neigh->ops->output。

每个协议提供三种不同的neigh_ops VFT实例:

    通用表(xxx_generic_ops).用于处理那些需要对L2地址进行解析的邻居。

    当设备驱动提供它自己的一组函数处理L2帧头时,就会使用一组经过优化的函数,这些函数能加快缓存的帧头(xxx_hh_ops)的使用。

    当设备不需要对L3地址到L2地址的映射时,就要用到一个表(xxx_direct_ops)。

neigh->output和neigh->nud_state的初始化:

邻居的状态(neigh->nud_state)和neigh->output函数彼此互相依赖。当nud_state改变状态时,output通常也必须相应的更新。邻居子系统提供了一个通用函数neigh_update,它能将邻居状态改变为其输入参数中提供的状态。

邻居进入NUD_REACHABLE状态的主要方式是:

    收到一个solicitation应答

    L4认证:当收到一个L4可到达性确认后,就会第一次执行neigh_timer_handler函数,邻居状态就会变为NUD_REACHABLE。

    人工配置

无论什么时候进入NUD_REACHABLE态,邻居基础结构就调用neigh_connect函数,将neigh->output指向neigh_ops->connected_output。

当邻居由NUD_REACHABLE状态转移到NUD_STALE或NUD_DELAY,或初始化为与NUD_CONNECTED中的任一状态不同的状态时,内核会请求neigh_suspect执行可到达性确认。

neigh->output由邻居的constructor函数初始化,之后由neigh_connect和neigh_suspect根据协议事件的结果对其操作。neigh->output总是被设置成neigh_ops的一个虚函数。下面列出了一些函数,可以被指定为neigh_ops的虚函数:

    dev_queue_xmit:当要传输一个封包时,L3总是调用这个函数。当出口设备上传输所需的信息都准备好,且邻居子系统没有其他工作,邻居协议会将neigh_ops的函数指针设为dev_queue_xmit。

    neigh_connected_output:该函数只填充L2帧头,然后调用neigh_ops->queue_xmit。

    neigh_resolve_output:该函数在数据传输前将L3地址解析为L2地址。

    neigh_compat_output:该函数是为了保证向下兼容。在引入邻居基础结构之前,由它负责调用dev_queue_xmit函数。

    neigh_blackhole:用于处理neighbour结构不能删除的临时情况,该函数会丢弃输入接口上接收的任何封包,确保任何试图给邻居传送封包的行为不会发生。

    

邻居信息的更新,neigh_update函数:

原型:int neigh_update(struct neighbour *neigh , const u8 *lladdr,u8 new,u32 flags)

参数:

    neigh:要更新的neighbour结构

    lladdr:新的L2地址

    new:新的NUD状态

    flags:NEIGH_UPDATE_F_OVERRIDE(表示当前的L2地址可以被lladdr覆盖),更多标志可以参考include/net/neighbour.h

图27-5a和27-5b是neigh_update函数内部流程,流程图被分为不同区域,每个区域负责不同的任务:

    合理性检测

    当前状态不是NUD_VALID态的邻居发生的变化

    选择L2地址,供状态不是NUD_VALID态的邻居使用

    设置一个新的链路层地址

    改变NUD状态

    处理arp_queue队列

在一些网络中,管理arp请求的是使用arpd的用户空间守护进程,而不是让内核自己负责。

下面介绍一些常用概念:缓存,引用计数,定时器。

缓存:

邻居层实现了两种形式的缓存:

    邻居映射:缓存L3到L2的地址映射。

    L2帧头:邻居基础结构会缓存L2帧头,这样可以缩短L3封包到L2帧的封装时间。

定时器:

    状态转移的定时器(neighbour->timer):

    失败的solicitation请求的定时器:如果在一个给定时间内没有收到solicitation请求的应答,那么就再发送一个新的solicitation请求。发送的最大次数再neigh_parm结构中设定。

    垃圾回收定时器(neigh_table->gc_timer):这是一个周期性定时器,确保内存不会浪费在每用的数据结构上。

    Proxy定时器(neigh_table->proxy_timer):对一个接收到大量solicitation请求的代理来说,推迟请求的处理是有好处的。这个定时器负责延迟处理的时间。

引用计数:

许多与创建新邻居有关的内核子系统都会在一些数据结构中保存对neighbour结构的引用。neighbour结构中包含一个名为refcnt的引用计数器。分别使用neigh_hold函数和neigh_release函数对该计数器进行加一和减1操作。每个邻居定时器的每次启动都会导致计数器加1。当一个定时器由于某些原因需要被删除时,不是立刻释放所占内存,而是令neighbour->dead字段设为1,表示该邻居已停用。之后不久,垃圾回收定时器就会负责清理。

创建一个邻居项:

当下列事件发生时,就会创建一个邻居项:

    传输请求:当向一台L2地址未知的主机传输请求时,就需要对该地址进行解析。

    收到solicitation请求:收到该请求的主机会创建一个缓存项,保存发送方的L3地址到L2地址的映射。但是这种方式和用明确的solicitation请求和应答获得的信息不具有相同的权威性。

    手工添加:管理员可以通过ip neigh add命令创建一个邻居缓存项。

当上述事件发生时,且邻居子系统的缓存没有命中,邻居协议就会尝试解析地址并缓存起来。

创建邻居项neighbour结构使用neigh_create函数:

原型:struct neighbour *neigh_create(struct neigh_table *tbl,const void *pkey,struct net_device *dev)

参数:

    tbl:表示使用的邻居协议,如果调用者来自IPv4程序,就设为arp_tbl。

    pkey:表示L3地址,之所以称为pkey,是因为它在缓存中查找被用作关键字。

    dev:与要创建的邻居项相关的设备。因为每个neighbour与一个L3地址关联,而一个L3地址与一个设备关联。

邻居初始化:

neighbour结构有两种初始化方式,一种由邻居协议执行,一种由设备驱动执行。

协议执行的初始化靠调用neigh_table->constructor函数完成。设备执行的初始化工作由neigh_setup函数完成(neighbour->parms->neigh_setup)。只有少数设备定义了这个函数。

删除邻居:

删除邻居的原因主要有三个:

    内核企图向一个不可到达的主机送封包,此时,邻居子系统察觉到了传输不成功,把neighbour转移到NUD_FAILED状态,稍后垃圾回收机制会清理该结构。

    与该邻居结构关联的L2地址改变了。

    该邻居结构存在的时间太长,且内核需要它占用的内存。

执行删除的函数是neigh_destroy,该函数完成下列任务:

    停止所用未决的定时器。

    释放所有对外部结构数据的引用。例如关联的设备以及缓存的L2帧头。

    如果一个邻居协议提供了destructor方法,该邻居协议就会执行这个方发自己清理邻居项。

    如果arp_queue队列非空,就要将其清空。

    将表示主机使用的neighbour项总数减一。

    释放该neighbour结构。

垃圾回收:

邻居基础结构的垃圾回收算法由两部分组成:

    同步清理:当需要分配一个新的邻居项,但是相关的内存池使用完了,就需要立即执行同步清理。

    异步清理:异步清理周期性执行,用于删除某段时间内没有使用过的neighbour结构。

调整垃圾回收行为的参数有:

    neigh_table中的参数有:gc_interval,gc_thresh1,gc_thresh2,gc_thresh3,last_flush,gc_timer

    neigh_parms中的参数有:gc_staletime。

同步清理函数neigh_forced_gc:

如果没有内存分配新的neighbour实例,主机就无法向该邻居传输任何封包。neigh_alloc函数负责在邻居子系统中分配,同时会启动同步垃圾回收。neigh_alloc会检查gc_thresh2和gc_thresh3两个变量(gc_thresh1在写这本书时没有使用,不知道现在什么情况),当neighbour实例数大于gc_thresh3,neigh_alloc强制执行垃圾回收,如果介于gc_thresh2和gc_thresh3之间,且上次回收已经过了5秒,也要执垃圾回收。

异步清理函数neigh_periodic_timer:

每个协议都有一个gc_timer定时器,它会周期性到期。当它到期时,调用neigh_periodic_timer函数。

该函数每次只浏览表中的一个bucket,然后记住最后浏览的bucekt,以便下次到期时浏览下一个bucket。

每次可到达性认证通过后,就会更新neigh-confirmed时间戳,其表示neighbour结构最后一次使用的标记。neigh_periodic_time函数在需要的时候更新neigh->used(当confirmed > used时)。

    

担任代理:

邻居子系统中的命名约定:以p字母开头的函数表示proxy代理。

由代理负责的solicitation请求可以延迟处理,延迟时间可以配置。为了使用延迟,邻居子系统会创建一个保存入口solicitation请求的队列和一个定时器。定时器到期后,出队并处理solicitation请求。下面是处理延时相关的主要变量和虚函数:

    来自neigh_table结构(协议参数):

        proxy_queue:临时缓存solicitation请求的队列。当队列满时,丢弃新元素。

        proxy_timer:用于执行延时的定时器,默认的处理函数是neigh_proxy_process。

        proxy_redo:处理出列请求的函数。

    来自neigh_parms结构(设备参数):

        proxy_delay:用于启动定时器的可配置延时。若该值为0.立即处理请求,不要延迟,非零则添加到proxy_queue队列。

        proxy_len:临时存储队列的最大长度。

 

L2帧头缓存:

当向一个目的地发送一个封包后,驱动程序就将其L2帧头保存在hh_cache结构中(前提是驱动支持这么干)。下面是Ethernet设备定义的一些操作hh_cache结构的方法,这些方法在ether_setup函数中初始化(见第八章):

    hard_header:该方法按字段填充L2帧头。当设备驱动不支持帧头缓存时,或者当帧头还没有准备好,不在缓存中时,就会用到hard_header。

    hard_header_cache:该方法将一个L2帧头缓存在hh_cache结构中。

    header_cache_update:该函数更新一个存在的hh_cache结构。

    hard_header_parse:从一个缓存区中取出源L2地址,然后返回地址长度。

    rebuild_header:兼容2.2版本前的内核设备驱动。

邻居层产生和接收的事件:

当一个邻居被声明是不可到达时,就会将这个事件通知上层协议。当邻居项中L3地址,L2地址,接口设备针中任一一个发生了变化,改邻居项也就失效了。邻居子系统通过两个函数来得知这样的事件:

    neigh_ifdown:其他的内核子系统要调用函数,以通知邻居子系统有关设备和L3地址的变化。

    neigh_changeaddr:当一个本地设备的L2地址变化时,邻居协议调用该函数来更新协议的缓存。为了得到这些事件的通知,每个协议可以向内核注册。

邻居协议和L3传输函数的交互:

ipv4子系统的封包传输过程的结尾要调用ip_finish_output2函数,该函数将封包传到L2,本节介绍这个函数如何与邻居子系统互相影响。

排队:

入口请求包和请求的应答包被送到邻居协议处理函数,通常都是立即处理。但是,代理能够配置成排队等候,延迟处理这些请求。当入口封包被放进队列时,所有邻居协议都要执行一些特定的任务,这些任务包括往缓存中增加封包,当收到solicitation应答时刷新arp_queue及使用代理的proxy_queue。

当发送一个封包时,如果目的L3地址和L2地址之间的关联还没有建立,邻居协议将该封包临时插入到arp_queue队列,如果关联被及时建立,该封包会被发送出去,否则,丢弃该封包。下图,给出了当L2地址没有准备好时,IPv4封包如何进入ARP的arp_queue队列。

上图没有考虑代理:

图a:空缓存,处理步骤:

    L3提交要传送的封包请求。

    查询缓存,没有找到L3地址到L2弟子的映射。

    临时将封包插入到队列中。

    发出solicitation请求。

    收到solicitation应答。

    缓存中增加相应的邻居项。

    缓存中的等待的封包被发送出去。

图b:地址解析待定,处理步骤:

    L3提交要传送的封包请求。

    查询缓存。

    地址不在缓存中,但是内核已经开始了地址的解析工作,因此封包被临时插入到队列中,等待应答。

图c:完成地址解析,处理步骤:

    L3提交要传送的封包。

    查询缓存。

    缓存命中,封包立即被发送出去。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值