Linux-kernel 网桥代码分析

本文分析的kernel版本为:2.6.24.4,网桥代码目录为:linux-2.6.24.4/net/bridge。
      网桥是kernel网络模块中相于独立的module,读者具有简单的kernel网络设备驱动开发和kerenl网络协议的基础知识即可。我在2007 年就开始接触网桥了,当时有位同事为了测试网桥的功能,还特地查看了网桥的代码,还特意转告我一定要看看这部分的代码,他说比较简单,也很容易看个明白。 我当时在做Linux系统的测试工作,还未正式进行开发工作,虽然把代码查看了一翻,但由于经验关系,看得一窍不通。两年过去了,在Linux的开发过程 了,接触了kernel的机会也很多。去年3月份,阅读了kernel中网络子模块的部分代码。最近由于工作的需要,阅读了项目中网络驱动部分的代码,就 这样,目光转向了Linux网桥代码。遂有写此文之愿。
 
第一部分: 网桥的报文处理功能分析


1.1  Linux网桥的配置实例
      在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地址必须分配在同一个网段。
1.2  网桥的数据结构
      网桥的核心数据结构主要有:struct net_bridge和struct net_bridge_port这两个结构,当然还有通用的网络设备结构struct net_device。为了简单起见,我们以上述为例子,描述出此时它的静态结构。
 
      每个网桥由struct net_bridge结构来维护,它主要的成员有:port_list,dev和hash。Port_list是一个双向链表,它元素的结构为 struct net_bridge_port,每个加入到网桥的设备都在里面占一个元素结点。Dev指针指向net_device变量,它存放网络设备br0的信息。 Hash是MAC地址的hash表,MAC地址的hash值为数组结构的下标,每个数组元素为链表,每个元素就是唯一的struct net_bridge_fdb_entry结构,以MAC地址为标识符。
1.3  网桥数据包入口
      网桥是一种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函数。代码和分析如下所述:
[linux-2.6.24.4/net/core/dev.c]
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;   
     }   
   }   
}   
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;
     }
   }

 
1.4  handle_bridge处理函数
[linux-2.6.24.4/net/core/dev.c]
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);   
}   
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);

1.5  网桥处理逻辑
[linux-2.6.24.4/net/bridge/br_input.c]
struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb)   {   
  //所有网桥通信的数据包都会进入到这里,谓之为网桥处理函数   
  const unsigned char *dest = eth_hdr(skb)->h_dest;   
  int (*rhook)(struct sk_buff *skb);   
     
  if (!is_valid_ether_addr(eth_hdr(skb)->h_source))   
    goto drop;   
  //如果skb是share的,则拷贝一份   
  skb = skb_share_check(skb, GFP_ATOMIC);   
  if (!skb)   
    return NULL;   
  if (unlikely(is_link_local(dest))) {   
    /* Pause frames shouldn't be passed up by driver anyway */  
    if (skb->protocol == htons(ETH_P_PAUSE))   
      goto drop;   
    //如果该数据包的目标地址为STP协议的组播地址,并且该网桥启用STP功能,   
    //则,结束该数据包的处理,它将会在第(2)处理得到处理   
    if (p->br->stp_enabled != BR_NO_STP) {   
      if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,   
                  NULL, br_handle_local_finish))   
        return NULL;   
      else  
        return skb;   
    }   
    // 如果该包是发往网桥组播的,但该网桥没有启用STP功能,则在下面处理,   
    // 并返回已处理的标识(返回NULL)来通知代码(2)处不需再处理。   
  }   
  switch (p->state) {   
    case BR_STATE_FORWARDING:   
      rhook = rcu_dereference(br_should_route_hook);   
      if (rhook != NULL) {   
        if (rhook(skb))   
          // 如果该接口处于Forwarding状态,并且该报文必需要走L3层   
          // 进行转发,则直接返回,让代码(2)进行处理。   
          // br_should_route_hook钩子函数在ebtable里面设置为ebt_broute函数,   
          //它根据用户的规则来决定该报文是否要能通过L3来转发。   
          return skb;   
        dest = eth_hdr(skb)->h_dest;   
     }   
      /* fall through */  
    case BR_STATE_LEARNING:   
      if (!compare_ether_addr(p->br->dev->dev_addr, dest))   
        //当用内核创建一个网桥的同时也会创建一个虚拟的网络设备,它的名字   
        //为网桥的名字,保存在p->br->dev指针里。P->br->dev和port_list里面的   
        //接口共同组成一个网桥。如果该报文是要发往此接,则标记skb->pkt_type为   
        //PACKET_HOST。因为报文最终是要发送到p->br->dev的输送队列里面,   
        //正如一般的网卡驱动程序将数据包送往到某个net_device的输入队列一样,   
        //这样bridge功能充当了虚拟网卡(如例子中的br0)驱动,应当设置   
        //skb->pkt_type   
        //为PACKET_HOST,表明数据包是要发送该接口,而非是因为打开混杂模式   
        //而接收到的。   
        skb->pkt_type = PACKET_HOST;   
        // 接着由br_handle_frame_finish函数继续处理。   
        NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,   
                br_handle_frame_finish);   
      break;   
  default:   
    //其它状态下的端口,不能处理数据包,直接丢弃。   
  drop:   
    kfree_skb(skb);   
  }   
  // 该数据包要么被网桥处理了,要么处理时出错,不需要上层协议处理,   
  // 返回NULL,代码(2)处不会处理该报文。   
  return NULL;   
}   
struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb) {
  //所有网桥通信的数据包都会进入到这里,谓之为网桥处理函数
  const unsigned char *dest = eth_hdr(skb)->h_dest;
  int (*rhook)(struct sk_buff *skb);
  
  if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
    goto drop;
  //如果skb是share的,则拷贝一份
  skb = skb_share_check(skb, GFP_ATOMIC);
  if (!skb)
    return NULL;
  if (unlikely(is_link_local(dest))) {
    /* Pause frames shouldn't be passed up by driver anyway */
    if (skb->protocol == htons(ETH_P_PAUSE))
      goto drop;
    //如果该数据包的目标地址为STP协议的组播地址,并且该网桥启用STP功能,
    //则,结束该数据包的处理,它将会在第(2)处理得到处理
    if (p->br->stp_enabled != BR_NO_STP) {
      if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
                  NULL, br_handle_local_finish))
        return NULL;
      else
        return skb;
    }
    // 如果该包是发往网桥组播的,但该网桥没有启用STP功能,则在下面处理,
    // 并返回已处理的标识(返回NULL)来通知代码(2)处不需再处理。
  }
  switch (p->state) {
    case BR_STATE_FORWARDING:
      rhook = rcu_dereference(br_should_route_hook);
      if (rhook != NULL) {
        if (rhook(skb))
          // 如果该接口处于Forwarding状态,并且该报文必需要走L3层
          // 进行转发,则直接返回,让代码(2)进行处理。
          // br_should_route_hook钩子函数在ebtable里面设置为ebt_broute函数,
          //它根据用户的规则来决定该报文是否要能通过L3来转发。
          return skb;
        dest = eth_hdr(skb)->h_dest;
     }
      /* fall through */
    case BR_STATE_LEARNING:
      if (!compare_ether_addr(p->br->dev->dev_addr, dest))
        //当用内核创建一个网桥的同时也会创建一个虚拟的网络设备,它的名字
        //为网桥的名字,保存在p->br->dev指针里。P->br->dev和port_list里面的
        //接口共同组成一个网桥。如果该报文是要发往此接,则标记skb->pkt_type为
        //PACKET_HOST。因为报文最终是要发送到p->br->dev的输送队列里面,
        //正如一般的网卡驱动程序将数据包送往到某个net_device的输入队列一样,
        //这样bridge功能充当了虚拟网卡(如例子中的br0)驱动,应当设置
        //skb->pkt_type
        //为PACKET_HOST,表明数据包是要发送该接口,而非是因为打开混杂模式
        //而接收到的。
        skb->pkt_type = PACKET_HOST;
        // 接着由br_handle_frame_finish函数继续处理。
        NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
                br_handle_frame_finish);
      break;
  default:
    //其它状态下的端口,不能处理数据包,直接丢弃。
  drop:
    kfree_skb(skb);
  }
  // 该数据包要么被网桥处理了,要么处理时出错,不需要上层协议处理,
  // 返回NULL,代码(2)处不会处理该报文。
  return NULL;

