转:http://blog.sina.com.cn/s/blog_67cc0c8f0101oh33.html

转载一篇Bridge的数据在内核处理流程,文章写的不错啊!

(2013-07-05 16:08:48)

  分类: linux学习文档

作者:林海枫

本文地址: http://blog.csdn.net/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]

  1. int netif_receive_skb(struct sk_buff *skb)
  2. {
  3. //当网络设备收到网络数据包时,最终会在软件中断环境里调用此函数
  4. //检查该数据包是否有packet socket来接收该包,如果有则往该socket
  5. //拷贝一份,由deliver_skb来完成。
  6. list_for_each_entry_rcu(ptype, &ptype_all, list) {
  7. if (!ptype->dev || ptype->dev == skb->dev) {
  8. if (pt_prev)
  9. ret = deliver_skb(skb, pt_prev, orig_dev);
  10. pt_prev = ptype;
  11. }
  12. }
  13. // 先试着将该数据包让网桥函数来处理,如果该数据包的入口接口确实是网桥接口,
  14. // 则按网桥方式来处理,并且handle_bridge返回NULL,表示网桥已处理了。
  15. // 如果不是网桥接口的数据包,则不应该让网桥来处理,handle_bridge返回skb,
  16. // 后面代码会让协议栈来处理上层协议。
  17. skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);
  18. if (!skb)
  19. goto out;
  20. skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);
  21. if (!skb)
  22. goto out;
  23. //对该数据包转达到它L3协议的处理函数
  24. type = skb->protocol;
  25. list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
  26. if (ptype->type == type &&
  27. (!ptype->dev || ptype->dev == skb->dev)) {
  28. if (pt_prev)
  29. ret = deliver_skb(skb, pt_prev, orig_dev);
  30. pt_prev = ptype;
  31. }
  32. }
  33. }

1.4 
handle_bridge处理函数

[linux-2.6.24.4/net/core/dev.c]

  1. static inline struct sk_buff *handle_bridge(struct sk_buff *skb,
  2. struct packet_type **pt_prev, int *ret,
  3. struct net_device *orig_dev)
  4. {
  5. struct net_bridge_port *port;
  6. //如果该数据包产生于本机,而目标同时为本机。
  7. if (skb->pkt_type == PACKET_LOOPBACK ||
  8. //如果该数据包的输入接口不是网桥接口
  9. (port = rcu_dereference(skb->dev->br_port)) == NULL)
  10. // 以上两种情况都需要让上层协议进行处理
  11. return skb;
  12. if (*pt_prev) {
  13. *ret = deliver_skb(skb, *pt_prev, orig_dev);
  14. *pt_prev = NULL;
  15. }
  16. //数据包的入口接口是网桥接口。下面将按网桥逻辑进行处理。
  17. //如假包换,数据包转达到真正的网桥处理函数
  18. //br_handle_frame_hook在网桥模块的init函数被初始化为
  19. //br_handle_frame
  20. return br_handle_frame_hook(port, skb);
  21. }

1.5  网桥处理逻辑

