深入理解Linux网络随笔(七):容器网络虚拟化--Veth设备对

发布于:2025-03-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

深入理解Linux网络随笔(七):容器网络虚拟化

微服务架构中服务被拆分成多个独立的容器,docker网络虚拟化的核心技术为:Veth设备对、Network Namespace、Bridg。

Veth设备对

veth设备是一种 成对 出现的虚拟网络接口,作用是 在 Linux 网络命名空间或不同网络栈之间建立一个虚拟的点对点连接,实现数据通信。例如实现容器与宿主机间的通信、不同neths间传递流量。

如图所示:

虚拟网络设备并不会直接连接物理网络设备,而是一端连接到协议栈,另一端连接到另一个 veth 设备。从一对 veth 设备中发出的数据包会直接传送到另一个 veth 设备。每个 veth 设备都可以配置 IP 地址,并作为 路由的一个接口,可以进行IP层通信。
在这里插入图片描述

特点:

(1)成对出现:创建时总是 两个设备*成对出现,例如 veth0veth1,它们之间类似于一条网络隧道

(2)工作方式:一端发送的流量会从另一端收到,就像网线直连一样

(3)不处理 L2/L3 转发:veth 设备 不会执行交换、路由*等功能,只是简单地在两端传输数据包

底层源码分析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

veth设备的初始化通过函数veth_init进行。

static __init int veth_init(void)
{
	//注册veth_link_ops veth设备的操作方法
	return rtnl_link_register(&veth_link_ops);
}

veth_link_ops中定义了veth设备的操作回调函数。

static struct rtnl_link_ops veth_link_ops = {
	.kind		= DRV_NAME,//设备类型
	.priv_size	= sizeof(struct veth_priv),//私有数据大小
	.setup		= veth_setup,//设备启动
	.validate	= veth_validate,//检查netlink请求参数的合法性
	.newlink	= veth_newlink,//处理新建的veth设备回调函数
	.dellink	= veth_dellink,//处理删除的veth设备回调函数
	.policy		= veth_policy,//netlink配置参数解析策略
	.maxtype	= VETH_INFO_MAX,// netlink 解析时允许的最大属性编号
	.get_link_net	= veth_get_link_net,// 获取 veth 设备所在的网络命名空间
	.get_num_tx_queues	= veth_get_num_queues,// 获取设备的 TX 队列数
	.get_num_rx_queues	= veth_get_num_queues,// 获取设备的 RX 队列数
};

veth设备创建调用veth_newlink函数。

static int veth_newlink(struct net *src_net, struct net_device *dev,
			struct nlattr *tb[], struct nlattr *data[],
			struct netlink_ext_ack *extack)
{
	int err;
	struct net_device *peer;
	struct veth_priv *priv;
	char ifname[IFNAMSIZ];
	struct nlattr *peer_tb[IFLA_MAX + 1], **tbp;
	unsigned char name_assign_type;
	struct ifinfomsg *ifmp;
	struct net *net;

	.....
	//创建peer设备
	peer = rtnl_create_link(net, ifname, name_assign_type,
				&veth_link_ops, tbp, extack);
	if (IS_ERR(peer)) {
		put_net(net);
		return PTR_ERR(peer);
	}
	....
	//注册peer设备
	err = register_netdevice(peer);
	...
	//获取dev的私有数据priv
	priv = netdev_priv(dev);
	//将dev的指针赋值给peer的私有数据peer->priv,建立连接,通过dev访问peer设备
	rcu_assign_pointer(priv->peer, peer);
	//初始化dev的TX、RX队列
	err = veth_init_queues(dev, tb);
	if (err)
		goto err_queues;
	//获取 peer 设备的 priv 结构体
	priv = netdev_priv(peer);
	//让 peer->priv->peer 指向 dev,建立 veth 设备对的双向连接
	rcu_assign_pointer(priv->peer, dev);
	//初始化peer设备的队列
	err = veth_init_queues(peer, tb);
	if (err)
		goto err_queues;
  .....
}

启动veth设备,通过veth_netdev_ops操作表找到发送过程中的回调函数veth_xmit。

