linux内核网络协议栈学习笔记(4)

时间:2022-07-01 11:08:31

本篇不关注交换机相关的如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之前也提到过,这里不多说了