[linux-2.6.24.4/net/bridge/br_input.c]

  1. struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb)
  2. {
  3. //所有网桥通信的数据包都会进入到这里,谓之为网桥处理函数
  4. const unsigned char *dest = eth_hdr(skb)->h_dest;
  5. int (*rhook)(struct sk_buff *skb);
  6. if (!is_valid_ether_addr(eth_hdr(skb)->h_source))
  7. goto drop;
  8. //如果skb是share的,则拷贝一份
  9. skb = skb_share_check(skb, GFP_ATOMIC);
  10. if (!skb)
  11. return NULL;
  12. if (unlikely(is_link_local(dest))) {
  13. if (skb->protocol == htons(ETH_P_PAUSE))
  14. goto drop;
  15. //如果该数据包的目标地址为STP协议的组播地址,并且该网桥启用STP功能,
  16. //则,结束该数据包的处理,它将会在第(2)处理得到处理
  17. if (p->br->stp_enabled != BR_NO_STP) {
  18. if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
  19. NULL, br_handle_local_finish))
  20. return NULL;
  21. else
  22. return skb;
  23. }
  24. // 如果该包是发往网桥组播的,但该网桥没有启用STP功能,则在下面处理,
  25. // 并返回已处理的标识(返回NULL)来通知代码(2)处不需再处理。
  26. }
  27. switch (p->state) {
  28. case BR_STATE_FORWARDING:
  29. rhook = rcu_dereference(br_should_route_hook);
  30. if (rhook != NULL) {
  31. if (rhook(skb))
  32. // 如果该接口处于Forwarding状态,并且该报文必需要走L3层
  33. // 进行转发,则直接返回,让代码(2)进行处理。
  34. // br_should_route_hook钩子函数在ebtable里面设置为ebt_broute函数,
  35. //它根据用户的规则来决定该报文是否要能通过L3来转发。
  36. return skb;
  37. dest = eth_hdr(skb)->h_dest;
  38. }
  39. case BR_STATE_LEARNING:
  40. if (!compare_ether_addr(p->br->dev->dev_addr, dest))
  41. //当用内核创建一个网桥的同时也会创建一个虚拟的网络设备,它的名字
  42. //为网桥的名字,保存在p->br->dev指针里。P->br->dev和port_list里面的
  43. //接口共同组成一个网桥。如果该报文是要发往此接,则标记skb->pkt_type为
  44. //PACKET_HOST。因为报文最终是要发送到p->br->dev的输送队列里面,
  45. //正如一般的网卡驱动程序将数据包送往到某个net_device的输入队列一样,
  46. //这样bridge功能充当了虚拟网卡(如例子中的br0)驱动,应当设置
  47. //skb->pkt_type
  48. //为PACKET_HOST,表明数据包是要发送该接口,而非是因为打开混杂模式
  49. //而接收到的。
  50. skb->pkt_type = PACKET_HOST;
  51. // 接着由br_handle_frame_finish函数继续处理。
  52. NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
  53. br_handle_frame_finish);
  54. break;
  55. default:
  56. //其它状态下的端口,不能处理数据包,直接丢弃。
  57. drop:
  58. kfree_skb(skb);
  59. }
  60. // 该数据包要么被网桥处理了,要么处理时出错,不需要上层协议处理,
  61. // 返回NULL,代码(2)处不会处理该报文。
  62. return NULL;
  63. }

1.6 
br_handle_frame_finish函数

[linux-2.6.24.4/net/bridge/br_input.c]

  1. int br_handle_frame_finish(struct sk_buff *skb)
  2. {
  3. const unsigned char *dest = eth_hdr(skb)->h_dest;
  4. struct net_bridge_port *p = rcu_dereference(skb->dev->br_port);
  5. struct net_bridge *br;
  6. struct net_bridge_fdb_entry *dst;
  7. struct sk_buff *skb2;
  8. if (!p || p->state == BR_STATE_DISABLED)
  9. goto drop;
  10. //对所有报的源MAC地址进行学习,这是网桥的特点之一,
  11. //通过对源地址的学习来建立MAC地址到端口的映射。
  12. br = p->br;
  13. br_fdb_update(br, p, eth_hdr(skb)->h_source);
  14. if (p->state == BR_STATE_LEARNING)
  15. goto drop;
  16. // skb2指针表明,有数据要发往本机的网络接口,即p->br->dev接口。
  17. skb2 = NULL;
  18. // 如果应用程序要dump本机接口的数据,那么该数据包应往主机发一份,
  19. // 一个明显的例子就是在用户在运行tcpdump –I br0或类似的程序。
  20. if (br->dev->flags & IFF_PROMISC)
  21. skb2 = skb;
  22. dst = NULL;
  23. if (is_multicast_ether_addr(dest)) {
  24. // 如果该报文是一个L2多播报文(如arp请求),那么它应该转发到
  25. // 该网桥的所有接口。
  26. // 这同样是网桥的一个特点,广播和组播报文要转发到它的所有接口。
  27. br->statistics.multicast++;
  28. skb2 = skb;
  29. } else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {
  30. // __br_fdb_get函数先查MAC-端口映射表,这一步是网桥的关键。
  31. // 这个报文应从哪个接口转发出去就看它了。
  32. // 如果这个报文应发往本机,那么skb置空。不需要再转发了,
  33. // 因为发往本机接口从逻辑上来说本身就是一个转发。
  34. skb2 = skb;
  35. skb = NULL;
  36. }
  37. if (skb2 == skb)
  38. skb2 = skb_clone(skb, GFP_ATOMIC);
  39. // skb2不为空,表明要发往本机,br_pass_frame_up函数来完成发往
  40. // 本机的工作。
  41. if (skb2)
  42. br_pass_frame_up(br, skb2);
  43. if (skb) {
  44. if (dst)
  45. // 由br_forward函数从dst所指向的端口将该报文发出去。
  46. br_forward(dst->dst, skb);
  47. else
  48. // 此报文是广播或组播报文,由br_flood_forward函数把报文向所有
  49. // 端口转发出去。
  50. br_flood_forward(br, skb);
  51. }
  52. out:
  53. return 0;
  54. drop:
  55. kfree_skb(skb);
  56. goto out;
  57. }

