Linux网络协议栈7--macvlan

macvlan是linux的一种虚拟网络接口,macvlan 允许你在主机的一个网络接口上配置多个虚拟的网络接口,这些网络 interface 有自己独立的 mac 地址,也可以配置上 ip 地址进行通信。macvlan 下的虚拟机或者容器网络和主机在同一个网段中,共享同一个广播域。
macvlan 和 bridge 比较相似,但因为它省去了 bridge 的存在,所以配置和调试起来比较简单,而且效率也相对高。除此之外,macvlan 自身也完美支持 VLAN。

macvlan 虚拟网卡设备包括5种模式:
private 模式:在这种模式下,macvlan设备不能接受寄生在同一个物理网卡的其他macvlan设备的数据包,即使是其他macvlan设备通过物理网卡发送出去并通过hairpin设备返回的包。
vepa 模式:在这种模式下,macvlan设备不能直接接受寄生在同一个物理网卡的其他macvlan设备的数据包,但是其他macvlan设备可以将数据包通过物理网卡发送出去,然后通过hairpin设备返回的给其他macvlan设备。
passthru 模式:在这种模式下,每一个物理设备只能寄生一个macvlan设备
bridge 模式:在这种模式下,寄生在同一个物理设备的macvlan设备可以直接通讯,不需要外接的hairpin设备帮助。
source 模式: 在这种模式下,寄生在物理设备的这类macvlan设备,只能接受指定的源 mac source的数据包,其他数据包都不接受。

macvlan在协议栈中两个重要的数据结构:
创建macvlan接口的时候,struct macvlan_dev 会作为macvlan接口设备数据结构net_device的私有数据结构创建(netdev_priv(dev)获取)保存单个macvlan接口的信息。
同时会为其宿主接口(如果是在其上创建的第一个macvlan接口)的net_device挂载特殊设备接收处理函数rx_handler=macvlan_handle_frame,以及这个函数需要用到的参数rx_handler_data,就是macvlan_port。保存的是这个宿主接口以及其下的macvlan接口的整体信息,最重要的当然是查找报文dmac锁对应的macvlan接口。
所有相关处理都在macvlan_common_newlink函数中。

struct macvlan_port {
	struct net_device	*dev;
	struct hlist_head	vlan_hash[MACVLAN_HASH_SIZE]; // macvlan设备macvlan_dev结构hash表,用于查找
	struct list_head	vlans; // macvlan设备macvlan_dev结构链表,用于遍历
	struct rcu_head		rcu;
	struct sk_buff_head	bc_queue;   // 广播报文队列

	struct work_struct	bc_work;    // 广播报文处理任务进程
	bool 			passthru;    
	int			count;
	struct hlist_head	vlan_source_hash[MACVLAN_HASH_SIZE]; // source模式用到
	DECLARE_BITMAP(mc_filter, MACVLAN_MC_FILTER_SZ);
};


struct macvlan_dev {
	struct net_device	*dev;      //macvlan网卡设备回指
	struct list_head	list;
	struct hlist_node	hlist;     
	struct macvlan_port	*port;    // struct macvlan_port 回指
	struct net_device	*lowerdev;  // 宿主结构设备 回指
	void			*fwd_priv;           // 物理网卡支持硬件加速时用到
	struct vlan_pcpu_stats __percpu *pcpu_stats;

	DECLARE_BITMAP(mc_filter, MACVLAN_MC_FILTER_SZ);

	netdev_features_t	set_features;
	enum macvlan_mode	mode;
	u16			flags;
	/* This array tracks active taps. */
	struct macvtap_queue	__rcu *taps[MAX_MACVTAP_QUEUES];
	/* This list tracks all taps (both enabled and disabled) */
	struct list_head	queue_list;
	int			numvtaps;
	int			numqueues;
	netdev_features_t	tap_features;
	int			minor;
	int			nest_level;
#ifdef CONFIG_NET_POLL_CONTROLLER
	struct netpoll		*netpoll;
#endif
	unsigned int		macaddr_count;
};

宿主接口接收函数,macvlan_handle_frame。


