4000-9696-28

Linux-网桥原理分析 (一)

2013年07月24日 09:51供稿中心:北大青鸟总部

摘要: 本文的参考分析的源代码版本是2.6.15,我是边学习边总结,学习的过程中得益于Linux论坛上大侠们总结分析的文档,他山之石可以攻玉...

目 录

1..... 前言... 6

2       网桥的原理... 7

2.1             桥接的概念... 7

2.2             linux的桥接实现... 8

2.3             网桥的功能... 9

3       网桥的配置... 10

4       网桥的实现... 10

4.1             初始化... 11

4.2             新建网桥... 11

4.3             添加删除端口... 14

5       网桥数据结构... 16

6       网桥数据库的维护... 19

6.1             数据库的创建和销毁... 19

6.2             数据库更新... 19

6.3             创建数据项... 20

6.4             查找数据项... 21

6.5             MAC地址过期清理... 22

7       网桥数据包的处理流程... 23

7.1             netif_receive_skb. 26

7.2             Br_handle_frame. 28

7.3             Br_handle_frame_finish. 29

7.4             Br_pass_frame_up. 29

7.5             Br_forward. 30

7.6             __br_forward. 31

7.7             Br_forward_finish. 31

7.8             Br_dev_queue_push_xmit 31

8..... 参考文献... 32

------------------------------------------------------------------------------------------------------------------------------------------------------------------

1  前言

本文的参考分析的源代码版本是2.6.15,我是边学习边总结,学习的过程中得益于Linux论坛上大侠们总结分析的文档,他山之石可以攻玉,学习过程中我也会边学边总结,开源的发展在于共享,我也抛块砖,望能引到玉!

由于自身水平有限,且相关的参考资料较少,因此其中的结论不能保证完全正确,如果在阅读本文的过程中发现了问题欢迎及时与作者联系。也希望能有机会和大家多多交流学习心得!

2  网桥的原理

2.1   桥接的概念

      简单来说,桥接就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。

     交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的。于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信。

     如下图:主机A发送的报文被送到交换机S1的eth0口,由于eth0与eth1、eth2桥接在一起,故而报文被复制到eth1和eth2,并且发送出 去,然后被主机B和交换机S2接收到。而S2又会将报文转发给主机C、D。


        交换机在报文转发的过程中并不会篡改报文数据,只是做原样复制。然而桥接却并不是在物理层实现的,而是在数据链路层。交换机能够理解数据链路层的报文,所以实际上桥接却又不是单纯的报文转发。

       交换机会关心填写在报文的数据链路层头部中的Mac地址信息(包括源地址和目的地址),以便了解每个Mac地址所代表的主机都在什么位置(与本交换机的哪个网口相连)。在报文转发时,交换机就只需要向特定的网口转发即可,从而避免不必要的网络交互。这个就是交换机的“地址学习”。但是如果交换机遇到一个自己未学习到的地址,就不会知道这个报文应该从哪个网口转发,则只好将报文转发给所有网口(接收报文的那个网口除外)。

      比如主机C向主机A发送一个报文,报文来到了交换机S1的eth2网口上。假设S1刚刚启动,还没有学习到任何地址,则它会将报文转发给eth0和 eth1。同时,S1会根据报文的源Mac地址,记录下“主机C是通过eth2网口接入的”。于是当主机A向C发送报文时,S1只需要将报文转发到 eth2网口即可。而当主机D向C发送报文时,假设交换机S2将报文转发到了S1的eth2网口(实际上S2也多半会因为地址学习而不这么做),则S1会 直接将报文丢弃而不做转发(因为主机C就是从eth2接入的)。

     然而,网络拓扑不可能是永不改变的。假设我们将主机B和主机C换个位置,当主机C发出报文时(不管发给谁),交换机S1的eth1口收到报文,于是交换机 S1会更新其学习到的地址,将原来的“主机C是通过eth2网口接入的”改为“主机C是通过eth1网口接入的”。

     但是如果主机C一直不发送报文呢?S1将一直认为“主机C是通过eth2网口接入的”,于是将其他主机发送给C的报文都从eth2转发出去,结果报文就发 丢了。所以交换机的地址学习需要有超时策略。对于交换机S1来说,如果距离最后一次收到主机C的报文已经过去一定时间了(默认为5分钟),则S1需要忘记 “主机C是通过eth2网口接入的”这件事情。这样一来,发往主机C的报文又会被转发到所有网口上去,而其中从eth1转发出去的报文将被主机C收到。