1.7 
通过br_pass_frame_up函数将报文发往本机接口。

[linux-2.6.24.4/net/bridge/br_input.c]

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

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

  1. void br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
  2. {
  3. if (should_deliver(to, skb)) {
  4. __br_forward(to, skb);
  5. return;
  6. }
  7. kfree_skb(skb);
  8. }

Should_deliver函数来测试是否应将该包转发出去,它由出口端的状态和报文的入口端口信息来决定,它的定义如下:

[linux-2.6.24.4/net/bridge/br_forward.c]

  1. static inline int should_deliver(const struct net_bridge_port *p,
  2. const struct sk_buff *skb)
  3. {
  4. //1) 入口端口和出口端口不能相同,如果是相同的话,那么源主机和目标
  5. // 主机在同一端口的子网段中,也即源主机和目标主机在同一广播域里面,
  6. // 目标主机和网桥都会同时收到该报文,因此网桥无需多此一举。
  7. //2) 如果出口端口的状态不是Forwarding,则不能转发出去。如果一个网桥
  8. // 没有启用STP功能,并且网络接口的状态为UP,那么它网桥端口的状态
  9. // 为Forwarding。如果启用STP,每个端口都有一个严格的状态,规定那些
  10. // 端口在什么情况下才能成为Forwarding状态,否则容易造成环路,产生
  11. // 网络风暴。
  12. return (skb->dev != p->dev && p->state == BR_STATE_FORWARDING);
  13. }

若报文的确需要转发,因为目标主机是在另一个子网段,而且没有其它网相连的网格端口可抵达该子网段(这里考虑到启用STP功能,如果搞不清楚可略过)。将调用__br_forward函数实施这一转发功能。

[linux-2.6.24.4/net/bridge/br_forward.c]

  1. static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
  2. {
  3. struct net_device *indev;
  4. indev = skb->dev;
  5. skb->dev = to->dev;
  6. skb_forward_csum(skb);
  7. // 通过br_forward_finish函数最终完成转发功能
  8. NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,
  9. br_forward_finish);
  10. }

[linux-2.6.24.4/net/bridge/br_forward.c]

  1. int br_forward_finish(struct sk_buff *skb)
  2. {
  3. return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit);
  4. }

Br_dev_queue_push_xmit在调用dev_queue_xmit函数前做些必要的检查工作。例如,报文的长度比出口端口的MTU还大,则丢掉该报文。

[linux-2.6.24.4/net/bridge/br_forward.c]

  1. int br_dev_queue_push_xmit(struct sk_buff *skb)
  2. {
  3. if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb))
  4. kfree_skb(skb);
  5. else {
  6. if (nf_bridge_maybe_copy_header(skb))
  7. kfree_skb(skb);
  8. else {
  9. // 网桥在处理数据包里,只需拆包来获得目标MAC地址,而不需要
  10. // 更改数据包的任何内容。但在入口网卡的驱动中已将以太网头部
  11. // 剥掉,现在需要将它套上。Skb_push函数实现这一功能。
  12. skb_push(skb, ETH_HLEN);
  13. // 放到网卡输出队列里,该网卡驱动将它送出去。
  14. dev_queue_xmit(skb);
  15. }
  16. }
  17. return 0;
  18. }

1.9 
br_flood_forward 函数把报文转发到网桥所有出口端口

[linux-2.6.24.4/net/bridge/br_forward.c]

  1. void br_flood_forward(struct net_bridge *br, struct sk_buff *skb)
  2. {
  3. br_flood(br, skb, __br_forward);
  4. }

__br_forward代码已在前面分析过,它从指定的出口端口转发该报文。

而br_flood函数,把__br_forward函数作为回调函数,依次遍网桥的所有出口端,调用__br_forward函数将该报文转发出去。一
个广播报文从某一端口进入,应该其余的端口都应该转发出去,但入口端口就不需要了。下面的代码看似把报文从所有端口都转发一份,其实不
然,should_deliver会阻止这样的事情发生。