/* called under rcu_read_lock() from netif_receive_skb */
static rx_handler_result_t macvlan_handle_frame(struct sk_buff **pskb)
{
	struct macvlan_port *port;
	struct sk_buff *skb = *pskb;
	const struct ethhdr *eth = eth_hdr(skb);
	const struct macvlan_dev *vlan;
	const struct macvlan_dev *src;
	struct net_device *dev;
	unsigned int len = 0;
	int ret;
	rx_handler_result_t handle_res;

	port = macvlan_port_get_rcu(skb->dev);
	// 宿主接口接收到的广播报文,有可能是外部设备发送的,也可能是内部设备发送的,入外部设备hairpin模式返回的
	if (is_multicast_ether_addr(eth->h_dest)) {
		unsigned int hash;

		skb = ip_check_defrag(dev_net(skb->dev), skb, IP_DEFRAG_MACVLAN);
		if (!skb)
			return RX_HANDLER_CONSUMED;
		*pskb = skb;
		eth = eth_hdr(skb);
		// source模式,不管是广播还是单播,无脑根据smac匹配接收
		macvlan_forward_source(skb, port, eth->h_source);
		src = macvlan_hash_lookup(port, eth->h_source);
		// private 模式不允许接收本地的接口(同一宿主接口派生出来的)发出的报文,passthru 模式只有一个派生接口,
		// 这两种类型等同于只向自己发,不需要入队列了。
		if (src && src->mode != MACVLAN_MODE_VEPA &&
		    src->mode != MACVLAN_MODE_BRIDGE) {
			/* forward to original port. */
			vlan = src;
			ret = macvlan_broadcast_one(skb, vlan, eth, 0) ?:
			      netif_rx(skb);
			handle_res = RX_HANDLER_CONSUMED;
			goto out;
		}

		hash = mc_hash(NULL, eth->h_dest);
		if (test_bit(hash, port->mc_filter))
			macvlan_broadcast_enqueue(port, src, skb);

		return RX_HANDLER_PASS;
	}
	/*source模式,不管是广播还是单播,无脑根据smac匹配接收。
	  而且不影响正常根据mac地址匹配做转发的流程。
	  所以这里如果smac 和 dmac同时和一个source类型的接口匹配了,岂不是接收了两份??但实际上没有,需要仔细看看什么原因。
	*/ 
	macvlan_forward_source(skb, port, eth->h_source);
	if (port->passthru)
		// passthru 模式只有一个派生接口,直接取链表中第一个数据
		vlan = list_first_or_null_rcu(&port->vlans,
					      struct macvlan_dev, list);
	else
		// 其它在hash表中查
		vlan = macvlan_hash_lookup(port, eth->h_dest);
	if (vlan == NULL)
		return RX_HANDLER_PASS;

	dev = vlan->dev;
	if (unlikely(!(dev->flags & IFF_UP))) {
		kfree_skb(skb);
		return RX_HANDLER_CONSUMED;
	}
	len = skb->len + ETH_HLEN;
	skb = skb_share_check(skb, GFP_ATOMIC);
	if (!skb) {
		ret = NET_RX_DROP;
		handle_res = RX_HANDLER_CONSUMED;
		goto out;
	}

	*pskb = skb;
	skb->dev = dev;
	skb->pkt_type = PACKET_HOST;

	ret = NET_RX_SUCCESS;
	// 单播,修改了后skb->dev为macvlan接口,返回RX_HANDLER_ANOTHER,
	// __netif_receive_skb_core 会重走自己的流程,等于在macvlan接口上再走一遍协议栈。
	handle_res = RX_HANDLER_ANOTHER;
out:
	macvlan_count_rx(vlan, len, ret == NET_RX_SUCCESS, false);
	return handle_res;
}

macvlan接口发包流程


static netdev_tx_t macvlan_start_xmit(struct sk_buff *skb,
				      struct net_device *dev)
{
	unsigned int len = skb->len;
	int ret;
	struct macvlan_dev *vlan = netdev_priv(dev);

	if (unlikely(netpoll_tx_running(dev)))
		return macvlan_netpoll_send_skb(vlan, skb);
	// 硬件优化的代码,不用管
	if (vlan->fwd_priv) {
		skb->dev = vlan->lowerdev;
		ret = dev_queue_xmit_accel(skb, vlan->fwd_priv);
	} else {
		// 实际调用 macvlan_queue_xmit
		ret = macvlan_queue_xmit(skb, dev);
	}

	if (likely(ret == NET_XMIT_SUCCESS || ret == NET_XMIT_CN)) {
		struct vlan_pcpu_stats *pcpu_stats;

		pcpu_stats = this_cpu_ptr(vlan->pcpu_stats);
		u64_stats_update_begin(&pcpu_stats->syncp);
		pcpu_stats->tx_packets++;
		pcpu_stats->tx_bytes += len;
		u64_stats_update_end(&pcpu_stats->syncp);
	} else {
		this_cpu_inc(vlan->pcpu_stats->tx_dropped);
	}
	return ret;
}


static int macvlan_queue_xmit(struct sk_buff *skb, struct net_device *dev)
{
	const struct macvlan_dev *vlan = netdev_priv(dev);
	const struct macvlan_port *port = vlan->port;
	const struct macvlan_dev *dest;
	// bridge模式,如果报文是发向同一宿主接口派生的bridge模式的macvlan接口,则转发,其它都被禁止。
	if (vlan->mode == MACVLAN_MODE_BRIDGE) {
		const struct ethhdr *eth = (void *)skb->data;

		/* send to other bridge ports directly */
		// 可以看到,只有bridge模式能向 bridge模式的派生口发广播报文
		if (is_multicast_ether_addr(eth->h_dest)) {
			macvlan_broadcast(skb, port, dev, MACVLAN_MODE_BRIDGE);
			goto xmit_world;
		}

		dest = macvlan_hash_lookup(port, eth->h_dest);
		// 可以看到,只有bridge模式能向 bridge模式的派生口发单播报文
		if (dest && dest->mode == MACVLAN_MODE_BRIDGE) {
			/* send to lowerdev first for its network taps */
			dev_forward_skb(vlan->lowerdev, skb);

			return NET_XMIT_SUCCESS;
		}
	}

xmit_world:
	// 其它情况,直接走宿主物理口发送,所以macvlan接口的性能还是很高的,除了上面的简单的查表,不会做额外的封装,很接近物理口。
	skb->dev = vlan->lowerdev;
	return dev_queue_xmit(skb);
}

版权声明:本文为bigsheng2原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。