2.2  linux的桥接实现

      linux内核支持网口的桥接(目前只支持以太网接口)。但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发、要么丢弃。小型的交换机里面只需要一块交换芯片即可,并不需要CPU。而运行着linux内核的机器本身就是一台主机,有可能就是网络报文的目的地。其收到的报文除了转 发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己消化。

      linux内核是通过一个虚拟的网桥设备来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如下图(摘自ULNI):

      网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看得到br0,因为桥接是在数据链路层实现的,上层不需要关心桥接的细节。于是协议栈上层需要发送的报文被送到br0,网桥设备的处理代码再来判断报文该被转发到eth0或是eth1,或者两者皆是;反过来,从eth0或从eth1接收到的报文被提交给网桥的处理代码,在这里会判断报文该转发、丢弃、或提交到协议栈上层。

     而有时候eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收(从而绕过网桥)。

2.3   网桥的功能

       概括来说,网桥实现最重要的两点:

       1. MAC学习:学习MAC地址,起初,网桥是没有任何地址与端口的对应关系的,它发送数据,还是得想HUB一样,但是每发送一个数据,它都会关心数据包的来源MAC是从自己的哪个端口来的,由于学习,建立地址-端口的对照表(CAM表)。

       2. 报文转发:每发送一个数据包,网桥都会提取其目的MAC地址,从自己的地址-端口对照表(CAM表)中查找由哪个端口把数据包发送出去。

3  网桥的配置

       在Linux里面使用网桥非常简单,仅需要做两件事情就可以配置了。其一是在编译内核里把CONFIG_BRIDGE或CONDIG_BRIDGE_MODULE编译选项打开;其二是安装brctl工具。第一步是使内核协议栈支持网桥,第二步是安装用户空间工具,通过一系列的ioctl调用来配置网桥。下面以一个相对简单的实例来贯穿全文,以便分析代码。

       Linux机器有4个网卡,分别是eth0~eth4,其中eth0用于连接外网,而eth1, eth2, eth3都连接到一台PC机,用于配置网桥。只需要用下面的命令就可以完成网桥的配置:

Brctl addbr br0 (建立一个网桥br0, 同时在Linux内核里面创建虚拟网卡br0)

Brctl addif br0 eth1

Brctl addif br0 eth2

Brctl addif br0 eth3 (分别为网桥br0添加接口eth1, eth2和eth3)

       其中br0作为一个网桥,同时也是虚拟的网络设备,它即可以用作网桥的管理端口,也可作为网桥所连接局域网的网关,具体情况视你的需求而定。要使用br0接口时,必需为它分配IP地址。为正常工作,PC1, PC2,PC3和br0的IP地址分配在同一个网段。

        在内核,网桥是以模块的方式存在,注册源码路径:\net\brige\br.c:

        4.1 初始化

static int __init br_init(void)

{

   br_fdb_init(); //网桥数据库初始化,分配slab缓冲区

#ifdef CONFIG_BRIDGE_NETFILTER

   if (br_netfilter_init()) //netfilter钩子初始化

       return 1;

#endif

   brioctl_set(br_ioctl_deviceless_stub); //设置ioctl钩子函数:br_ioctl_hook

   br_handle_frame_hook = br_handle_frame;//设置报文处理钩子:br_ioctl_hook

   //网桥数据库处理钩子

   br_fdb_get_hook = br_fdb_get;

   br_fdb_put_hook = br_fdb_put;

   //在netdev_chain通知链表上注册

   register_netdevice_notifier(&br_device_notifier);

   return 0;

}

         4.2 新建网桥

        前面说到通过brctl addbr br0命令建立网桥,此处用户控件调用的brctl命令最终对应到内核中的br_ioctl_deviceless_stub处理函数:

        /net/bridge/br_ioctl.c

int br_ioctl_deviceless_stub(unsigned int cmd, void __user *uarg)

{

   switch (cmd) {

   case SIOCGIFBR:

   case SIOCSIFBR:

       return old_deviceless(uarg);

       

   case SIOCBRADDBR: //新建网桥

   case SIOCBRDELBR: //删除网桥

   {

       char buf[IFNAMSIZ];

       if (!capable(CAP_NET_ADMIN))

           return -EPERM;

           

       //copy_from_user:把用户空间的数据拷入内核空间

       if (copy_from_user(buf, uarg, IFNAMSIZ))

           return -EFAULT;

       buf[IFNAMSIZ-1] = 0;

       if (cmd == SIOCBRADDBR)

           return br_add_bridge(buf);

       return br_del_bridge(buf);

   }

   }

   return -EOPNOTSUPP;

}

         在这里,我们传入的cmd为SIOCBRADDBR.转入br_add_bridge(buf)中进行(./net/bridge/br_if.c):