[linux-2.6.24.4/net/bridge/br_forward.c]

  1. static void br_flood(struct net_bridge *br, struct sk_buff *skb,
  2. void (*__packet_hook)(const struct net_bridge_port *p,
  3. struct sk_buff *skb))
  4. {
  5. struct net_bridge_port *p;
  6. struct net_bridge_port *prev;
  7. prev = NULL;
  8. list_for_each_entry_rcu(p, &br->port_list, list) {
  9. if (should_deliver(p, skb)) {
  10. if (prev != NULL) {
  11. struct sk_buff *skb2;
  12. if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL) {
  13. br->statistics.tx_dropped++;
  14. kfree_skb(skb);
  15. return;
  16. }
  17. __packet_hook(prev, skb2);
  18. }
  19. prev = p;
  20. }
  21. }
  22. if (prev != NULL) {
  23. __packet_hook(prev, skb);
  24. return;
  25. }
  26. kfree_skb(skb);
  27. }

第二部分:网桥转发数据库的维护

众所周知,网桥需要维护一个MAC地址-端口映射表,端口是指网桥自身提供的端口,而MAC地址是指与端口相连的另一端的MAC地址。当网桥收到一个报文
时,先获取它的源MAC,更新数据库,然后读取该报文的目标MAC地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端
口以外的所有端口转发。

2.1 数据库的创建和销毁

数据库使用kmem_cache_create函数进行创建,使用kmem_cache_desctory进行销毁。

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. int __init br_fdb_init(void)
  2. {
  3. br_fdb_cache = kmem_cache_create("bridge_fdb_cache",
  4. sizeof(struct net_bridge_fdb_entry),
  5. 0,
  6. SLAB_HWCACHE_ALIGN, NULL);
  7. if (!br_fdb_cache)
  8. return -ENOMEM;
  9. get_random_bytes(&fdb_salt, sizeof(fdb_salt));
  10. return 0;
  11. }

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. void br_fdb_fini(void)
  2. {
  3. kmem_cache_destroy(br_fdb_cache);
  4. }

2.2 数据库更新

当网桥收到一个数据包时,它会获取该数据的源MAC地址,然后对数据库进行更新。如果该MAC地址不在数库中,则创新一个数据项。如果存在,更新它的年龄。数据库使用hash表的结构方式,便于高效查询。下面是hash功能代码的分析:

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
  2. const unsigned char *addr)
  3. {
  4. // br_mac_hash函数是hash表中的hash函数,具体算法过程可参阅该函数代码。
  5. // br->hash就是数据库的hash表,每个hash值对应一个链表。数据库的每项为
  6. // net_bridge_fdb_entry结构。
  7. struct hlist_head *head = &br->hash[br_mac_hash(addr)];
  8. struct net_bridge_fdb_entry *fdb;
  9. if (hold_time(br) == 0)
  10. return;
  11. if (!(source->state == BR_STATE_LEARNING ||
  12. source->state == BR_STATE_FORWARDING))
  13. return;
  14. fdb = fdb_find(head, addr);
  15. if (likely(fdb)) {
  16. // 接收到的MAC地址竟然是自己端口的MAC地址,确实不应该有这样的
  17. // 事情发生。
  18. if (unlikely(fdb->is_local)) {
  19. if (net_ratelimit())
  20. printk(KERN_WARNING "%s: received packet with "
  21. " own address as source address/n",
  22. source->dev->name);
  23. } else {
  24. // 收到该MAC地址的报文,更新它的年龄。
  25. fdb->dst = source;
  26. fdb->ageing_timer = jiffies;
  27. }
  28. } else {
  29. spin_lock(&br->hash_lock);
  30. if (!fdb_find(head, addr))
  31. // 这是新的MAC地址,在数据库里为之创建一个数据项。
  32. fdb_create(head, source, addr, 0);
  33. spin_unlock(&br->hash_lock);
  34. }
  35. }

2.3 创建数据项

在更新函数里面已为某一MAC找到了它所属于的Hash链表,因此,创建函数只需要在该链上添加一个数据项即可。

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,
  2. struct net_bridge_port *source,
  3. const unsigned char *addr,
  4. int is_local)
  5. {
  6. struct net_bridge_fdb_entry *fdb;
  7. fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC);
  8. if (fdb) {
  9. memcpy(fdb->addr.addr, addr, ETH_ALEN);
  10. atomic_set(&fdb->use_count, 1);
  11. hlist_add_head_rcu(&fdb->hlist, head);
  12. fdb->dst = source;
  13. fdb->is_local = is_local;
  14. fdb->is_static = is_local;
  15. fdb->ageing_timer = jiffies;
  16. }
  17. return fdb;
  18. }