static void veth_setup(struct net_device *dev)
{
	ether_setup(dev);

	dev->priv_flags &= ~IFF_TX_SKB_SHARING;
	dev->priv_flags |= IFF_LIVE_ADDR_CHANGE;
	dev->priv_flags |= IFF_NO_QUEUE;
	dev->priv_flags |= IFF_PHONY_HEADROOM;
	//veth操作列表
	dev->netdev_ops = &veth_netdev_ops;
	dev->ethtool_ops = &veth_ethtool_ops;
	dev->features |= NETIF_F_LLTX;
	dev->features |= VETH_FEATURES;
	dev->vlan_features = dev->features &
			     ~(NETIF_F_HW_VLAN_CTAG_TX |
			       NETIF_F_HW_VLAN_STAG_TX |
			       NETIF_F_HW_VLAN_CTAG_RX |
			       NETIF_F_HW_VLAN_STAG_RX);
	dev->needs_free_netdev = true;
	dev->priv_destructor = veth_dev_free;
	dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS;
	dev->max_mtu = ETH_MAX_MTU;

	dev->hw_features = VETH_FEATURES;
	dev->hw_enc_features = VETH_FEATURES;
	dev->mpls_features = NETIF_F_HW_CSUM | NETIF_F_GSO_SOFTWARE;
	netif_set_tso_max_size(dev, GSO_MAX_SIZE);
}
static const struct net_device_ops veth_netdev_ops = {
	.ndo_init            = veth_dev_init,
	.ndo_open            = veth_open,
	.ndo_stop            = veth_close,
	.ndo_start_xmit      = veth_xmit,//veth发送函数
	.ndo_get_stats64     = veth_get_stats64,
	.ndo_set_rx_mode     = veth_set_multicast_list,
	.ndo_set_mac_address = eth_mac_addr,
#ifdef CONFIG_NET_POLL_CONTROLLER
	.ndo_poll_controller	= veth_poll_controller,
#endif
	.ndo_get_iflink		= veth_get_iflink,
	.ndo_fix_features	= veth_fix_features,
	.ndo_set_features	= veth_set_features,
	.ndo_features_check	= passthru_features_check,
	.ndo_set_rx_headroom	= veth_set_rx_headroom,
	.ndo_bpf		= veth_xdp,
	.ndo_xdp_xmit		= veth_ndo_xdp_xmit,
	.ndo_get_peer_dev	= veth_peer_dev,
};
通信过程

数据包发送过程中到达网络设备层会进入dev_hard_start_xmit函数,遍历链表上的所有skb包调用xmit_one发送数据包。

//网络设备数据包发送路径(TX Path) 的关键部分,负责调用底层驱动的 ndo_start_xmit() 发送数据包
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
		    struct netdev_queue *txq, bool more)
{
	unsigned int len;
	int rc;
	//监测是否有协议栈上层监听
	if (dev_nit_active(dev))
	//AF_PACKET 套接字正在监听,发送数据包副本给监听进程
		dev_queue_xmit_nit(skb, dev);
//记录数据包长度
	len = skb->len;
	//触发tracepoint机制,记录数据包发送开始
	trace_net_dev_start_xmit(skb, dev);
	//调用底层驱动的ndo_start_xmit()方法发送数据包
	rc = netdev_start_xmit(skb, dev, txq, more);
	trace_net_dev_xmit(skb, rc, dev, len);

	return rc;
}

获取驱动设备回调函数集合ops,结构体net_device_ops,调用__netdev_start_xmit发送数据包。

static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,
					    struct netdev_queue *txq, bool more)
{
	const struct net_device_ops *ops = dev->netdev_ops;
	netdev_tx_t rc;
	//发送数据包
	rc = __netdev_start_xmit(ops, skb, dev, more);
	if (rc == NETDEV_TX_OK)
		txq_trans_update(txq);

	return rc;
}

在这里会首先判断当前cpu发送队列是否还有数据待处理,然后调用驱动的ndo_start_xmit函数发送数据包,回调函数veth_xmit,lo是loopback_xmit。也就是在veth启动的时候注册的回调函数。

static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,
					      struct sk_buff *skb, struct net_device *dev,
					      bool more)
{
	__this_cpu_write(softnet_data.xmit.more, more);
	return ops->ndo_start_xmit(skb, dev);
}

veth_xmit核心是获取veth设备数据,将数据发送到对端设备。

static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
	struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
	struct veth_rq *rq = NULL;
	int ret = NETDEV_TX_OK;
	struct net_device *rcv;
	int length = skb->len;
	bool use_napi = false;
	int rxq;

	rcu_read_lock();
	//获取veth设备
	rcv = rcu_dereference(priv->peer);
	...
	//获取rcv设备私有数据
	rcv_priv = netdev_priv(rcv);
	//获取skb队列索引
	rxq = skb_get_queue_mapping(skb);
	if (rxq < rcv->real_num_rx_queues) {
		rq = &rcv_priv->rq[rxq];
		//rq绑定了napi,且数据包适合GRO,开启NAPI机制轮询数据包
		use_napi = rcu_access_pointer(rq->napi) &&
			   veth_skb_is_eligible_for_gro(dev, rcv, skb);
	}

	skb_tx_timestamp(skb);
	//尝试将skb转发到对端veth设备
	if (likely(veth_forward_skb(rcv, skb, rq, use_napi) == NET_RX_SUCCESS)) {
		//未使用NAPI机制,更新统计信息
		if (!use_napi)
			dev_sw_netstats_tx_add(dev, 1, length);
	} else {
.....
}

