6. 网桥数据转发
6.1 网桥数据包入口
网桥是一种2层网络互连设备,而不是一种网络协议。它在协议结构上并没有占有一席之地,因此不能通过向协议栈注册协议的方式来申请网桥数据包的处理。相 反,网桥接口(如上述的eth1)的数据包和一般接口(如eth0)在格式上完全是一样的,不同之处是网桥在2层上就对它进行了转了,而一般接口要在3层 才能根据路由信息来决定是否要转发,如何转发。那么一个网络接口,在驱动处理完数据包后,怎么才知道该接口分配在一个网桥里面呢?其实很简单,当 brctl工具通过ioctl系统调用时,kernel为该添加的设备生成一个bridge_port结构并放到port_list链中,同时将该 bridge_port的值赋予设备net_device的br_port指针。因此,要识别接口是否属于某个网桥,只需判断net_device的 br_port指针是否不为空即可。
现假设PC1向PC2发送其个数据包,数据首先会由eth1网卡接收,此后网卡向CPU发送接收中断。当CPU执行当前指令后(如果开中断的话),马上跳 到网卡的驱动程去。Eth1的网卡驱动首先生成一个skb结构,然后对以太网层进行分析,最后驱动将该skb结构放到当前CPU的输入队列中,唤醒软中断。如果没有其它中断的到来,那么软中断将调用netif_receive_skb函数。代码和分析如下所述:
int netif_receive_skb(struct sk_buff *skb) {
//当网络设备收到网络数据包时,最终会在软件中断环境里调用此函数
//检查该数据包是否有packet socket来接收该包,如果有则往该socket
//拷贝一份,由deliver_skb来完成。
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
// 先试着将该数据包让网桥函数来处理,如果该数据包的入口接口确实是网桥接口,
// 则按网桥方式来处理,并且handle_bridge返回NULL,表示网桥已处理了。
// 如果不是网桥接口的数据包,则不应该让网桥来处理,handle_bridge返回skb,
// 后面代码会让协议栈来处理上层协议。
skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);
if (!skb)
goto out;
skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);
if (!skb)
goto out;
//对该数据包转达到它L3协议的处理函数
type = skb->protocol;
list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
if (ptype->type == type &&
(!ptype->dev || ptype->dev == skb->dev)) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
}
【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
6.2 网桥处理逻辑
static inline struct sk_buff *handle_bridge(struct sk_buff *skb,
struct packet_type **pt_prev, int *ret,
struct net_device *orig_dev) {
struct net_bridge_port *port;
//如果该数据包产生于本机,而目标同时为本机。
if (skb->pkt_type == PACKET_LOOPBACK ||
//如果该数据包的输入接口不是网桥接口
(port = rcu_dereference(skb->dev->br_port)) == NULL)
// 以上两种情况都需要让上层协议进行处理
return skb;
if (*pt_prev) {
*ret = deliver_skb(skb, *pt_prev, orig_dev);
*pt_prev = NULL;
}
//数据包的入口接口是网桥接口。下面将按网桥逻辑进行处理。
//如假包换,数据包转达到真正的网桥处理函数
//br_handle_frame_hook在网桥模块的init函数被初始化为
//br_handle_frame
return br_handle_frame_hook(port, skb);
}
这里回调了br_handle_frame_hook()函数,这个是一个钩子函数。Br_handle_frame_hook()函数在Linux2.6.34\net\bridge\Br_input.c中,br_handle_frame_hook=br_handle_frame,所以实际函数为br_handle_frame.
6.3 br_handle_frame
好,现在我们看看bridge端口的处理函数 br_handle_frame如何处理skb和指示后续操作。该函数位于br_input.c中。
if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
goto drop;
网桥端口不打算处理回环数据;源地址必须为合法Ethernet地址:源MAC地址不能是全0,不能是MAC广播和多播,是的话就丢弃。
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb)
return RX_HANDLER_CONSUMED;
如果skb是共享的,考虑的网桥端口会修改skb,将它clone一份。
接下来数据被分为两类:目的地址是Link Local MAC层多播的数据包括了STP的BPDU,和普通数据。
STP帧(BPDU)和其他保留多播帧
首先是Link Local MAC多播的处理。802.1D有组保留的 Link Local 多播MAC地址,他们用于控制协议,如STP。如果接收到了STP但网桥没有开STP协议,就视为普通数据处理;换句话说,就是本网桥当作自己是不认识STP的网桥,例如Hub或不支持STP的Switch。这时需要Flood STP报文到其他端口,而保证那些支持STP网桥则看不到不支持STP设备的存在。对于其他Kernel不支持的管理帧处理方式类似。
最后能够在此函数直接递交到Local Host的只能STP功能打开情况下收到的STP帧。递交的时候经过Netfilter的NF_BR_LOCAL_IN的 HOOK点,然后是br_handle_local_finish。br_handle_local_finish的处理实际上不如说是“不处理”,它只是在端口处于Learning的情况下学习个skb的源MAC,并且总是返回0指示包 RX_HANDLER_PASS,由netif_receive_skb继续根据ptype_base处理(STP报文)。所有这段代码对于STP的处理也只是学了个源MAC,然后继续有netif_receive_skb处理。并没有处理STP帧(BPDU).
if (unlikely(is_link_local_ether_addr(dest))) { // MAC Link Local地址通常是管理帧
... ...
switch (dest[5]) {
case 0x00: /* Bridge Group Address */// 看看STP要怎么弄法,如果真要处理的话不是在这,而是稍后的protocol dipatching(ptype_base)的地方
/* If STP is turned off,
then must forward to keep loop detection */
if (p->br->stp_enabled == BR_NO_STP)
goto forward;// 没开STP,那STP帧就和普通数据帧一样处理
break;
case 0x01: /* IEEE MAC (Pause) */
goto drop; // MAC Control帧不能通过网桥
default:// 其他的保留MAC多播和普通数据帧一样处理
/* Allow selective forwarding for most other protocols */
if (p->br->group_fwd_mask & (1u << dest[5]))
goto forward;
}
//如果能到达这,只有一种情况:STP功能打开的情况下,收到了STP帧
/* Deliver packet to local host only */
if (NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
NULL, br_handle_local_finish)) { // br_hanle_local_finishq其实只是在Learning状态下学习MAC并返回0
return RX_HANDLER_CONSUMED; /* consumed by filter */
} else { // 通常,NF_HOOK(br_handle_local_finish)返回0,于是STB BPDU到此处“pass”,最后由netif_receive_skb根据ptype_base分发到STP协议层。
*pskb = skb;
return RX_HANDLER_PASS; /* continue processing */
}
}
记住,这个函数不会进行STP BPDU的处理!
普通数据帧
走到这里的帧要么是普通数据帧,要么是被视为普通数据的控制帧。它们的处理都是一样的,就是当作普通数据处理。 普通数据帧(非STP帧BPDU), 没有打开STP功能情况下的STP帧,那么就和普通帧一样处理 要么就是其他的保留多播(非MAC Control),那么就和普通帧一样处理
如果目的MAC和网桥设备(而不是网桥端口)的MAC相同,标记为
PACKET_HOST skb->pkt_type = PACKET_HOST;
br_handle_frame分析完了,我们留下两个问题, STP为什么在该函数中没有被处理,并且还去向了ptype_base的流程。 br_handle_frame_finish是做什么的 第一个问题其实好理解,STP作为一种特殊类型的Ethernet packet type,注册了自己的packet_type{}。在br_handler_frame的STP处理只是分流一下不该处理的情况(netif_receive_skb的流程做不到这种分流)。正经的STP处理的方法是在稍后查询ptype_base,找到相应的处理函数。
// net/llc/llc_core.c
static struct packet_type llc_packet_type __read_mostly = { .type = cpu_to_be16(ETH_P_802_2), .func = llc_rcv, };
反观普通数据流量,普通NIC收到这些数据时应递交到协议栈,即查询ptype_base然后递交。但设备一旦作为网桥端口,就不能这么处理了,可能需要转发的其他端口什么的,所以才要走br_handler_frame及后续函数。我们看看第二个问题,br_handle_frame_finish接下来是怎么处理普通数据流量(或当作普通数据处理的保留多播流量)的 。
6.4 数据帧处理:br_handle_frame_finish
接着br_handle_frame讨论数据帧的处理,这里的数据帧代表非(STP等)控制帧,当然也包括“视为数据帧”的控制帧(例如STP功能关闭的情况下,BPDU就视为普通数据帧处理)。后面就不再罗嗦了,统一称为“数据帧”或“数据流量”。
如果数据来自 br_handle_frame,那么 br_handle_frame_finish被调用的时候端口只能处于两种状态:Learning和Frowarding。STP端口如果处于Learning和Forwarding状态,就需要学习新的源MAC(更新FDB)br_fdb_update(br, p, eth_hdr(skb)->h_source);
如果端口是Learning就说明不是Forwarding,学个MAC就行了,不能继续接收数据。
if (p->state == BR_STATE_LEARNING)
goto drop;
接下来是链路层广播、多播和单播的处理,这段代码出现两个skb指针:skb2和原来的skb。理解这段代码,只需要时刻明白,skb2代表递交本地host, skb代表需要转发。抓住这个关键即可。
// skb2指针表明,有数据要发往本机的网络接口,即p->br->dev接口。
skb2 = NULL;
// 如果应用程序要dump本机接口的数据,那么该数据包应往主机发一份,
// 一个明显的例子就是在用户在运行tcpdump –I br0或类似的程序。
if (br->dev->flags & IFF_PROMISC)
skb2 = skb;
dst = NULL;
if (is_multicast_ether_addr(dest)) {
//如果该报文是一个L2多播报文(如arp请求),那么它应该转发到
//该网桥的所有接口。
//这同样是网桥的一个特点,广播和组播报文要转发到它的所有接口。
br->dev->stats.multicast++;
skb2 = skb;
} else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {
//__br_fdb_get函数先查MAC-端口映射表,这一步是网桥的关键。
// 这个报文应从哪个接口转发出去就看它了。
//如果这个报文应发往本机,那么skb置空。不需要再转发了,
skb2 = skb;
/* Do not forward the packet since it's local. */
skb = NULL;
}
决定完是不是要转发,是不是要递交到Host,就可以正在的干活了。如果需要转发(skb不为NULL),又在FBI中找到了目的端口,就转发到改端口。否则就flooding。如果需要递交,就调用br_pass_frame_up。
if (skb) {
if (dst) {
dst->used = jiffies;
br_forward(dst->dst, skb, skb2);// 数据转发到FDB查询到的端口
} else
br_flood_forward(br, skb, skb2, unicast);// 数据Flood到所有端口
}
if (skb2)
return br_pass_frame_up(skb2);// 数据递交到本地Host
... ...
顺便提一下,目前为止skb->dev还么有改变,因为不能确定要交换的skb->dev是哪个,如果是本地递交,就会被替换成网桥设备,如果是转发或者flooding则需要换成对应端口设备,而且skb可能还需要再clone。
6.5 本地递交:br_pass_frame_up
进入br_pass_frame_up的skb是打算经由Bridge设备,输入到本地Host的。数据包从网桥端口设备进入,经过网桥设备,然后再进入协议栈,其实是“两次经过net_device”,一次是端口设备,另一次是网桥设备。现在数据包离开网桥端口进入网桥设备,需要修改skb->dev字段。
indev = skb->dev;
skb->dev = brdev;
skb->dev 起初是网桥端口设备,现在离开网桥端口进入网桥的时候,被替换为网桥设备的net_device。如果设备是TX,或者从一个端口转发的另一个skb->dev也会相应改变。不论数据的流向如何,skb->dev总是指向目前所在的net_device{}。
递交的最后一步是经过NF_BR_LOCAL_IN钩子点,然后是我们熟悉的netif_receive_skb,只不过这次进入该函数的时候skb->dev已经被换成了Bridge设备。这可以理解为进入了Bridge设备的处理。
return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,netif_receive_skb);
Bridge Local In的数据被修改skb->dev后再次进入netif_receive_skb,原来那个netif_receive_skb因为rx_handler返回CONSUMED而结束。
6.6 数据转发到端口:br_forward
我们再看看 br_handle_frame_finish的另一个支流,转发支流,首先是转发到单个端口的情况,出现这种精确的转发,意味着FDB里面有目的MAC对应的条目,找到了目的端口。直接转发的某个端口通过函数br_forward。
转发前需要做几个检查,必须同时满足以下条件:
a.不能转发给自己 (ingress/egress端口 不能相同)除非目的端口设置了HAIRPIN模式。
b.如果出口端口的状态不是Forwarding,则不能转发出去。如果一个网桥没有启用STP功能,并且网络接口的状态为UP,那么它网桥端口的状态为Forwarding。如果启用STP,每个端口都有一个严格的状态,规定那些端口在什么情况下才能成为Forwarding状态,否则容易造成环路,产生网络风暴。
以上检查由should_deliver()完成。
接着调用函数_br_forward(),_br_forward函数也没干什么,就是调用了br_forward_finish()函数。Br_forward_finish()函数调用了br_dev_queue_push_xmit()函数。
int br_dev_queue_push_xmit(struct sk_buff *skb)
{
/* drop mtu oversized packets except gso */
if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb))
kfree_skb(skb);
else {
/* ip_refrag calls ip_fragment, doesn't copy the MAC header. */
if (nf_bridge_maybe_copy_header(skb))
kfree_skb(skb);
else {
skb_push(skb, ETH_HLEN);
dev_queue_xmit(skb);
}
}
return 0;
}
该函数主要完成如下工作:
- 做些必要的检查工作。例如,报文的长度比出口端口的MTU还大,则丢掉该报文。
- 网桥在处理数据包里,只需拆包来获得目标MAC地址,而不需要 更改数据包的任何内容。但在入口网卡的驱动中已将以太网头部 剥掉,现在需要将它套上。Skb_push函数实现这一功能。
- 放到网卡输出队列里,该网卡驱动将它送出去。
6.7 Flooding到各个端口:br_flood_forwards
br_flood_forwards只是函数br_flood的包裹函数。br_flood()遍历每个网桥端口,如果可以的话(满足刚刚说过的should_deliver的要求),就用__packet_hook( __br_forward())转发之。不过函数实现的时候用了一个小技巧,判断为能不能转发后先不急着转发,而是看看下一个端口,如果下一个端口也需要转发,才把数据转发到上次那个要转发到端口。这么做的原因也是减少一次clone。如果没有后续可以转发的端口,就不需要clone了。
struct net_bridge_port *p;
struct net_bridge_port *prev;
prev = NULL;
list_for_each_entry_rcu(p, &br->port_list, list) {
if (should_deliver(p, skb)) {
if (prev != NULL) {
struct sk_buff *skb2;
if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL) {
br->dev->stats.tx_dropped++;
kfree_skb(skb);
return;
}
__packet_hook(prev, skb2);
}
prev = p;
}
}
if (prev != NULL) {
__packet_hook(prev, skb);
return;
}
此外br_flood也会使用__br_forward最终转发数据帧,和br_forward一样。
6.8 网桥数据流小节
两次经过net_device{}小节,再谈谈skb经过两次net_device{}这事。 输入路径经过两次net_device{}分别是网桥端口的和网桥设备的,也就是两次调用netif_receive_skb。 和输入路径一样,输出的帧同样会经过两次net_device,即先网桥设备后网桥端口,对输出而言的函数是两次调用dev_queue_xmit; 如果将这个概念扩展,其实对于转发(forward)的数据帧也是两次经过net_device,两次都是网桥端口的net_device{},函数的话,一次是netif_receive_skb,一次是dev_queue_xmit)。
7. 转发数据库
转发数据库用于记录MAC地址端口映射。网桥通过地址学习,将学习到的MAC地址和相应端口加入该数据库;网桥端口本身的MAC会被永久的加入到FDB中(br_add_if());用户还可以配置静态的映射。FDB和是否打开STP无关,只不过打开STP后,只有Learning/Forwardnig才会学习。
记录下的MAC地址(数据库条目)会被更新,并且有老化时间(默认是300秒,也就是5min),如果使用旧STP算法,拓扑变化的时候该老化时间被设置成15秒,如果使用RSTP,FDB中,某端口相关所有条目会被清除。虽然之前已经介绍过net_device_fdb_entry{},我们还是罗列一下.
struct net_bridge_fdb_entry {
struct hlist_node hlist; // FDB的各个Entry使用哈希表组织,这个是bucket冲突元素链表节点 ;
struct net_bridge_port *dst; // 条目对应的网桥端口(没有直接使用端口ID);
struct rcu_head rcu;
unsigned long updated; // 最后一次更新的时间,会与Bridge的老化定时器比较。
unsigned long used;
mac_addr addr; // 条目对应的MAC地址
unsigned char is_local; // 是否是本地端口的MAC
unsigned char is_static; // 是否是静态配置的MAC
}
这里重申一下FDB是网桥的属性,因此保存在net_bridge{}中,保存的方式是一个Hash表。
struct net_bridge { ... ... struct hlist_head hash[BR_HASH_SIZE]; ... ... };
FDB条目的添加、删除,查询,更新操作本身想必不会太复杂,无非是哈希表链表操作。关键是搞弄清楚FDB访问和修改的场景。
7.1 FDB初始化,查找
FDB的初始化非常简单,为net_bridge_fdb_entry{}结构初始化一个cache以便快速分配条目。另外还以随机值生成一个salt,这个salt在hash的时候使用,引入随机值可以分散各个Hash键,并且防止DoS攻击。
int __init br_fdb_init(void)
{
br_fdb_cache = kmem_cache_create("bridge_fdb_cache",
sizeof(struct net_bridge_fdb_entry),
0,
SLAB_HWCACHE_ALIGN, NULL);
if (!br_fdb_cache)
return -ENOMEM;
get_random_bytes(&fdb_salt, sizeof(fdb_salt));
return 0;
}
7.2 地址老化
我们知道网桥学到地址都有一个老化的过程。网桥维护了几个超期时间值,包括老化时间br->ageing_time,默认300秒;和转发延迟br->foward_delay,默认15秒。FDB中的每个地址如果自上次跟新(记录于net_bridge_fdb_entry->updated)以来,流逝的时间超过了“保持时间”(由hold_time(),返回可能是老化时间或者短老化时间),地址就需要被删除。hold_time()在正常情况下返回老化时间br->ageing_time,但是如果检测到了拓扑变化,这将老化时间缩短为br->forward_delay,后者也称为“短老化定时器(short aging timer)”。
7.2.1 注册、打开垃圾收集定时器
网桥在什么时候检查FDB中的各个地址是否老化、并将老化的地址从FDB中移除呢?Kernel将这个工作交由“垃圾收集定时器”来完成。gc_timer保存在net_bridge{}中。
struct net_bridge { ... ... struct timer_list gc_timer; ... ... };
网桥设备被创建并初始化的时候,具体说来是br_dev_setup的时候,通过br_stp_timer_init初始化STP相关的几个定时器,其中包括了垃圾收集定时器。
br_add_bridge + |- alloc_netdev + |- br_dev_setup + |- br_stp_timer_init + |- ... HELLO Timer ... |- ... TCN Timer ... |- ... Topology Change Timer ... - setup_timer(&br->gc_timer, br_fdb_cleanup, (unsigned long) br);
setup_timer函数将timer->function和timer->data设置为:br_fdb_cleanup和net_bridge{}。要注意的是,不论STP协议是否运行,地址老化(垃圾收集)都是必要的。这里只是设置各个timer的回调函数和私有数据。并没有启动Timer。
在网桥设备打开的时候,br_stp_enable_bridge会把各个timer打开,包括gc_timer,
br_dev_open + |- br_stp_enable_bridge + |- ... |- mod_timer(&br->gc_timer, jiffies + HZ/10); // gc_timer第一次启动的地方 |- ...
第一次打开的时候,在1/10秒后br_fdb_cleanup被调用;此后回调函数br_fdb_cleanup将timer自己设置为每br->aging_time或者“最近的一个条目到期时间”调用。这个timer的实现是值得学习的,因为它不是完全周期性的timer,而是根据条目中需要检查的时间结合一个最大默认周期来进行。
7.2.2 地址老化处理
我们看看br_fdb_cleanup()是怎么实现的,顺便也提一下hold_time()。
void br_fdb_cleanup(unsigned long _data) {
struct net_bridge *br = (struct net_bridge *)_data;
unsigned long delay = hold_time(br);// 地址老化时间,MIN {ageing_time, forward_delay} unsigned long next_timer = jiffies + br->ageing_time;// 预设下次收集时间为 ageing_time秒后,稍后可能调整 int i;
spin_lock(&br->hash_lock);
for (i = 0; i < BR_HASH_SIZE; i++) {// 遍历所有FDB Hash Bucket
struct net_bridge_fdb_entry *f;
struct hlist_node *n;
hlist_for_each_entry_safe(f, n, &br->hash[i], hlist) { // 遍历所有FDB Hash冲突链表
unsigned long this_timer;
if (f->is_static)// 静态条目,包括端口地址和用户设置的条目,不会老化、删除。
continue;
this_timer = f->updated + delay;// 条目老化到期的时间
if (time_before_eq(this_timer, jiffies))// 已经到期(到期时间在当前时间之前),就把它删除
fdb_delete(br, f); // 这就是清除到期FDB Entry的地方
else if (time_before(this_timer, next_timer))
next_timer = this_timer;// 如果FDB中的某个条目中默认的下次检查时间之前,就将下次收集时间提前
}
}
spin_unlock(&br->hash_lock);
mod_timer(&br->gc_timer, round_jiffies_up(next_timer));// 设置下次垃圾收集的时间
}
7.3 “本地”FDB条目
网桥设备、网桥端口设备的MAC地址作为“Local”条目添加到FDB表,其is_local和is_static都需要置1,不会老化。这类FDB Entry通过fdb_insert添加,并且在地址改变的时候,需要做相应的更新。
从下图我们发现,并没有添加“网桥设备”MAC FDB的地方,这是因为网桥的MAC因默认情况下是其端口之一的地址,因此无需加入FDB。但是如果网桥端口地址改变时则需要更新。
对于网桥的地址加入,或者不加入FDB对于入口流量的影响,我们应该了解到, 只要帧的目的MAC是网桥或者各个网桥端口的MAC之一,帧就是要被递交到本地Host的。
了解了何时“插入”本地且静态的网桥端口、网桥的地址后,我们看看fdb_insert的实现,
static int fdb_insert(struct net_bridge *br, struct net_bridge_port *source, const unsigned char *addr, u16 vid) {
struct hlist_head *head = &br->hash[br_mac_hash(addr, vid)];// FDB是Per-VLAN的,addr和vid都作为Hash键 struct net_bridge_fdb_entry *fdb;
if (!is_valid_ether_addr(addr))// 要插入的地址必须是合法的Ethernet地址
return -EINVAL;
fdb = fdb_find(head, addr, vid);// 在某个VLAN中,地址是否已经存在
if (fdb) { // 地址已经存在?
/* it is okay to have multiple ports with same
* address, just use the first one.
*/
if (fdb->is_local) // 并且是Local的
return 0; // 允许多个端口用于同一个地址
br_warn(br, "adding interface %s with same address "
"as a received packet\n",
source ? source->dev->name : br->dev->name);
fdb_delete(br, fdb);// 但如果地址和分本地地址冲突,就需要将非本地地址的条目删除
}
fdb = fdb_create(head, source, addr, vid);// 创建新的net_bridge_fdb_entry{},并插入FDB(br->hash)中
if (!fdb)
return -ENOMEM;
fdb->is_local = fdb->is_static = 1;// “插入”的地址一定是本地且静态的
fdb_notify(br, fdb, RTM_NEWNEIGH);
return 0;
}
7.4 地址学习
除了网桥端口和网桥的MAC地址,用户还能手动添加静态(通过netlink套接字),已经网桥字段学习地址的过程,
fdb_create的实现也不难理解,
static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head, struct net_bridge_port *source, const unsigned char *addr,
__u16 vid)
{ struct net_bridge_fdb_entry *fdb;
fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC);
if (fdb) {
memcpy(fdb->addr.addr, addr, ETH_ALEN);
fdb->dst = source;
fdb->vlan_id = vid;
fdb->is_local = 0;
fdb->is_static = 0;
fdb->updated = fdb->used = jiffies;
hlist_add_head_rcu(&fdb->hlist, head);
}
return fdb;
}