1.6  br_handle_frame_finish函数
[linux-2.6.24.4/net/bridge/br_input.c]
int br_handle_frame_finish(struct sk_buff *skb)   {   
  const unsigned char *dest = eth_hdr(skb)->h_dest;   
  struct net_bridge_port *p = rcu_dereference(skb->dev->br_port);   
  struct net_bridge *br;   
  struct net_bridge_fdb_entry *dst;   
  struct sk_buff *skb2;   
  if (!p || p->state == BR_STATE_DISABLED)   
    goto drop;   
  //对所有报的源MAC地址进行学习,这是网桥的特点之一,   
  //通过对源地址的学习来建立MAC地址到端口的映射。   
  br = p->br;   
  br_fdb_update(br, p, eth_hdr(skb)->h_source);   
  if (p->state == BR_STATE_LEARNING)   
    goto drop;   
  // 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->statistics.multicast++;   
    skb2 = skb;   
  } else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {   
    // __br_fdb_get函数先查MAC-端口映射表,这一步是网桥的关键。   
    // 这个报文应从哪个接口转发出去就看它了。   
    // 如果这个报文应发往本机,那么skb置空。不需要再转发了,   
    // 因为发往本机接口从逻辑上来说本身就是一个转发。   
    skb2 = skb;   
    skb = NULL;   
  }   
  if (skb2 == skb)   
    skb2 = skb_clone(skb, GFP_ATOMIC);   
  // skb2不为空,表明要发往本机,br_pass_frame_up函数来完成发往   
  // 本机的工作。   
  if (skb2)   
    br_pass_frame_up(br, skb2);   
  if (skb) {   
    if (dst)   
      // 由br_forward函数从dst所指向的端口将该报文发出去。   
      br_forward(dst->dst, skb);   
    else  
      // 此报文是广播或组播报文,由br_flood_forward函数把报文向所有   
      // 端口转发出去。   
      br_flood_forward(br, skb);   
  }   
  out:   
    return 0;   
  drop:   
    kfree_skb(skb);   
  goto out;   
}   
int br_handle_frame_finish(struct sk_buff *skb) {
  const unsigned char *dest = eth_hdr(skb)->h_dest;
  struct net_bridge_port *p = rcu_dereference(skb->dev->br_port);
  struct net_bridge *br;
  struct net_bridge_fdb_entry *dst;
  struct sk_buff *skb2;
  if (!p || p->state == BR_STATE_DISABLED)
    goto drop;
  //对所有报的源MAC地址进行学习,这是网桥的特点之一,
  //通过对源地址的学习来建立MAC地址到端口的映射。
  br = p->br;
  br_fdb_update(br, p, eth_hdr(skb)->h_source);
  if (p->state == BR_STATE_LEARNING)
    goto drop;
  // 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->statistics.multicast++;
    skb2 = skb;
  } else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {
    // __br_fdb_get函数先查MAC-端口映射表,这一步是网桥的关键。
    // 这个报文应从哪个接口转发出去就看它了。
    // 如果这个报文应发往本机,那么skb置空。不需要再转发了,
    // 因为发往本机接口从逻辑上来说本身就是一个转发。
    skb2 = skb;
    skb = NULL;
  }
  if (skb2 == skb)
    skb2 = skb_clone(skb, GFP_ATOMIC);
  // skb2不为空,表明要发往本机,br_pass_frame_up函数来完成发往
  // 本机的工作。
  if (skb2)
    br_pass_frame_up(br, skb2);
  if (skb) {
    if (dst)
      // 由br_forward函数从dst所指向的端口将该报文发出去。
      br_forward(dst->dst, skb);
    else
      // 此报文是广播或组播报文,由br_flood_forward函数把报文向所有
      // 端口转发出去。
      br_flood_forward(br, skb);
  }
  out:
    return 0;
  drop:
    kfree_skb(skb);
  goto out;

1.7  通过br_pass_frame_up函数将报文发往本机接口。
[linux-2.6.24.4/net/bridge/br_input.c]
static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb)   {   
  struct net_device *indev;   
  br->statistics.rx_packets++;   
  br->statistics.rx_bytes += skb->len;   
  indev = skb->dev;   
  skb->dev = br->dev;   
  //br->dev是一个虚拟的网络设备,这是网桥局域网通往本机的必经之道。   
  //请注意,br->dev是本机和网桥相连的接口。当报文经网桥处理后,发现   
  //该报文应该发往本机,那就使用netif_receive_skb函数将该报文向上层   
  //协议投递。并且要将skb->dev设置为本机接口即br->dev,并且所有数据在   
  //它的入口接口indev的驱动中已处理完毕,因此可直接通知上层协议来处理。   
  NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,   
          netif_receive_skb);   
}   
static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb) {
  struct net_device *indev;
  br->statistics.rx_packets++;
  br->statistics.rx_bytes += skb->len;
  indev = skb->dev;
  skb->dev = br->dev;
  //br->dev是一个虚拟的网络设备,这是网桥局域网通往本机的必经之道。
  //请注意,br->dev是本机和网桥相连的接口。当报文经网桥处理后,发现
  //该报文应该发往本机,那就使用netif_receive_skb函数将该报文向上层
  //协议投递。并且要将skb->dev设置为本机接口即br->dev,并且所有数据在
  //它的入口接口indev的驱动中已处理完毕,因此可直接通知上层协议来处理。
  NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,
          netif_receive_skb);

1.8  通过br_forward函数将报文从另一个端口转发出去
view plaincopy to clipboardprint?
void br_forward(const struct net_bridge_port *to, struct sk_buff *skb)   {   
  if (should_deliver(to, skb)) {   
    __br_forward(to, skb);   
    return;   
  }   
  kfree_skb(skb);   
}   
void br_forward(const struct net_bridge_port *to, struct sk_buff *skb) {
  if (should_deliver(to, skb)) {
    __br_forward(to, skb);
    return;
  }
  kfree_skb(skb);

        Should_deliver函数来测试是否应将该包转发出去,它由出口端的状态和报文的入口端口信息来决定,它的定义如下:
[linux-2.6.24.4/net/bridge/br_forward.c]
static inline int should_deliver(const struct net_bridge_port *p,   
                                 const struct sk_buff *skb)   {   
  //1) 入口端口和出口端口不能相同,如果是相同的话,那么源主机和目标   
  // 主机在同一端口的子网段中,也即源主机和目标主机在同一广播域里面,   
  // 目标主机和网桥都会同时收到该报文,因此网桥无需多此一举。   
  //2) 如果出口端口的状态不是Forwarding,则不能转发出去。如果一个网桥   
  // 没有启用STP功能,并且网络接口的状态为UP,那么它网桥端口的状态   
  // 为Forwarding。如果启用STP,每个端口都有一个严格的状态,规定那些   
  // 端口在什么情况下才能成为Forwarding状态,否则容易造成环路,产生   
  // 网络风暴。   
  return (skb->dev != p->dev && p->state == BR_STATE_FORWARDING);   
}   
static inline int should_deliver(const struct net_bridge_port *p,
                                 const struct sk_buff *skb) {
  //1) 入口端口和出口端口不能相同,如果是相同的话,那么源主机和目标
  // 主机在同一端口的子网段中,也即源主机和目标主机在同一广播域里面,
  // 目标主机和网桥都会同时收到该报文,因此网桥无需多此一举。
  //2) 如果出口端口的状态不是Forwarding,则不能转发出去。如果一个网桥
  // 没有启用STP功能,并且网络接口的状态为UP,那么它网桥端口的状态
  // 为Forwarding。如果启用STP,每个端口都有一个严格的状态,规定那些
  // 端口在什么情况下才能成为Forwarding状态,否则容易造成环路,产生
  // 网络风暴。
  return (skb->dev != p->dev && p->state == BR_STATE_FORWARDING);

      若报文的确需要转发,因为目标主机是在另一个子网段,而且没有其它网相连的网格端口可抵达该子网段(这里考虑到启用STP功能,如果搞不清楚可略过)。将调用__br_forward函数实施这一转发功能。
[linux-2.6.24.4/net/bridge/br_forward.c]
static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)   {   
  struct net_device *indev;   
  indev = skb->dev;   
  skb->dev = to->dev;   
  skb_forward_csum(skb);   
  // 通过br_forward_finish函数最终完成转发功能   
  NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,   
          br_forward_finish);   
}   
static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb) {
  struct net_device *indev;
  indev = skb->dev;
  skb->dev = to->dev;
  skb_forward_csum(skb);
  // 通过br_forward_finish函数最终完成转发功能
  NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,
          br_forward_finish);