veth_forward_skb会根据数据包选择不同路径,数据包转发到对端设备dev_forward_skb,对端开启XDP则调用veth_xdp_rx,普通数据包调用netif_rx

static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
			    struct veth_rq *rq, bool xdp)
{
	return __dev_forward_skb(dev, skb) ?: xdp ?
		veth_xdp_rx(rq, skb) :
		__netif_rx(skb);
}

函数调用关系:__dev_forward_skb-->__dev_forward_skb2-->____dev_forward_skb

//处理dev设备的转发数据包
static int __dev_forward_skb2(struct net_device *dev, struct sk_buff *skb,
			      bool check_mtu)
{
	//实际数据包转发处理
	int ret = ____dev_forward_skb(dev, skb, check_mtu);
	if (likely(!ret)) {
		//将skb所属设备设置成刚才取到的veth对端设备rcv
		skb->protocol = eth_type_trans(skb, dev);
		//修正skb校验和
		skb_postpull_rcsum(skb, eth_hdr(skb), ETH_HLEN);
	}
	return ret;
}

eth_type_trans设置完成会继续执行__netif_rx路径,函数调用逻辑netif_rx_internal-->enqueue_to_backlog,在这里获取每个CPU核心对应的softnet_data数据结构,将skb添加到等待队列input_pkt_queue,在函数__test_and_set_bit检查sd->backlog.state是否已包含 NAPI_STATE_SCHEDNAPI_STATE_SCHED是NAPI轮询处理的一个 状态标志位防止同一个 NAPI 任务被重复调度,未设置调用napi_schedule_rps触发NAPI调度,触发软中断 NET_RX_SOFTIRQ

static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
			      unsigned int *qtail)
{
	enum skb_drop_reason reason;
	struct softnet_data *sd;
	unsigned long flags;
	unsigned int qlen;
	//丢包原因:未指定
	reason = SKB_DROP_REASON_NOT_SPECIFIED;
	sd = &per_cpu(softnet_data, cpu);
    	if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state))
			napi_schedule_rps(sd);
.....
}

在这里根据是否开启RPS机制走不同的路径,RPS 是 Linux 内核的一种 多核负载均衡机制将收到的数据包分配到多个 CPU 进行处理,避免所有网络流量只由单个 CPU 处理,减少 CPU 瓶颈,在这里通过rps_ipi_list将数据包从一个CPU转发到另外一个CPU上,提高多核环境下的负载均衡,减少CPU之间的竞争。如果没有开启RPS机制,数据包会在当前CPU软中断上下文中处理NAP任务。

static int napi_schedule_rps(struct softnet_data *sd)
{
	struct softnet_data *mysd = this_cpu_ptr(&softnet_data);
// RPS将接收的数据包调度到 不同的 CPU 进行处理
#ifdef CONFIG_RPS
//sd不等于mysd,说明要在另一个 CPU 上执行 NAPI 任务,而不是本 CPU 处理
	if (sd != mysd) {
		//rps_ipi_list 负责存储要在其他 CPU 处理的 softnet_data 队列,并通过 NET_RX_SOFTIRQ 触发软中断来完成调度。
		sd->rps_ipi_next = mysd->rps_ipi_list;
		mysd->rps_ipi_list = sd;
		//出发软中断,处理softnet_data队列的NAPI任务
		__raise_softirq_irqoff(NET_RX_SOFTIRQ);
		return 1;
	}
//本地CPU处理
#endif /* CONFIG_RPS */
//直接调度 mysd->backlog 设备的 NAPI 任务,并安排 net_rx_action() 来执行数据包处理。
	__napi_schedule_irqoff(&mysd->backlog);
	return 0;
}

实践操作

Linux 中创建 veth 设备对,设备名veth,指定的虚拟网卡类型为veth,创建的另一端设备名为veth1。

ip link add veth0 type veth peer name veth1

使用ip link show可以看到veth0veth1 设备已创建,但还未启用。

在这里插入图片描述

veth设备需要配置IP地址才能进行通信,为veth0veth1 设备配置ip。

sudo ip addr add 192.168.1.1/24 dev veth0
sudo ip addr add 192.168.1.2/24 dev veth1

启动设备

sudo ip link set veth1 up
sudo ip link set veth0 up

使用ip link show可以看到veth0veth1 设备状态已变成开启

在这里插入图片描述

为了使veth设备之间能够顺利通信,需要关闭反向路径过滤(rp_filter)并设置允许接收本机数据包。root角色下修改系统配置如下:

!

ping测试veth0veth1 设备间通信

在这里插入图片描述