本篇不关注交换机相关的如BPDU,STP之类的实现,如果可能后续会在研究ovs的文章中跟进这块,本文只关注linux内核中的bridge模块在数据包收发链中的角色
我们知道内核的net_device的结构后面一般会跟一块内存作为私有数据,不同的网卡驱动会利用这块内存存放自己的私有结构,如intel驱动的ixgbe_q_vector。bridge驱动的私有结构为net_bridge
struct net_bridge
{
spinlock_t lock;
struct list_head port_list;
struct net_device *dev;
spinlock_t hash_lock;
struct hlist_head hash[BR_HASH_SIZE];
struct list_head age_list;
unsigned long feature_mask;
/* STP相关结构,这里省略 */
struct timer_list hello_timer;
struct timer_list tcn_timer;
struct timer_list topology_change_timer;
struct timer_list gc_timer;
struct kobject *ifobj;
};
其中port_list是一个net_bridge_port的链表,和net_bridge_port->list指向同一个llist_head,net_bridge_port的br指向对应的net_bridge,dev指向对应的net_device,整个net_bridge_port 结构如下:
struct net_bridge_port
{
struct net_bridge *br;
struct net_device *dev;
struct list_head list;
/* STP相关结构,这里省略 */
struct timer_list forward_delay_timer;
struct timer_list hold_timer;
struct timer_list message_age_timer;
struct kobject kobj;
struct rcu_head rcu;
unsigned long flags;
};
net_bridge内部维护了一个hash表,大小为BR_HASH_SIZE,这是一个net_bridge_fdb_entry的哈希表,和交换机所用的hash - port表是一个类型。hash[BR_HASH_SIZE]是一个hlist_head数组,我们知道hlist_head只是一个hlist_node指针,同一个hash值所有碰撞的bucket通过hlist_node形成一个链表
struct net_bridge_fdb_entry
{
struct hlist_node hlist;
struct net_bridge_port *dst;
struct rcu_head rcu;
unsigned long ageing_timer;
mac_addr addr;
unsigned char is_local;
unsigned char is_static;
};
new_bridge_dev创建一个新bridge设备,首先调用alloc_netdev创建一个net_device通用设备,其中alloc_netdev会调用br_dev_setup,函数实现如下:
void br_dev_setup(struct net_device *dev)
{
random_ether_addr(dev->dev_addr);
ether_setup(dev);
dev->netdev_ops = &br_netdev_ops;
dev->destructor = free_netdev;
SET_ETHTOOL_OPS(dev, &br_ethtool_ops);
dev->tx_queue_len = 0;
dev->priv_flags = IFF_EBRIDGE;
dev->features = NETIF_F_SG | NETIF_F_FRAGLIST | NETIF_F_HIGHDMA |
NETIF_F_GSO_MASK | NETIF_F_NO_CSUM | NETIF_F_LLTX |
NETIF_F_NETNS_LOCAL | NETIF_F_GSO;
}
linux网桥只支持以太网,所以dev->tx_queue_len为1000,把dev->netdev_ops = &br_netdev_ops,可以看出网桥也被当作一种网卡驱动来对待,其操作函数为:
static const struct net_device_ops br_netdev_ops = {
.ndo_open = br_dev_open,
.ndo_stop = br_dev_stop,
.ndo_start_xmit = br_dev_xmit,
.ndo_set_mac_address = br_set_mac_address,
.ndo_set_multicast_list = br_dev_set_multicast_list,
.ndo_change_mtu = br_change_mtu,
.ndo_do_ioctl = br_dev_ioctl,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_netpoll_cleanup = br_netpoll_cleanup,
.ndo_poll_controller = br_poll_controller,
#endif
};
最后是初始化br->port_list链表,br->group_addr地址,各种stp相关的设置,比如ageing_time设置为300秒;调用br_stp_timer_init设置定时器:hello_timer, tcn_timer, topology_change_timer,gc_timer等等,不一一讲述了
br_dev_open/br_dev_close:打开/关闭stp,start/stop 发送队列
br_add_bridge:首先调用new_bridge_dev创建一个新bridge的net_device,然后调用register_netdev把设备注册到内核
br_del_bridge:调用del_br,后者对net_bridge->port_list的每个net_bridge_port,调用del_nbq删掉net_bridge_port,删掉br->gc_timer,最后unregister_netdevice设备
del_nbp:dev_set_promiscuity减1(当这个值>0表示处于混杂模式),调用br_stp_disable_port挂起port同时删除stp对应的计时器,调用br_fdb_delete_by_port删除port相关的fdb表项同时flush所有静态表项
br_add_if:
首先判断,如果port的设备不是以太设备或者是回环设备,返回EINVAL,如果dev->br_port不为空说明已经绑到bridge上,返回EBUSY,如果设备已经是个bridge了,返回ELOOP。
接着调用new_nbp创建一个net_bridge_port,此时状态是BR_STATE_DISABLED,调用dev_set_promiscuity增加计数,调用br_fdb_insert把port和port对应的mac地址加到fdb表中,可以看到这里用jhash对mac地址算一个hash值,然后把port加到fdb哈希表对应的hlist_head的hlist_node链表中
接着调用dev_disable_lro禁用LRO,调用br_stp_recalculate_bridge_id重新计算一个bridge id,最后,如果port状态为IFF_UP,能侦测到载波,且bridge状态也为IFF_UP,就把port加入stp计算中
br_del_if:
基本上是br_add_if的一个反向过程,首先调用del_nbp,这个函数前面已经分析过了,接着调用br_stp_recalculate_bridge_id重新计算bridge id
下面来看bridge如何收发包和转包:
bridge收包的入口函数是handle_bridge,里面会调用br_handle_frame_hook,这是个br_handle_frame函数的封装(不知道这里为啥要封一层,感觉很多余)
br_handle_frame:
首先调用is_link_local看下dest是不是本地,如果是的话调用NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev, NULL, br_handle_local_finish)本地接收掉了
否则进入forward部分,这里有可能会skb->pkt_type = PACKET_HOST,最终调用到br_handle_frame_finish中
br_handle_frame_finish:
先基于eth_hdr(skb)->h_source更新下fdb转发数据库,下面会考虑广播(我这里忽略掉多播)的场景,如果dst是广播地址的话,还会多一个步骤就是调用br_flood_forward,否则如果在fdb里发现了dst对应的port,并且这个port连在bridge上,此时调用br_pass_frame_up(别忘了如果是广播也会走到这步)
br_flood_forward:br_flood_forward就是调br_flood,里面对bridge每一个port,调用maybe_deliver判断下可不可以收,如果这个port可以收包,则会把这个port发到prev变量里,然后再下一次调用maybe_deliver的时候调用到deliver_clone,里面clone一个skb出来调用__br_forward(本人至今不明白为啥这么麻烦。。。)
看了下br_forward.c的代码,对于bridge而言,__br_forward,__br_deliver区别不大,只是改了下进出设备的顺序而已,最终都要调br_forward_finish
br_forward_finish:调用NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, xx, xx, br_dev_queue_push_xmit),这个br_dev_queue_push_xmit后面会提到,是bridge的发送核心函数
br_pass_frame_up:表示bridge准备接收而不是转发了,里面调用了NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL, netif_receive_skb),同时把skb->dev设置为bridge代表的net_device,这时调用netif_receive_skb就认为是从bridge设备而不是网卡设备收到的包了,下面会转给上层协议(IP)处理
bridge发包的入口函数是br_dev_xmit,分为广播,多播和单播三种情况,如果是广播或者单播的目的地址没在fdb表里找到,那么就br_flood_deliver,否则调用br_deliver
br_deliver:核心是调用NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_OUT, skb, NULL, skb->dev, br_forward_finish),这个skb->dev在bridge看来是进来的port
br_forward_finish:上面提到其实是掉br_dev_queue_push_xmit
br_dev_queue_push_xmit:如果超过了mtu又没有要求gso的话,直接drop;最终调用dev_queue_xmit把skb发出去,这个之前的文章有讲过
如果是广播,调用br_flood_deliver,里面实际上调用了br_flood(br, skb, NULL, __br_deliver),这个br_flood之前也提到过,这里不多说了