[linux-2.6.24.4/net/bridge/br_forward.c]
int br_forward_finish(struct sk_buff *skb)    {   
  return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit);   
}   
   
int br_forward_finish(struct sk_buff *skb) {
  return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit);
}
 
     Br_dev_queue_push_xmit在调用dev_queue_xmit函数前做些必要的检查工作。例如,报文的长度比出口端口的MTU还大,则丢掉该报文。
[linux-2.6.24.4/net/bridge/br_forward.c]
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 {   
      // 网桥在处理数据包里,只需拆包来获得目标MAC地址,而不需要   
      // 更改数据包的任何内容。但在入口网卡的驱动中已将以太网头部   
      // 剥掉,现在需要将它套上。Skb_push函数实现这一功能。   
      skb_push(skb, ETH_HLEN);   
      // 放到网卡输出队列里,该网卡驱动将它送出去。   
      dev_queue_xmit(skb);   
    }   
  }   
  return 0;   

 
1.9  br_flood_forward 函数把报文转发到网桥所有出口端口
[linux-2.6.24.4/net/bridge/br_forward.c]
void br_flood_forward(struct net_bridge *br, struct sk_buff *skb)   {   
  br_flood(br, skb, __br_forward);   
}  
void br_flood_forward(struct net_bridge *br, struct sk_buff *skb) {
  br_flood(br, skb, __br_forward);
}
     __br_forward代码已在前面分析过,它从指定的出口端口转发该报文。
      而br_flood函数,把__br_forward函数作为回调函数,依次遍网桥的所有出口端,调用__br_forward函数将该报文转发出去。一 个广播报文从某一端口进入,应该其余的端口都应该转发出去,但入口端口就不需要了。下面的代码看似把报文从所有端口都转发一份,其实不 然,should_deliver会阻止这样的事情发生。
[linux-2.6.24.4/net/bridge/br_forward.c]
static void br_flood(struct net_bridge *br, struct sk_buff *skb,   
                     void (*__packet_hook)(const struct net_bridge_port *p,   
                     struct sk_buff *skb))   {   
  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->statistics.tx_dropped++;   
          kfree_skb(skb);   
          return;   
        }   
        __packet_hook(prev, skb2);   
      }   
      prev = p;   
    }   
  }   
  if (prev != NULL) {   
    __packet_hook(prev, skb);   
    return;   
  }   
  kfree_skb(skb);   
}   
static void br_flood(struct net_bridge *br, struct sk_buff *skb,
                     void (*__packet_hook)(const struct net_bridge_port *p,
                     struct sk_buff *skb)) {
  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->statistics.tx_dropped++;
          kfree_skb(skb);
          return;
        }
        __packet_hook(prev, skb2);
      }
      prev = p;
    }
  }
  if (prev != NULL) {
    __packet_hook(prev, skb);
    return;
  }
  kfree_skb(skb);

 
第二部分:网桥转发数据库的维护
      众所周知,网桥需要维护一个MAC地址-端口映射表,端口是指网桥自身提供的端口,而MAC地址是指与端口相连的另一端的MAC地址。当网桥收到一个报文 时,先获取它的源MAC,更新数据库,然后读取该报文的目标MAC地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端 口以外的所有端口转发。
2.1 数据库的创建和销毁
     数据库使用kmem_cache_create函数进行创建,使用kmem_cache_desctory进行销毁。
[linux-2.6.24.4/net/bridge/br_fdb.c]
 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;   
}   
   
 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;
}
 
[linux-2.6.24.4/net/bridge/br_fdb.c]
void br_fdb_fini(void)   {   
  kmem_cache_destroy(br_fdb_cache);   
}   
void br_fdb_fini(void) {
  kmem_cache_destroy(br_fdb_cache);



2.2 数据库更新
      当网桥收到一个数据包时,它会获取该数据的源MAC地址,然后对数据库进行更新。如果该MAC地址不在数库中,则创新一个数据项。如果存在,更新它的年龄。数据库使用hash表的结构方式,便于高效查询。下面是hash功能代码的分析:
[linux-2.6.24.4/net/bridge/br_fdb.c]
void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,   
                   const unsigned char *addr)   {   
  // br_mac_hash函数是hash表中的hash函数,具体算法过程可参阅该函数代码。   
  // br->hash就是数据库的hash表,每个hash值对应一个链表。数据库的每项为   
  // net_bridge_fdb_entry结构。   
  struct hlist_head *head = &br->hash[br_mac_hash(addr)];   
  struct net_bridge_fdb_entry *fdb;   
  /* some users want to always flood. */  
  if (hold_time(br) == 0)   
    return;   
  /* ignore packets unless we are using this port */  
  if (!(source->state == BR_STATE_LEARNING ||   
        source->state == BR_STATE_FORWARDING))   
    return;   
  fdb = fdb_find(head, addr);   
  if (likely(fdb)) {   
    // 接收到的MAC地址竟然是自己端口的MAC地址,确实不应该有这样的   
    // 事情发生。   
    if (unlikely(fdb->is_local)) {   
      if (net_ratelimit())   
        printk(KERN_WARNING "%s: received packet with "  
               " own address as source address\n",   
               source->dev->name);   
    } else {   
      // 收到该MAC地址的报文,更新它的年龄。   
      fdb->dst = source;   
      fdb->ageing_timer = jiffies;   
    }   
  } else {   
    spin_lock(&br->hash_lock);   
    if (!fdb_find(head, addr))   
      // 这是新的MAC地址,在数据库里为之创建一个数据项。   
      fdb_create(head, source, addr, 0);   
   /* else we lose race and someone else inserts  
    * it first, don't bother updating  
    */  
    spin_unlock(&br->hash_lock);   
  }   
}   
void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
                   const unsigned char *addr) {
  // br_mac_hash函数是hash表中的hash函数,具体算法过程可参阅该函数代码。
  // br->hash就是数据库的hash表,每个hash值对应一个链表。数据库的每项为
  // net_bridge_fdb_entry结构。
  struct hlist_head *head = &br->hash[br_mac_hash(addr)];
  struct net_bridge_fdb_entry *fdb;
  /* some users want to always flood. */
  if (hold_time(br) == 0)
    return;
  /* ignore packets unless we are using this port */
  if (!(source->state == BR_STATE_LEARNING ||
        source->state == BR_STATE_FORWARDING))
    return;
  fdb = fdb_find(head, addr);
  if (likely(fdb)) {
    // 接收到的MAC地址竟然是自己端口的MAC地址,确实不应该有这样的
    // 事情发生。
    if (unlikely(fdb->is_local)) {
      if (net_ratelimit())
        printk(KERN_WARNING "%s: received packet with "
               " own address as source address\n",
               source->dev->name);
    } else {
      // 收到该MAC地址的报文,更新它的年龄。
      fdb->dst = source;
      fdb->ageing_timer = jiffies;
    }
  } else {
    spin_lock(&br->hash_lock);
    if (!fdb_find(head, addr))
      // 这是新的MAC地址,在数据库里为之创建一个数据项。
      fdb_create(head, source, addr, 0);
   /* else we lose race and someone else inserts
    * it first, don't bother updating
    */
    spin_unlock(&br->hash_lock);
  }

 


2.3 创建数据项
     在更新函数里面已为某一MAC找到了它所属于的Hash链表,因此,创建函数只需要在该链上添加一个数据项即可。
[linux-2.6.24.4/net/bridge/br_fdb.c]
 static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,   
                                               struct net_bridge_port *source,   
                                               const unsigned char *addr,   
                                               int is_local)   {   
  struct net_bridge_fdb_entry *fdb;   
  fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC);   
  if (fdb) {   
    memcpy(fdb->addr.addr, addr, ETH_ALEN);   
    atomic_set(&fdb->use_count, 1);   
    hlist_add_head_rcu(&fdb->hlist, head);   
    fdb->dst = source;   
    fdb->is_local = is_local;   
    fdb->is_static = is_local;   
    fdb->ageing_timer = jiffies;   
  }   
  return fdb;   
}   
 static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,
                                               struct net_bridge_port *source,
                                               const unsigned char *addr,
                                               int is_local) {
  struct net_bridge_fdb_entry *fdb;
  fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC);
  if (fdb) {
    memcpy(fdb->addr.addr, addr, ETH_ALEN);
    atomic_set(&fdb->use_count, 1);
    hlist_add_head_rcu(&fdb->hlist, head);
    fdb->dst = source;
    fdb->is_local = is_local;
    fdb->is_static = is_local;
    fdb->ageing_timer = jiffies;
  }
  return fdb;

 