2.4 查找数据项。

网桥的数据项查找与一般的查找类似,但略有不同。前面提到,如果要更新一MAC地址,不管该地址是否已经过期了,只需遍历该MAC地址对应的Hash链
表,然后更新年龄,此时它肯定不过期了。但网桥要转发数据时,除了要找到该目标MAC的出口端口外,还要判断该记录是否过期了。因此,数据项的查找有两
种,一种用于更新,另一用于转发。

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. static inline struct net_bridge_fdb_entry *fdb_find(struct hlist_head *head,
  2. const unsigned char *addr)
  3. {
  4. struct hlist_node *h;
  5. struct net_bridge_fdb_entry *fdb;
  6. hlist_for_each_entry_rcu(fdb, h, head, hlist) {
  7. if (!compare_ether_addr(fdb->addr.addr, addr))
  8. return fdb;
  9. }
  10. return NULL;
  11. }

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. struct net_bridge_fdb_entry *__br_fdb_get(struct net_bridge *br,
  2. const unsigned char *addr)
  3. {
  4. struct hlist_node *h;
  5. struct net_bridge_fdb_entry *fdb;
  6. hlist_for_each_entry_rcu(fdb, h, &br->hash[br_mac_hash(addr)], hlist) {
  7. if (!compare_ether_addr(fdb->addr.addr, addr)) {
  8. if (unlikely(has_expired(br, fdb)))
  9. break;
  10. return fdb;
  11. }
  12. }
  13. return NULL;
  14. }

除了__br_fdb_get函数多调用了has_expired外,其余无一不同。Has_expired函数来决定该数据项是否是过期的,代码如下:

[linux-2.6.24.4/net/bridge/br_fdb.c]

  1. static inline int has_expired(const struct net_bridge *br,
  2. const struct net_bridge_fdb_entry *fdb)
  3. {
  4. // 如果该数据项是静态的,即不是学习过来的,它永远不会过期。因为它就是
  5. // 网桥自己端口的地址。
  6. // 如果现在时间,与该数据项的最近更新时间和可保留时间之和相等,
  7. // 或者更早,则为过期。
  8. return !fdb->is_static
  9. && time_before_eq(fdb->ageing_timer + hold_time(br), jiffies);
  10. }
  11. // 数据项的可保留时间根据拓扑结构是否改变来决定,改变则为forward_delay,
  12. // 否则为ageing_time。
  13. static inline unsigned long hold_time(const struct net_bridge *br)
  14. {
  15. return br->topology_change ? br->forward_delay : br->ageing_time;
  16. }

第三部分: 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]

  1. void brioctl_set(int (*hook) (struct net *, unsigned int, void __user *))
  2. {
  3. mutex_lock(&br_ioctl_mutex);
  4. br_ioctl_hook = hook;
  5. mutex_unlock(&br_ioctl_mutex);
  6. }

用户空间程序使用网桥相关的命令来调用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]

  1. int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg)
  2. {
  3. switch (cmd) {
  4. case SIOCGIFBR:
  5. case SIOCSIFBR:
  6. // 这两个网桥命令是比较老式的,我们在这里不作讨论
  7. return old_deviceless(uarg);
  8. // 新式的网桥ioctl命令有两个,添加新网桥和删除现有的网桥
  9. // 需要用户空间提供网桥的名字。
  10. case SIOCBRADDBR:
  11. case SIOCBRDELBR:
  12. {
  13. char buf[IFNAMSIZ];
  14. if (!capable(CAP_NET_ADMIN))
  15. return -EPERM;
  16. if (copy_from_user(buf, uarg, IFNAMSIZ))
  17. return -EFAULT;
  18. buf[IFNAMSIZ-1] = 0;
  19. if (cmd == SIOCBRADDBR)
  20. return br_add_bridge(buf);
  21. return br_del_bridge(buf);
  22. }
  23. }
  24. return -EOPNOTSUPP;
  25. }

该函数调用br_add_bridge和br_del_brdge函数的实现新建和删除网桥的功能。由于这两个函数所完成的事情刚好相反,在此,我们只讨论br_add_bridge的代码:

[linux-2.6.24.4/net/bridge/br_if.c]

  1. int br_add_bridge(const char *name)
  2. {
  3. struct net_device *dev;
  4. int ret;
  5. // 创建网桥的核心工作,创建一个与网桥同名的网络设备。
  6. // 可以通过该设备分配的IP地址来管理该网桥。 同时该设备
  7. // 是虚拟的设备,它的接收包和发送包处理函数与一般的真实网卡
  8. // 设备不同。
  9. dev = new_bridge_dev(name);
  10. if (!dev)
  11. return -ENOMEM;
  12. rtnl_lock();
  13. if (strchr(dev->name, '%')) {
  14. ret = dev_alloc_name(dev, dev->name);
  15. if (ret < 0) {
  16. free_netdev(dev);
  17. goto out;
  18. }
  19. }
  20. // 向kernel注册该网桥设备,这样在用户空间就以使用
  21. // ifconfig来为之分配IP,或通ioctl来对该网桥添加新的接口。
  22. ret = register_netdevice(dev);
  23. if (ret)
  24. goto out;
  25. ret = br_sysfs_addbr(dev);
  26. if (ret)
  27. unregister_netdevice(dev);
  28. out:
  29. rtnl_unlock();
  30. return ret;
  31. }

现在创建网桥设备的任务落到new_bridge_dev的身上。New_bridge_dev函数的功能与一般的网卡驱动初化为代码非常类似的。因为这里段代就创建一个网桥设备,从这个层面来说,这段代码也算是驱动代码,结构和真实驱动非常类似。

[linux-2.6.24.4/net/bridge/br_if.c]

  1. static struct net_device *new_bridge_dev(const char *name)
  2. {
  3. struct net_bridge *br;
  4. struct net_device *dev;
  5. // 分配net_device结构,它的priv数据为net_bridge结构体。
  6. // br_dev_setup函数初化了net_device结构的很多函数指针。
  7. dev = alloc_netdev(sizeof(struct net_bridge), name,
  8. br_dev_setup);
  9. if (!dev)
  10. return NULL;
  11. br = netdev_priv(dev);
  12. br->dev = dev;
  13. spin_lock_init(&br->lock);
  14. INIT_LIST_HEAD(&br->port_list);
  15. spin_lock_init(&br->hash_lock);
  16. br->bridge_id.prio[0] = 0x80;
  17. br->bridge_id.prio[1] = 0x00;
  18. ….
  19. return dev;
  20. }

[linux-2.6.24.4/net/bridge/br_device.c]

  1. void br_dev_setup(struct net_device *dev)
  2. {
  3. // 为该网桥设备随机分配MAC地址
  4. random_ether_addr(dev->dev_addr);
  5. // 初始化dev的部分函数指针,因为目前网桥设备主适用于以及网
  6. // 以太网的部分功能对它也适用。
  7. ether_setup(dev);
  8. // 设置设备的ioctl函数为br_dev_ioctl。下面可以看到通过该ioctl函数
  9. // 来为网桥添加网络接口。
  10. dev->do_ioctl = br_dev_ioctl;
  11. // 网桥与一般网卡不同,网桥统一统计它的数据包和字节数等信息。
  12. dev->get_stats = br_dev_get_stats;
  13. // 网桥接口的数据包发送函数,真实设备要向外发送数据时,是通过
  14. // 网卡向外发送数据。而该网桥设备要向外发送数据时,它的处理逻辑与
  15. // 网桥其它接口的基本一致。
  16. dev->hard_start_xmit = br_dev_xmit;
  17. dev->open = br_dev_open;
  18. dev->set_multicast_list = br_dev_set_multicast_list;
  19. dev->change_mtu = br_change_mtu;
  20. dev->destructor = free_netdev;
  21. SET_ETHTOOL_OPS(dev, &br_ethtool_ops);
  22. dev->stop = br_dev_stop;
  23. dev->tx_queue_len = 0;
  24. dev->set_mac_address = br_set_mac_address;
  25. dev->priv_flags = IFF_EBRIDGE;
  26. dev->features = NETIF_F_SG | NETIF_F_FRAGLIST | NETIF_F_HIGHDMA |
  27. NETIF_F_GSO_MASK | NETIF_F_NO_CSUM | NETIF_F_LLTX;
  28. }

3.2 通过ioctl系统调用为网桥添加端口

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