int br_add_bridge(const char *name)

{

   struct net_device *dev;

   int ret;

   

   //为虚拟桥新建一个net_device

   dev = new_bridge_dev(name);

   if (!dev)

       return -ENOMEM;

   rtnl_lock();

   //由内核确定接口名字,例如eth0 eth1等

   if (strchr(dev->name, '%')) {

       ret = dev_alloc_name(dev, dev->name);

       if (ret < 0)

           goto err1;

   }

   //向内核注册此网络设备

   ret = register_netdevice(dev);

   if (ret)

       goto err2;

   /* network device kobject is not setup until

    * after rtnl_unlock does it's hotplug magic.

    * so hold reference to avoid race.

    */

   dev_hold(dev);

   rtnl_unlock();

   

   //在sysfs中建立相关信息

   ret = br_sysfs_addbr(dev);

   dev_put(dev);

   if (ret)

       unregister_netdev(dev);

out:

   return ret;

err2:

   free_netdev(dev);

err1:

   rtnl_unlock();

   goto out;

}

         网桥是一个虚拟的设备,它的注册跟实际的物理网络设备注册是一样的。我们关心的是网桥对应的net_device结构是什么样的,继续跟踪进new_bridge_dev(./net/bridge/br_if.c):

static struct net_device *new_bridge_dev(const char *name)

{

   struct net_bridge *br;

   struct net_device *dev;

   //分配net_device

   dev = alloc_netdev(sizeof(struct net_bridge), name,

            br_dev_setup);  

   if (!dev)

       return NULL;

   //网桥的私区结构为net_bridge

   br = netdev_priv(dev);

   //私区结构中的dev字段指向设备本身

   br->dev = dev;

   //队列初始化。在port_list中保存了这个桥上的端口列表

   spin_lock_init(&br->lock);

   INIT_LIST_HEAD(&br->port_list);

   spin_lock_init(&br->hash_lock);

   //下面这部份代码跟stp协议相关,我们暂不关心

   br->bridge_id.prio[0] = 0x80;

   br->bridge_id.prio[1] = 0x00;

   memset(br->bridge_id.addr, 0, ETH_ALEN);

   br->stp_enabled = 0;

   br->designated_root = br->bridge_id;

   br->root_path_cost = 0;

   br->root_port = 0;

   br->bridge_max_age = br->max_age = 20 * HZ;

   br->bridge_hello_time = br->hello_time = 2 * HZ;

   br->bridge_forward_delay = br->forward_delay = 15 * HZ;

   br->topology_change = 0;

   br->topology_change_detected = 0;

   br->ageing_time = 300 * HZ;

   INIT_LIST_HEAD(&br->age_list);

   br_stp_timer_init(br);

   return dev;

}

        在br_dev_setup中还做了一些另外在函数指针初始化(./net/bridge/br_device.c):

void br_dev_setup(struct net_device *dev)

{

   //将桥的MAC地址设为零

   memset(dev->dev_addr, 0, ETH_ALEN);

   //初始化dev的部分函数指针,因为目前网桥设备主适用于以及网,

   //以太网的部分功能对它也适用

   ether_setup(dev);

   

   //设置设备的ioctl函数为br_dev_ioctl

   dev->do_ioctl = br_dev_ioctl;

   //网桥与一般网卡不同,网桥统一统计它的数据包和字节数等信息

   dev->get_stats = br_dev_get_stats;

   // 网桥接口的数据包发送函数,真实设备要向外发送数据时,是通过网卡向外发送数据

   // 而该网桥设备要向外发送数据时,它的处理逻辑与网桥其它接口的基本一致。

   dev->hard_start_xmit = br_dev_xmit;

   dev->open = br_dev_open;

   dev->set_multicast_list = br_dev_set_multicast_list;

   dev->change_mtu = br_change_mtu;

   dev->destructor = free_netdev;

   SET_MODULE_OWNER(dev);

   dev->stop = br_dev_stop;

   dev->tx_queue_len = 0;

   dev->set_mac_address = NULL;

   dev->priv_flags = IFF_EBRIDGE;

}