2.4 查找数据项
      网桥的数据项查找与一般的查找类似,但略有不同。前面提到,如果要更新一MAC地址,不管该地址是否已经过期了,只需遍历该MAC地址对应的Hash链 表,然后更新年龄,此时它肯定不过期了。但网桥要转发数据时,除了要找到该目标MAC的出口端口外,还要判断该记录是否过期了。因此,数据项的查找有两 种,一种用于更新,另一用于转发。
[linux-2.6.24.4/net/bridge/br_fdb.c]
static inline struct net_bridge_fdb_entry *fdb_find(struct hlist_head *head,   
                                                    const unsigned char *addr)   {   
  struct hlist_node *h;   
  struct net_bridge_fdb_entry *fdb;   
  hlist_for_each_entry_rcu(fdb, h, head, hlist) {   
  if (!compare_ether_addr(fdb->addr.addr, addr))   
    return fdb;   
  }   
  return NULL;   
}   
static inline struct net_bridge_fdb_entry *fdb_find(struct hlist_head *head,
                                                    const unsigned char *addr) {
  struct hlist_node *h;
  struct net_bridge_fdb_entry *fdb;
  hlist_for_each_entry_rcu(fdb, h, head, hlist) {
  if (!compare_ether_addr(fdb->addr.addr, addr))
    return fdb;
  }
  return NULL;

 [linux-2.6.24.4/net/bridge/br_fdb.c]
struct net_bridge_fdb_entry *__br_fdb_get(struct net_bridge *br,   
                                           const unsigned char *addr)   {   
  struct hlist_node *h;   
  struct net_bridge_fdb_entry *fdb;   
  hlist_for_each_entry_rcu(fdb, h, &br->hash[br_mac_hash(addr)], hlist) {   
    if (!compare_ether_addr(fdb->addr.addr, addr)) {   
      if (unlikely(has_expired(br, fdb)))   
        break;   
      return fdb;   
    }   
  }   
  return NULL;   
}   
struct net_bridge_fdb_entry *__br_fdb_get(struct net_bridge *br,
                                           const unsigned char *addr) {
  struct hlist_node *h;
  struct net_bridge_fdb_entry *fdb;
  hlist_for_each_entry_rcu(fdb, h, &br->hash[br_mac_hash(addr)], hlist) {
    if (!compare_ether_addr(fdb->addr.addr, addr)) {
      if (unlikely(has_expired(br, fdb)))
        break;
      return fdb;
    }
  }
  return NULL;

      除了__br_fdb_get函数多调用了has_expired外,其余无一不同。Has_expired函数来决定该数据项是否是过期的,代码如下:
[linux-2.6.24.4/net/bridge/br_fdb.c]
static inline int has_expired(const struct net_bridge *br,   
                              const struct net_bridge_fdb_entry *fdb)   {   
  // 如果该数据项是静态的,即不是学习过来的,它永远不会过期。因为它就是   
  // 网桥自己端口的地址。   
  // 如果现在时间,与该数据项的最近更新时间和可保留时间之和相等,   
  // 或者更早,则为过期。   
  return !fdb->is_static   
         && time_before_eq(fdb->ageing_timer + hold_time(br), jiffies);   
}   
// 数据项的可保留时间根据拓扑结构是否改变来决定,改变则为forward_delay,   
// 否则为ageing_time。   
static inline unsigned long hold_time(const struct net_bridge *br)   {   
  return br->topology_change ? br->forward_delay : br->ageing_time;   
}   
static inline int has_expired(const struct net_bridge *br,
                              const struct net_bridge_fdb_entry *fdb) {
  // 如果该数据项是静态的,即不是学习过来的,它永远不会过期。因为它就是
  // 网桥自己端口的地址。
  // 如果现在时间,与该数据项的最近更新时间和可保留时间之和相等,
  // 或者更早,则为过期。
  return !fdb->is_static
         && time_before_eq(fdb->ageing_timer + hold_time(br), jiffies);
}
// 数据项的可保留时间根据拓扑结构是否改变来决定,改变则为forward_delay,
// 否则为ageing_time。
static inline unsigned long hold_time(const struct net_bridge *br) {
  return br->topology_change ? br->forward_delay : br->ageing_time;



第三部分: ioctl管理网桥
3.1 通过ioctl系统调用创建网桥
     仍然以前的配置作为例,我们分用户空间程序brctl是如何通过ioctl系统调用在kernel空间内创建上述的数据结构。创建网桥,我们不需要预知任 何网络设备信息,因此我们通过ioctl来创建网桥时不应该与任何网络设备绑定到一起。网桥模块为此ioctl函数提供了一个恰如其分的名字 br_ioctl_deviceless_stub。Brctl工具使用的ioctl系统调用最终会调用此函数,它相关代码如下:
[linux-2.6.24.4/net/bridge/br.c]
brioctl_set(br_ioctl_deviceless_stub);
[linux-2.6.24.4/net/socket.c]
void brioctl_set(int (*hook) (struct net *, unsigned int, void __user *))   {   
  mutex_lock(&br_ioctl_mutex);   
  br_ioctl_hook = hook;   
  mutex_unlock(&br_ioctl_mutex);   
}   
void brioctl_set(int (*hook) (struct net *, unsigned int, void __user *)) {
  mutex_lock(&br_ioctl_mutex);
  br_ioctl_hook = hook;
  mutex_unlock(&br_ioctl_mutex);

        用户空间程序使用网桥相关的命令来调用ioctl函数时,它经kernel依据命令所属的分类分派到sock_ioctl函数。在sock_ioctl函 数里面,当ioctl命令为SIOCGIFBR,SIOCSIFBR, SIOCBRADDBR 和SIOCBRDELBR,它将ioctl的请求转发到br_ioctl_deviceless_stub函数。
Br_ioctl_deviceless_stub函数代码和分析如下:
[linux-2.6.24.4/net/bridge/br_ioctl.c]
int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg)   {   
  switch (cmd) {   
    case SIOCGIFBR:   
    case SIOCSIFBR:   
      // 这两个网桥命令是比较老式的,我们在这里不作讨论   
      return old_deviceless(uarg);   
    // 新式的网桥ioctl命令有两个,添加新网桥和删除现有的网桥   
    // 需要用户空间提供网桥的名字。   
    case SIOCBRADDBR:   
    case SIOCBRDELBR:   
    {   
      char buf[IFNAMSIZ];   
      if (!capable(CAP_NET_ADMIN))   
        return -EPERM;   
      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;   
}   
int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg) {
  switch (cmd) {
    case SIOCGIFBR:
    case SIOCSIFBR:
      // 这两个网桥命令是比较老式的,我们在这里不作讨论
      return old_deviceless(uarg);
    // 新式的网桥ioctl命令有两个,添加新网桥和删除现有的网桥
    // 需要用户空间提供网桥的名字。
    case SIOCBRADDBR:
    case SIOCBRDELBR:
    {
      char buf[IFNAMSIZ];
      if (!capable(CAP_NET_ADMIN))
        return -EPERM;
      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;

      该函数调用br_add_bridge和br_del_brdge函数的实现新建和删除网桥的功能。由于这两个函数所完成的事情刚好相反,在此,我们只讨论br_add_bridge的代码:
[linux-2.6.24.4/net/bridge/br_if.c]
int br_add_bridge(const char *name)   {   
  struct net_device *dev;   
  int ret;   
  // 创建网桥的核心工作,创建一个与网桥同名的网络设备。   
  // 可以通过该设备分配的IP地址来管理该网桥。 同时该设备   
  // 是虚拟的设备,它的接收包和发送包处理函数与一般的真实网卡   
  // 设备不同。   
  dev = new_bridge_dev(name);   
  if (!dev)   
    return -ENOMEM;   
  rtnl_lock();   
  if (strchr(dev->name, '%')) {   
    ret = dev_alloc_name(dev, dev->name);   
    if (ret < 0) {   
      free_netdev(dev);   
      goto out;   
    }   
  }   
  // 向kernel注册该网桥设备,这样在用户空间就以使用   
  // ifconfig来为之分配IP,或通ioctl来对该网桥添加新的接口。   
  ret = register_netdevice(dev);   
  if (ret)   
    goto out;   
  ret = br_sysfs_addbr(dev);   
  if (ret)   
    unregister_netdevice(dev);   
out:   
  rtnl_unlock();   
  return ret;   
}   
int br_add_bridge(const char *name) {
  struct net_device *dev;
  int ret;
  // 创建网桥的核心工作,创建一个与网桥同名的网络设备。
  // 可以通过该设备分配的IP地址来管理该网桥。 同时该设备
  // 是虚拟的设备,它的接收包和发送包处理函数与一般的真实网卡
  // 设备不同。
  dev = new_bridge_dev(name);
  if (!dev)
    return -ENOMEM;
  rtnl_lock();
  if (strchr(dev->name, '%')) {
    ret = dev_alloc_name(dev, dev->name);
    if (ret < 0) {
      free_netdev(dev);
      goto out;
    }
  }
  // 向kernel注册该网桥设备,这样在用户空间就以使用
  // ifconfig来为之分配IP,或通ioctl来对该网桥添加新的接口。
  ret = register_netdevice(dev);
  if (ret)
    goto out;
  ret = br_sysfs_addbr(dev);
  if (ret)
    unregister_netdevice(dev);
out:
  rtnl_unlock();
  return ret;

      现在创建网桥设备的任务落到new_bridge_dev的身上。New_bridge_dev函数的功能与一般的网卡驱动初化为代码非常类似的。因为这里段代就创建一个网桥设备,从这个层面来说,这段代码也算是驱动代码,结构和真实驱动非常类似。
[linux-2.6.24.4/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结构,它的priv数据为net_bridge结构体。   
  // br_dev_setup函数初化了net_device结构的很多函数指针。   
  dev = alloc_netdev(sizeof(struct net_bridge), name,   
                     br_dev_setup);   
  if (!dev)   
    return NULL;   
  br = netdev_priv(dev);   
  br->dev = dev;   
  spin_lock_init(&br->lock);   
  INIT_LIST_HEAD(&br->port_list);   
  spin_lock_init(&br->hash_lock);   
  br->bridge_id.prio[0] = 0x80;   
  br->bridge_id.prio[1] = 0x00;   
  ….   
  return dev;   
}   
static struct net_device *new_bridge_dev(const char *name) {
  struct net_bridge *br;
  struct net_device *dev;
  // 分配net_device结构,它的priv数据为net_bridge结构体。
  // br_dev_setup函数初化了net_device结构的很多函数指针。
  dev = alloc_netdev(sizeof(struct net_bridge), name,
                     br_dev_setup);
  if (!dev)
    return NULL;
  br = netdev_priv(dev);
  br->dev = dev;
  spin_lock_init(&br->lock);
  INIT_LIST_HEAD(&br->port_list);
  spin_lock_init(&br->hash_lock);
  br->bridge_id.prio[0] = 0x80;
  br->bridge_id.prio[1] = 0x00;
  ….
  return dev;

 [linux-2.6.24.4/net/bridge/br_device.c]
void br_dev_setup(struct net_device *dev)   {   
  // 为该网桥设备随机分配MAC地址   
  random_ether_addr(dev->dev_addr);   
  // 初始化dev的部分函数指针,因为目前网桥设备主适用于以及网   
  // 以太网的部分功能对它也适用。   
  ether_setup(dev);   
  // 设置设备的ioctl函数为br_dev_ioctl。下面可以看到通过该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_ETHTOOL_OPS(dev, &br_ethtool_ops);   
  dev->stop = br_dev_stop;   
  dev->tx_queue_len = 0;   
  dev->set_mac_address = br_set_mac_address;   
  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;   
}   
void br_dev_setup(struct net_device *dev) {
  // 为该网桥设备随机分配MAC地址
  random_ether_addr(dev->dev_addr);
  // 初始化dev的部分函数指针,因为目前网桥设备主适用于以及网
  // 以太网的部分功能对它也适用。
  ether_setup(dev);
  // 设置设备的ioctl函数为br_dev_ioctl。下面可以看到通过该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_ETHTOOL_OPS(dev, &br_ethtool_ops);
  dev->stop = br_dev_stop;
  dev->tx_queue_len = 0;
  dev->set_mac_address = br_set_mac_address;
  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;

 
3.2 通过ioctl系统调用为网桥添加端口
        仅仅创建网桥,还是不够的。实际应用中的网桥需要添加实际的端口(即物理接口),如例子中的eth1, eth2等。应用程序在使用ioctl来为网桥增加物理接口,br_dev_ioctl的代码和分析如下:
[linux-2.6.24.4/net/bridge/br_ioctl.c]
// dev 为网桥接口,ifreq 为添加/删除的物理接口的参数   
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;   
}   
// dev 为网桥接口,ifreq 为添加/删除的物理接口的参数
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函数来控制网桥的物理接口,该函数的代码和分析如下:
[linux-2.6.24.4/net/bridge/br_ioctl.c]
// br 网桥,ifindex 添加/删除物理接口的index   
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(&init_net, 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 网桥,ifindex 添加/删除物理接口的index
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(&init_net, 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中,出于讨论的方便,我们只分析br_add_if函数。
[linux-2.6.24.4/net/bridge/br_if.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;   
  // 为该接口创建一个网桥端口数据,并初始化好该端口的相关   
  // 数据,详情可参阅该函数代码。   
  p = new_nbp(br, dev);   
  if (IS_ERR(p))   
    return PTR_ERR(p);   
  err = kobject_add(&p->kobj);   
  if (err)   
    goto err0;   
  // 将该接口的物理地址写入到 MAC-端口映射表中。   
  // 该MAC是属于网桥内部端口的固定MAC地址,   
  // 它在fdb中的记录是固定的,不会失效(agged)   
  err = br_fdb_insert(br, p, dev->dev_addr);   
  if (err)   
    goto err1;   
  err = br_sysfs_addif(p);   
    
  if (err)   
    goto err2;   
  rcu_assign_pointer(dev->br_port, p);   
  // 打开该接口的混杂模式,网桥中的各个端口必须处于   
  // 混杂模式,网桥才能正确工作。   
  dev_set_promiscuity(dev, 1);   
  // 加到端口列表   
  list_add_rcu(&p->list, &br->port_list);   
  spin_lock_bh(&br->lock);   
  br_stp_recalculate_bridge_id(br);   
  br_features_recompute(br);   
  if ((dev->flags & IFF_UP) && netif_carrier_ok(dev) &&   
      (br->dev->flags & IFF_UP))   
    br_stp_enable_port(p);   
  spin_unlock_bh(&br->lock);   
  br_ifinfo_notify(RTM_NEWLINK, p);   
  dev_set_mtu(br->dev, br_min_mtu(br));   
  kobject_uevent(&p->kobj, KOBJ_ADD);   
  return 0;   
err2:   
  br_fdb_delete_by_port(br, p, 1);   
err1:   
  kobject_del(&p->kobj);   
err0:   
  kobject_put(&p->kobj);   
  return err;   
}  
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;
  // 为该接口创建一个网桥端口数据,并初始化好该端口的相关
  // 数据,详情可参阅该函数代码。
  p = new_nbp(br, dev);
  if (IS_ERR(p))
    return PTR_ERR(p);
  err = kobject_add(&p->kobj);
  if (err)
    goto err0;
  // 将该接口的物理地址写入到 MAC-端口映射表中。
  // 该MAC是属于网桥内部端口的固定MAC地址,
  // 它在fdb中的记录是固定的,不会失效(agged)
  err = br_fdb_insert(br, p, dev->dev_addr);
  if (err)
    goto err1;
  err = br_sysfs_addif(p);
 
  if (err)
    goto err2;
  rcu_assign_pointer(dev->br_port, p);
  // 打开该接口的混杂模式,网桥中的各个端口必须处于
  // 混杂模式,网桥才能正确工作。
  dev_set_promiscuity(dev, 1);
  // 加到端口列表
  list_add_rcu(&p->list, &br->port_list);
  spin_lock_bh(&br->lock);
  br_stp_recalculate_bridge_id(br);
  br_features_recompute(br);
  if ((dev->flags & IFF_UP) && netif_carrier_ok(dev) &&
      (br->dev->flags & IFF_UP))
    br_stp_enable_port(p);
  spin_unlock_bh(&br->lock);
  br_ifinfo_notify(RTM_NEWLINK, p);
  dev_set_mtu(br->dev, br_min_mtu(br));
  kobject_uevent(&p->kobj, KOBJ_ADD);
  return 0;
err2:
  br_fdb_delete_by_port(br, p, 1);
err1:
  kobject_del(&p->kobj);
err0:
  kobject_put(&p->kobj);
  return err;
}
 
第四部分: 总结

    网桥是2层的网格连接设备,它工作在协议栈的第二层。本文以简单的例子作为基础,分析网桥处理报文,更新MAC-端口映射表,和如何控制网桥和端口的功 能。文中帖上了大量的关键代码,并以代码加上注释这种贴近程序员的方式来分析代码。对于缺少kernel网络编程经验的朋友,在某些代码处,写了在背景知 识的分析和解释。



参考:

http://www.ibm.com/developerworks/cn/linux/kernel/l-netbr/index.html

一、Linux内核网桥的实现分析

Linux 内核分别在2.2 和 2.4内核中实现了网桥。但是2.2 内核和 2.4内核的实现有很大的区别,2.4中的实现几乎是全部重写了所有的实现代码。本文以2.4.0内核版本为例进行分析。

在分析具体的实现之前,先描述几个概念,有助于对网桥的功能及实现有更深的理解。

  1. 冲突域

    一个冲突域由所有能够看到同一个冲突或者被该冲突涉及到的设备组成。以太网使用C S M A / C D(Carrier Sense Multiple Access with Collision Detection,带有冲突监测的载波侦听多址访问)技术来保证同一时刻,只有一个节点能够在冲突域内传送数据。网桥或者交换机,构成了一个冲突域的边界。缺省情况下,网桥中的每个端口实际上就是一个冲突域的结束点。

  2. 广播域

    一个广播域由所有能够看到一个广播数据包的设备组成。一个路由器,构成一个广播域的边界。网桥能够延伸到的最大范围就是一个广播域。缺省的情况下,一个网桥或交换机的所有端口在同一个广播域中。VLAN技术可以把交换机或者网桥的不同端口分割成不同的广播域。一般情况下, 一个广播域代表一个逻辑网段。

  3. 网桥中的CAM表

    网桥和交换机一样,为了能够实现对数据包的转发,网桥保存着许多(MAC,端口)项。所有的这些项组成一个表,叫做CAM表。每个项有超时机制,如果一定时间内未接收到以这个MAC为源MAC地址的数据包,这个项就会被删除。

图1:一个交换网络的逻辑图
图1:一个交换网络的逻辑图

在Linux内核网桥的实现中,一个逻辑网段用net_bridge结构体表示。一个逻辑网段需要保留的信息有:

  1. 本逻辑网段中所有的端口(port_list)

    每个端口用net_bridge_port结构体来表示,从net_bridge_port结构体中可以看出,它主要有:

    1. 逻辑网段中的下一个端口(next)
    2. 本端口所属的逻辑网段(br)
    3. 本端口所指向的物理网卡(dev)
    4. 本端口在网桥中的编号(port_no)
    5. 用于生成树管理的信息
  2. 一个逻辑网段中可以具有很多个端口,所有的端口都挂在以port_list为链表头的链表上。

    本网段中CAM表(hash[BR_HASH_SIZE])

    CAM表中的每个项用net_bridge_fdb_entry结构体代表,每项中有:

    1. 用于CAM表连接的链表指针(next_hash,pprev_hash)
    2. 此项当前的引用计数(use_count)
    3. MAC地址(addr)
    4. 此项所对应的端口(dst)
    5. 处理MAC超时(ageing_timer)
    6. 是否是本机的MAC地址(is_local)
    7. 是否是静态MAC地址(is_static)
  3. 一个逻辑网段中的所有表项形成一个CAM表,他们之间的组织关系是一个HASH链表。HASH链的个数为BR_HASH_SIZE(256)。

    本逻辑网段用于和外部通信的虚拟网络设备(dev)

    Linux网桥可以在网桥上为每个逻辑网段配置一个IP,用于和外部通信。实际上这个IP不是配置在一个特定的物理网卡上面, 而是建立一个虚拟的网卡,虚拟网卡可以附在每个同一逻辑网段的物理网卡上,让这个网卡可以象所有的物理网卡一样工作。从而使网桥可以和外部通信。

  4. 本逻辑网段虚拟网卡的统计数据(statistics)

    按照Linux网卡驱动的接口,一个网卡的统计信息是由每个网卡的私有数据处理的。一般的写法是用dev->priv来指向每个网卡的统计数据。网卡的get_stats方法就是用来读取统计数据。

  5. 用户一个网段的生成树(STP)信息

以上对几个结构体的描述和分析可以通过下图来表示:

图2:Linux网桥数据结构描述图
图2:Linux网桥数据结构描述图

描述了网桥的数据结构后,就可以开始数据包处理流程的分析。

网桥处理包遵循着以下几条原则:

  1. 在一个接口上接收到的包不会再在那个接口上发送这个数据包。
  2. 每个接收到的数据包都要学习其源MAC地址。
  3. 如果数据包是多播包或广播包,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包,如果上层协议栈对多播包感兴趣,则需要把数据包提交给上层协议栈。
  4. 如果数据包的目的MAC地址不能在CAM表中找到,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包。
  5. 如果能够在CAM表中查询到目的MAC地址,则在特定的端口上发送这个数据包,如果发送端口和接收端口是同一端口,则不发送。

在网络软中断处理函数net_rx_action中,嵌入了handle_bridge用于把数据包skb送入网桥模块处理。

#if defined(CONFIG_BRIDGE) || defined(CONFIG_BRIDGE_MODULE)
			if (skb->dev->br_port != NULL &&
			    br_handle_frame_hook != NULL) {
				handle_bridge(skb, pt_prev);
				dev_put(rx_dev);
				continue;
			}
#endif

br_handle_frame_hook是网桥处理接收到数据包的中入口,网桥初始化(br_init)的时候,把br_handle_frame_hook赋值为br_handle_frame。skb->dev->br_port用于判断接收到这个数据包的接口是否是网桥中的一个端口,如果是,skb->dev->br_port不为NULL,那么数据包应该由网桥处理。反之,数据包由上层协议栈处理。网桥中虚拟网卡对应的数据包就是在这个判断点时不再进入网桥。(实际上虚拟网卡并不会自己主动接收数据包,而是在网桥处理中把数据包向本地上层协议栈提交,并且修改了skb->dev,使得数据包不会多次进入桥处理代码)。

前面提到,网桥处理接收包的入口是br_handle_frame(net/bridge/br_input.c)函数。

br_handle_frame函数首先从skb中获得这个包属于的逻辑网段。然后调用__br_handle_frame进行转发处理。 br_handle_frame函数里有一个值得了解的地方,里面有一个加读锁。因为在转发中需要读CAM表,所以必须加读锁,避免在这个过程中另外的内核控制路径(如多处理机上另外一个CPU上的系统调用)修改CAM表。

对输入包的转发决策都是在__br_handle_frame函数中。这个函数的处理可以分为以下几个部分:

  1. 如果网桥的虚拟网卡处于混杂模式,那么每个接收到的数据包都需要克隆一份送到AF_PACKET协议处理体(网络软中断函数net_rx_action中ptype_all链的处理)。 

    if (br->dev.flags & IFF_PROMISC) {
    		struct sk_buff *skb2;
    		skb2 = skb_clone(skb, GFP_ATOMIC);
    		if (skb2) {
    			passedup = 1;
    			br_pass_frame_up(br, skb2);
    		}
    	}
  2. 如果源MAC地址是多播或者是广播地址,那么这个包格式是错误的,简单的丢弃。 

    		if (skb->mac.ethernet->h_source[0] & 1)
    		goto freeandout;
  3. 如果是一个多播包,则需要向本机的上层协议栈传送这个数据包(如果在之前没有向上提交的话,即passedup为0。如果为1,则前面已经发送了,现在就不需要提交了,在后面中的处理都是一样的)。 

    	if (!passedup &&
    	    (dest[0] & 1) &&
    	    (br->dev.flags & IFF_ALLMULTI || br->dev.mc_list != NULL)) {
    		struct sk_buff *skb2;
    		skb2 = skb_clone(skb, GFP_ATOMIC);
    		if (skb2) {
    			passedup = 1;
    			br_pass_frame_up(br, skb2);
    		}
    	}
  4. 如果启动了生成树协议,一个生成树包需要由生成树协议处理模块单独处理。如果不支持,则这个包的目的MAC肯定在CAM中查询不到,所以是向所有的端口发送(除接收口)。这样才不会影响整个网络的生成树协议运行。 

    		if (br->stp_enabled &&
    	    !memcmp(dest, bridge_ula, 5) &&
    	    !(dest[5] & 0xF0))
    		goto handle_special_frame;
  5. 如果接收端口不是处于LEARNING或者FORWARDING,那么就学习这个包的源MAC地址,或者更新CAM表中相应项的定时器。 

    if (p->state == BR_STATE_LEARNING ||
    	    p->state == BR_STATE_FORWARDING)
    		br_fdb_insert(br, p, skb->mac.ethernet->h_source, 0);
  6. 如果是一个多播包或广播包,则调用br_flood函数向每个口发送(除接收口)这个数据包。如果之前没有提交上层协议,则需要克隆一个包提交上层协议。 

    	if (dest[0] & 1) {
    		br_flood(br, skb, 1);
    		if (!passedup)
    			br_pass_frame_up(br, skb);
    		else
    			kfree_skb(skb);
    		return;
    	}
  7. 用接收到数据包的目的MAC地址查询CAM表。 

    dst = br_fdb_get(br, dest);
  8. 查询CAM表后,如果能够找到表项,并且目的MAC是到本机的虚拟网卡的,那么就需要把这个包提交给上层协议。网桥就是通过这个地方的处理和外部通信,实现远程管理的目的。 

    	if (dst != NULL && dst->is_local) {
    		if (!passedup)
    			br_pass_frame_up(br, skb);
    		else
    			kfree_skb(skb);
    		br_fdb_put(dst);
    		return;
    	}
  9. 如果查询CAM表有结果,并且目的MAC不是到本地的,那么就通过调用br_forward发送到特定的端口。 

    	if (dst != NULL) {
    		br_forward(dst->dst, skb);
    		br_fdb_put(dst);
    		return;
    	}
  10. 如果在CAM表中查询不到数据包的目的MAC地址,那么就需要向别的每个端口发送这个数据包。调用br_flood来进行这个处理。 

    	br_flood(br, skb, 0);
    	return;

在br_forward和br_flood函数中都必须判断源接口和目的接口是否是同一个,如果是同一端口,就不发送这个数据包。数据包的最后发送都是通过统一的发送接口dev_queue_xmit函数来完成的。

以下就是数据包的处理流程:

图3:数据包处理流程图
图3:数据包处理流程图

前面多次提到网桥的虚拟网卡,实际上在网桥中,这个网卡存在着一个net_device结构(在net_bridge里),但是不存在着实际的物理设备,而是附在网桥中每个物理网卡上面。这个虚拟网卡的支持函数在(br_device.c)。因为是虚拟的网卡,所以没有物理中断产生,每个需要发送到这个设备的数据包都是靠判断数据包的目的MAC地址来决定是否需要提交到本地上层协议栈(在__br_handle_frame判断)。

如果数据包需要向上层协议提交,都调用br_pass_frame_up函数来处理。在这个函数中,首先把skb->dev设置成br->dev。然后再模拟在中断中处理数据包一样,进行相应的处理, 然后调用netif_rx放入接收队列。这里有一个要十分注意的地方,这个数据包的skb->dev已经变成br->dev。所以在网络接收软中断处理函数net_rx_action中不会再次进入handle_bridge了。

static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb)
{
	br->statistics.rx_packets++;
	br->statistics.rx_bytes += skb->len;
	skb->dev = &br->dev;
	skb->pkt_type = PACKET_HOST;
	skb_pull(skb, skb->mac.raw - skb->data);
	skb->protocol = eth_type_trans(skb, &br->dev);
	netif_rx(skb);
}

二、配置内核 2.4 Linux 网桥

要配置网桥,首先需要网桥的配置工具bridge-utils。这个配置程序的源代码可以在 http://bridge.sourceforge.net/bridge-utils/ 下载。编译成功之后,就可以生成网桥配置的主要工具brctl。

下面,我们将用brctl对以下网络拓扑配置网桥,使Linux能够对数据包进行交换。

上图中,有五台主机。其中中间那台主机装有linux ,安装了网桥模块,而且有四块物理网卡,分别连接同一网段的其他主机。我们希望其成为一个网桥,为其他四台主机(IP分别为192.168.1.2 ,192.168.1.3,192.168.1.4,192.168.1.5) 之间转发数据包。同时,为了方便管理,希望网桥能够有一个IP(192.168.1.1),那样管理员就可以在192.168.1.0/24网段内的主机上telnet到网桥,对其进行配置,实现远程管理。

前一节中提到,网桥在同一个逻辑网段转发数据包。针对上面的拓扑,这个逻辑网段就是192.168.1.0/24网段。我们为这个逻辑网段一个名称,br_192。首先需要配置这样一个逻辑网段。

# brctl addbr br_192			(建立一个逻辑网段,名称为br_192)

实际上,我们可以把逻辑网段192.168.1.0/24看作使一个VLAN ,而br_192则是这个VLAN的名称。

建立一个逻辑网段之后,我们还需要为这个网段分配特定的端口。在Linux中,一个端口实际上就是一个物理网卡。而每个物理网卡的名称则分别为eth0,eth1,eth2,eth3。我们需要把每个网卡一一和br_192这个网段联系起来,作为br_192中的一个端口。

# brctl addif br_192 eth0			(让eth0成为br_192的一个端口)
# brctl addif br_192 eth1			(让eth1成为br_192的一个端口)
# brctl addif br_192 eth0			(让eth2成为br_192的一个端口)
# brctl addif br_192 eth3			(让eth3成为br_192的一个端口)

网桥的每个物理网卡作为一个端口,运行于混杂模式,而且是在链路层工作,所以就不需要IP了。

# ifconfig eth0 0.0.0.0
# ifconfig eth1 0.0.0.0
# ifconfig eth2 0.0.0.0
# ifconfig eth3 0.0.0.0

然后给br_192的虚拟网卡配置IP:192.168.1.1。那样就能远程管理网桥。

# ifconfig br_192 192.168.1.1

给br_192配置了IP之后,网桥就能够工作了。192.168.1.0/24网段内的主机都可以telnet到网桥上对其进行配置。

以上配置的是一个逻辑网段,实际上Linux网桥也能配置成多个逻辑网段(相当于交换机中划分多个VLAN)。具体的方法可以参考bridge-util中的HOWTO。


三、总结

本文分析了Linux网桥的实现,并且举例说明如何配置网桥。 通过学习网桥的实现,就能够了解网络中二层交换的原理。

网桥和交换机的功能非常相似,所以在分析网桥的时候,绝大多数情况下可以用交换机的处理方法来分析网桥的动作。



http://zhumeng8337797.blog.163.com/blog/static/1007689142011643834429/

一、什么是桥接

             桥接工作在OSI网络参考模型的第二层数据链路层,是一种 以MAC地址来作为判断依据来将网络划分成两个不同物理段的技术,其被广泛应用于早期的计算机网络当中。
              我们都知道,以太网是一种共享网络传输介质的技术,在这种技术下,如果一台计算机发送数据的时候,在同一物理网络介质上的计算机都需要接收,在接收后分析目的MAC地址,如果是属于目的MAC地址和自己的MAC地址相同便进行封装提供给网络层,如果目的MAC地址不是自己的MAC地址,那么就丢弃数据包。
             桥接的工作机制是将物理网络段(也就是常说的冲突域)进行分隔,根据MAC地址来判断连接两个物理网段的计算机的数据包发送。
下面,我们举个例子来为各位网友讲解:在下图中的网络结构中,有两台集线器分别连接多台计算机,我们分别将A集线器和B集线器定为A冲突域和B冲突域。在这样的网络环境中,如果计算机A向计算机C发送数据包时,集线器A会将数据包在整个网络中的全部计算机(包括集线器B)发送一遍,而不管这些数据包是不是需要发送到另一台区域B。
 
图1
 
             我们再将集线器A和集线器B分别连接到网桥的两个端口上,如果计算机A再向计算机C发送数据包时会遇到什么样的情况呢?这时集线器A也是同样会将数据包在全网发送,当到达网桥后,网桥会进行数据包目的MAC地址的分析,然后对比自己学习到的MAC地址表,如果这个表中没有此MAC地址,网桥便会在两个网段上的发送数据包,同时会将计算机A的MAC地址记录在自己的表当中。
             经过多次这样的记录,网桥会将所有的MAC地址记录,并划分为两个段。这时计算机A再次发送数据包给B的时候,因为这两台计算机同处在一个物理段位上,数据包到达网桥时,网桥会将目的MAC地址和自己的表进行对比,并且判断计算机A和计算机B在同一个段位上,便不会转发到区域B当中,而如果不在同一个物理段当中,网桥便会允许数据包通过网桥。
             通过以上的例子我们了解到,网桥实际上是一种控制冲突域流量的设备。网桥现在基本上已经很少用到了,除了隔离冲突域以外,网桥还可以实现不同O类型网络的连接(令牌环网和以太网之间的连接)和网络的扩展(IEEE的5.4.3连接规则)等等功能。

二、什么是交换
             交换同样工作在OSI网络参考模型的第二层数据链路层,也是一种 以MAC地址来作为判断依据来将网络划分成两个不同段 的技术, 不同的是交换将物理网段划分到每一个端口当中 ,简单的理解就是一种多端口的网桥,它实际上是一种桥接技术的延伸。
             在前面的了解当中,我们已经知道 桥接是连接两个不同的物理网段(冲突域)的技术,交换是连接多个物理网段技术,典型的交换机通常都有多个端口,每个端口实际上就是一个网桥,当连接到交换机端口的计算机要发送数据包时,所有的端口都会判断这个数据包是否是发给自己的,如果不是就将其丢弃,这样就将冲突域的概念扩展到每个交换机端口上。
             我们还是举例为大家说明,在下面的图中,我们可以看到计算机A、B分别连接到交换机的不同端口当中,当计算机A向B发送数据包时,假设这时A端口并没有学习到B端口的MAC地址,这时,A端口便会使用广播将数据包发送到除A端口以外的所有端口(广播域),当其他计算机接收到数据包后会与自己的MAC地址进行对比,然后简单的丢弃数据包;当B接收到数据包后,通过对比后接收数据包,并且记录源地址。通过反复这样的学习,交换机会构建一个基于所有端口的转发数据库,存储在交换机的内容可寻址存储器当中(CAM)。
 
图1
 
             在交换机学习到所有端口的信息后,计算机A再次发送数据包给B时,就不再广播地址,而是直接发送到转发数据库中所对应的B端口。通过这样的学习,在交换机上实现了微分段,每个连接到交换机端口的计算机都可以独享带宽。

三、什么是路由
             路由工作在OSI参考模型的第三层网络层当中,它是 基于第三层的IP地址信息来作为判断依据来将网络划分成不同段(IP子网)的技术,与桥接和交换不同, 路由划分的是独立的逻辑网段,每个所连接的网段都具有独立的网络IP地址信息,而不是以MAC地址作为判断路径的依据,这样路由便有隔离广播的能力;而交换和桥接是划分物理网段,它们仅仅是将物理传输介质进行分段处理。同时路由具备路径选择的功能,会根据不同的目的IP地址来分析到达目的地最合适的路径。
             在下图中,我们看到路由器所连接了三台交换机,这三台交换机分别被划分为三个不同的子网地址段:192.168.0.0、192.168.1.0、192.168.3.0。当计算机A向B发送数据包时,在不知道到达B的路径时,交换机A会将数据包在自己所在的段上全网广播,当到达路由器中,路由器便不会再广播这个数据包,它根据路由协议的规则来判断到达B应该选择将其转发到那个段上,这时便会将数据包转发到对应的IP地址段当中,而不广播到不需要这个数据包的C网段当中。如果路由器中没有规则定义到达目的IP地址的路径时,它会直接丢弃这个数据包。
 
图3

             路由器主要有路径选择和数据转发两个基本功能,但在很多场景下,路由器一般都承担着网关的角色。在国内,我们通常都是采用PPPOE拨号或者静态路由两种方式实现局域网共享上网。这时,路由器主要的功能是实现局域网和广域网之间的协议转换,这同样也是网关的主要用途。

四、三者之间的区别
1、位于参考模型的层数不同
             在开放系统互联参考模型当中,网桥和交换机都是位于参考模型的第二层-数据链路层,而路由器则位于更高一层-网络层。
2、基于的路径判断条件不同
             由于位于OSI参考模型的层数不同,所以使交换机、网关这两种设备判断路径的条件也不相同,网桥和交换机是根据端口的MAC地址来判断数据包转发,而路由器则使用IP地址来进行判断。
3、控制广播的能力不同
             网桥和交换机(三层交换机或支持VLAN功能的除外)这两种设备是无法控制网络的广播,如果有广播数据包,就会向所有的端口转发,所以在大的网络环境当中,必须得要有路由器来控制网络广播。
4、智能化程度不同
             在判断数据的时候,网桥只能判断是否在同一个物理网段,交换机则可以判断数据包是属于那个端口,但是这两种设备都没有选择最优路径的能力,而路由器基于IP地址判断路径,所以会根据IP地址信息来判断到达目的地的最优路径。
 
五、三者的不同应用场景及未来发展
             在现实的应用环境当中,网桥已经基本上不会被使用了,在中小型的局域网当中,最常用到的组网设备便是交换机,是否选择路由器会根据网络的规模和功能来决定,在大型网络中,路由器是必须的,用来控制广播,但是由于技术的不断延伸,交换机也被集成了基于IP地址判断路径及控制广播的功能,所以,路由器现在逐步在被可路由式交换机所取代。
             前面提到,路由器在很多场景下都是被用过网关,所以,随着宽带技术的迅速发展,在最末一公里,一种新兴的设备-宽带路由器将会逐步取代传统路由器来实现网络的接入功能。


  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值