[linux-2.6.24.4/net/bridge/br_ioctl.c]

  1. // dev 为网桥接口,ifreq 为添加/删除的物理接口的参数
  2. int br_dev_ioctl(struct net_device *dev, struct ifreq *rq, int cmd)
  3. {
  4. struct net_bridge *br = netdev_priv(dev);
  5. switch(cmd) {
  6. case SIOCDEVPRIVATE:
  7. return old_dev_ioctl(dev, rq, cmd);
  8. case SIOCBRADDIF:
  9. case SIOCBRDELIF:
  10. return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF);
  11. }
  12. pr_debug("Bridge does not support ioctl 0x%x/n", cmd);
  13. return -EOPNOTSUPP;
  14. }

这段代码一目了然,通过add_del_if函数来控制网桥的物理接口,该函数的代码和分析如下:

[linux-2.6.24.4/net/bridge/br_ioctl.c]

  1. // br 网桥,ifindex 添加/删除物理接口的index
  2. static int add_del_if(struct net_bridge *br, int ifindex, int isadd)
  3. {
  4. struct net_device *dev;
  5. int ret;
  6. if (!capable(CAP_NET_ADMIN))
  7. return -EPERM;
  8. dev = dev_get_by_index(&init_net, ifindex);
  9. if (dev == NULL)
  10. return -EINVAL;
  11. if (isadd)
  12. ret = br_add_if(br, dev);
  13. else
  14. ret = br_del_if(br, dev);
  15. dev_put(dev);
  16. return ret;
  17. }

具体的代码在br_add_if和br_del_if中,出于讨论的方便,我们只分析br_add_if函数。

[linux-2.6.24.4/net/bridge/br_if.c]

  1. int br_add_if(struct net_bridge *br, struct net_device *dev)
  2. {
  3. struct net_bridge_port *p;
  4. int err = 0;
  5. // Kernel仅支持以太网网桥
  6. if (dev->flags & IFF_LOOPBACK || dev->type != ARPHRD_ETHER)
  7. return -EINVAL;
  8. // 把网桥接口当作物理接口加入到另一个网桥中,是不行的。
  9. // 逻辑和代码上都会出现 loop
  10. if (dev->hard_start_xmit == br_dev_xmit)
  11. return -ELOOP;
  12. // 该物理接口加绑定到另一个网桥了。
  13. if (dev->br_port != NULL)
  14. return -EBUSY;
  15. // 为该接口创建一个网桥端口数据,并初始化好该端口的相关
  16. // 数据,详情可参阅该函数代码。
  17. p = new_nbp(br, dev);
  18. if (IS_ERR(p))
  19. return PTR_ERR(p);
  20. err = kobject_add(&p->kobj);
  21. if (err)
  22. goto err0;
  23. // 将该接口的物理地址写入到 MAC-端口映射表中。
  24. // 该MAC是属于网桥内部端口的固定MAC地址,
  25. // 它在fdb中的记录是固定的,不会失效(agged)
  26. err = br_fdb_insert(br, p, dev->dev_addr);
  27. if (err)
  28. goto err1;
  29. err = br_sysfs_addif(p);
  30. if (err)
  31. goto err2;
  32. rcu_assign_pointer(dev->br_port, p);
  33. // 打开该接口的混杂模式,网桥中的各个端口必须处于
  34. // 混杂模式,网桥才能正确工作。
  35. dev_set_promiscuity(dev, 1);
  36. // 加到端口列表
  37. list_add_rcu(&p->list, &br->port_list);
  38. spin_lock_bh(&br->lock);
  39. br_stp_recalculate_bridge_id(br);
  40. br_features_recompute(br);
  41. if ((dev->flags & IFF_UP) && netif_carrier_ok(dev) &&
  42. (br->dev->flags & IFF_UP))
  43. br_stp_enable_port(p);
  44. spin_unlock_bh(&br->lock);
  45. br_ifinfo_notify(RTM_NEWLINK, p);
  46. dev_set_mtu(br->dev, br_min_mtu(br));
  47. kobject_uevent(&p->kobj, KOBJ_ADD);
  48. return 0;
  49. err2:
  50. br_fdb_delete_by_port(br, p, 1);
  51. err1:
  52. kobject_del(&p->kobj);
  53. err0:
  54. kobject_put(&p->kobj);
  55. return err;
  56. }

第四部分: 总结

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

