主要的数据结构:
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提交要传送的封包。
查询缓存。
缓存命中,封包立即被发送出去。