kernel 网桥代码分析

本文详细解析了Linux网桥的工作原理和技术细节,包括网桥的数据结构、配置方法及数据包处理流程等关键内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:林海枫

本文地址: http://blog.youkuaiyun.com/linyt/archive/2010/01/14/5191512.aspx

注:本文由作者所拥用,欢迎转载,但请全文转载并注明作者,请勿用于 任何商途。

本文分析的kernel版本为:2.6.24.4,网桥代码目录为:linux-2.6.24.4/net/bridge。

本文着重分析网桥的基本功能,关于STP的功能,我想从另写一篇文章进行分析。由于时间仓促,分析可能存在不足之外。

      网桥是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;
     }
   }
}


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) ;
}


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 ;
}


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;
}


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) ;
}

1.8  通过br_forward函数将报文从另一个端口转发出去

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) ;
}


若报文的确需要转发,因为目标主机是在另一个子网段,而且没有其它网相连的网格端口可抵达该子网段(这里考虑到启用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) ;
}

[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) ;
}
 

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) ;
}

__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) ;
}


 

 原文地址 http://blog.youkuaiyun.com/linyt/archive/2010/01/14/5191512.aspx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值