Bridge的数据在内核处理流程的更多相关文章

  1. PF_PACKET在内核的流程

    PF_PACKET在内核的流程   套接字创建 packet_create() --> 赋值packet_ops   接收流程 packet_recvmsg() skb_recv_datagra ...

  2. 【内核】linux内核启动流程详细分析

    Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ...

  3. 【内核】linux内核启动流程详细分析【转】

    转自:http://www.cnblogs.com/lcw/p/3337937.html Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件 ...

  4. Linux内核启动流程(简介)

    1. vmlinux.lds 首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以找到 Linux 内核的第一行程序是从哪里执行的: 第 ...

  5. ICC_lab总结——ICC_lab1:数据设置和基本流程

    ICC_lab总结 最近在学习后端的流程,做lab是最好不过了.但是有时候做过了lab,过了一段时间之后就会忘记,因此需要自己总结一下,加强印象. ICC_lab1:数据设置和基本流程 数据设置: 一 ...

  6. linux 内核启动流程分析,移植

    分析 linux-2.6.22.6 内核启动流程 移植 linux-3.4.2 到 JZ2440 开发板 Linux内核源码百度云链接: https://pan.baidu.com/s/1m1ymGl ...

  7. linux 内核启动流程

    Linux内核启动流程详细分析: http://www.linuxidc.com/Linux/2014-10/108034.htm ARM Linux内核启动过程: http://blog.csdn. ...

  8. Tiny4412 Linux 内核启动流程

    Linux内核的启动分为压缩内核和非压缩内核两种,这里我们以压缩内核为例.压缩内核运行时,将运行一段解压缩程序,得到真正的内核镜像,然后跳转到内核镜像运行.此时,Linux进入非压缩内核入口,在非压缩 ...

  9. 关于大数据T+1执行流程

    关于大数据T+1执行流程 前提: 搭建好大数据环境(hadoop hive hbase sqoop zookeeper oozie hue) 1.将所有数据库的数据汇总到hive (这里有三种数据源 ...

随机推荐

  1. 使用kubeadm安装kubernetes1.12.2版本脚本【h】

    Master节点脚本: #!/bin/sh#使用系统的PATH环境export PATH=`echo $PATH` #停止firewall防火墙,并禁止开机自启动 systemctl stop fir ...

  2. kubeadm安装集群系列-3.添加工作节点

    添加工作节点 worker通过kubeadm join加入集群,加入所需的集群的token默认24小时过期 查看Token kubeadm token list # 如果失效创建一个新的 kubead ...

  3. C学习笔记-流程控制

    常用的流程图符号 起止框用于表示流程的开始或结束: 输入/输出框用平行四边形表示,在平行四边形内可以写明输入或输出的内容: 判断框用菱形表示,它的作用是对条件进行判断,根据条件是否成立来决定如何执行后 ...

  4. kubenetes 的svc从ClusterPort 改为NodePort

    1.yaml文件如下 spec: clusterIP: 10.233.43.125 ports: - name: http-metrics port: protocol: TCP targetPort ...

  5. 论文阅读 | Robust Neural Machine Translation with Doubly Adversarial Inputs

    (1)用对抗性的源实例攻击翻译模型; (2)使用对抗性目标输入来保护翻译模型,提高其对对抗性源输入的鲁棒性. 生成对抗输入:基于梯度 (平均损失)  ->  AdvGen 我们的工作处理由白盒N ...

  6. Python全栈开发之6、面向对象

    一.创建类和对象 面向对象是一种编程方式,此编程方式的实现是基于对 类 和 对象 的使用 类是一个模板,模板中包装了多个“函数”供使用(可以讲多函数中公用的变量封装到对象中) 对象,根据模板创建的实例 ...

  7. [bzoj4842][bzoj1283][Neerc2016]Delight for a Cat/序列_线性规划_费用流

    4842: [Neerc2016]Delight for a Cat_1283: 序列 题目大意:ls是一个特别堕落的小朋友,对于n个连续的小时,他将要么睡觉要么打隔膜,一个小时内他不能既睡觉也打隔膜 ...

  8. [转帖]nginx配置ssl证书实现https访问

    https://www.cnblogs.com/tianhei/p/7726505.html 今天就是如此处理的 感觉挺不错的. 一,环境说明 服务器系统:ubuntu16.04LTS 服务器IP地址 ...

  9. Windows冷门快捷键

    Win+Shift+>或者+<光标键,可以使一个程序,在双屏显示器上左右切换. alt+space快捷键相当于在窗口的标题栏上面右键单击,弹出菜单,选择M键,就可以使用光标键上下左右移动来 ...

  10. (5.15)mysql高可用系列——MHA实践

    关键词:MHA,mysql mha [1]需求 采用mysql技术,实现MHA高可用主从环境,预计未来数据量几百G MHA概念参考:MYSQL高可用技术概述 [2]环境技术架构 [2.1]MHA简介 ...