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

深入理解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进行。

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

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

c 复制代码
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函数。

c 复制代码
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。

c 复制代码
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);
}
c 复制代码
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发送数据包。

c 复制代码
//网络设备数据包发送路径(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发送数据包。

c 复制代码
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启动的时候注册的回调函数。

c 复制代码
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设备数据,将数据发送到对端设备。

c 复制代码
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

c 复制代码
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

c 复制代码
//处理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

c 复制代码
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任务。

c 复制代码
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。

c 复制代码
ip link add veth0 type veth peer name veth1

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

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

c 复制代码
sudo ip addr add 192.168.1.1/24 dev veth0
sudo ip addr add 192.168.1.2/24 dev veth1

启动设备

c 复制代码
sudo ip link set veth1 up
sudo ip link set veth0 up

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

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

ping测试veth0veth1 设备间通信

相关推荐
A小辣椒21 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
网络研究院3 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展