4.3  添加删除端口

       仅仅创建网桥,还是不够的。实际应用中的网桥需要添加实际的端口(即物理接口),如例子中的eth1, eth2等。应用程序在使用ioctl来为网桥增加物理接口,对应内核函数br_dev_ioctl的代码和分析如下( /net/bridge/br_ioctl.c):

int br_dev_ioctl(struct net_device *dev, struct ifreq *rq, int cmd)

{

   struct net_bridge *br = netdev_priv(dev);

   switch(cmd) {

   case SIOCDEVPRIVATE:

       return old_dev_ioctl(dev, rq, cmd);

   case SIOCBRADDIF: //添加

   case SIOCBRDELIF: //删除

       //同一处理函数,默认为添加

       return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF);

   }

   pr_debug("Bridge does not support ioctl 0x%x\n", cmd);

   return -EOPNOTSUPP;

}

       下面分析具体的添加删除函数add_del_if ( /net/bridge/br_ioctl.c):

static int add_del_if(struct net_bridge *br, int ifindex, int isadd)

{

   struct net_device *dev;

   int ret;

   if (!capable(CAP_NET_ADMIN))

       return -EPERM;

   dev = dev_get_by_index(ifindex);

   if (dev == NULL)

       return -EINVAL;

   

   if (isadd)

       ret = br_add_if(br, dev);

   else

       ret = br_del_if(br, dev);

   dev_put(dev);

   return ret;

}

          对应的添加删除函数分别为:br_add_if, br_del_if( /net/bridge/br_ioctl.c):

int br_add_if(struct net_bridge *br, struct net_device *dev)

{

   struct net_bridge_port *p;

   int err = 0;

   /*--Kernel仅支持以太网网桥--*/

   if (dev->flags & IFF_LOOPBACK || dev->type != ARPHRD_ETHER)

       return -EINVAL;

   /*--把网桥接口当作物理接口加入到另一个网桥中,是不行的,逻辑和代码上都会出现 loop--*/

   if (dev->hard_start_xmit == br_dev_xmit)

       return -ELOOP;

   /*--该物理接口已经绑定到另一个网桥了--*/

   if (dev->br_port != NULL)

       return -EBUSY;

   /*--为该接口创建一个网桥端口数据,并初始化好该端口的相关数据--*/

   if (IS_ERR(p = new_nbp(br, dev, br_initial_port_cost(dev))))

       return PTR_ERR(p);

       

   /*--将该接口的物理地址写入到 MAC-端口映射表中,该MAC是属于网桥内部端口的固定MAC地址,

       它在fdb中的记录是固定的,不会失效(agged)--*/

    if ((err = br_fdb_insert(br, p, dev->dev_addr)))

       destroy_nbp(p);

    /*--添加相应的系统文件信息--*/

   else if ((err = br_sysfs_addif(p)))

       del_nbp(p);

   else {

       /*--打开该接口的混杂模式,网桥中的各个端口必须处于混杂模式,网桥才能正确工作--*/

       dev_set_promiscuity(dev, 1);

       

       /*--加到端口列表--*/

       list_add_rcu(&p->list, &br->port_list);

       /*--STP相关设置-*/

       spin_lock_bh(&br->lock);

       br_stp_recalculate_bridge_id(br);

       br_features_recompute(br);

       if ((br->dev->flags & IFF_UP)

        && (dev->flags & IFF_UP) && netif_carrier_ok(dev))

           br_stp_enable_port(p);

       spin_unlock_bh(&br->lock);

       

       /*--设置设备的mtu--*/

       dev_set_mtu(br->dev, br_min_mtu(br));

   }

   return err;

}

 

int br_del_if(struct net_bridge *br, struct net_device *dev)

{

   struct net_bridge_port *p = dev->br_port;

   

   if (!p || p->br != br)

       return -EINVAL;

   br_sysfs_removeif(p);

   del_nbp(p);

   spin_lock_bh(&br->lock);

   br_stp_recalculate_bridge_id(br);

   br_features_recompute(br);

   spin_unlock_bh(&br->lock);

   return 0;

}

关于我们
公司简介
发展历程
青鸟荣誉
联系我们
加入我们
青鸟课程
BCVE视频特效课程
BCUI全链路UI设计
ACCP
学士后Java
启蒙星IT工程师基础课程
学习客户端下载
青鸟优师
青鸟云课堂
微信 公众号 咨询 顶部 首页
官方新版意见收集

*

官方新版意见收集

提交成功,感谢您的反馈。

我们会认真阅读和考虑每个用户的反馈。