【Linux4.1.12源码分析】vxlan报文发送之udp_tunnel_xmit_skb

Linux内核4.1.12中VXLAN报文UDP隧道发送分析
本文深入剖析Linux 4.1.12内核中的udp_tunnel_xmit_skb函数,该函数作为Open vSwitch (OVS) 2.5发送UDP报文的入口。在调用此函数前,必须预先准备好headroom空间,并已构建VXLAN头部,skb数据指针指向VXLAN头部。

udp_tunnel_xmit_skb函数是OVS2.5发送UDP报文的内核入口,在调用该函数之前,headroom空间需要准备完成,且vxlan头已经创建,skb结构体的data指向vxlan头的首地址。

1、udp_tunnel_xmit_skb函数

int udp_tunnel_xmit_skb(struct rtable *rt, struct sock *sk, struct sk_buff *skb,
			__be32 src, __be32 dst, __u8 tos, __u8 ttl,
			__be16 df, __be16 src_port, __be16 dst_port,
			bool xnet, bool nocheck)
{
	struct udphdr *uh;

	__skb_push(skb, sizeof(*uh));		//skb增加UDP头,skb报文的headroom大小在vxlan头封装前就完成准备,线性区的空间是充足的
	skb_reset_transport_header(skb);	//重置报文(外层报文)的transport header的偏移
	uh = udp_hdr(skb);

	uh->dest = dst_port;			//设置目的端口
	uh->source = src_port;			//设置源端端口
	uh->len = htons(skb->len);		//设置UDP头中的长度,该长度包括UDP头 + vxlan头 + 用户数据(payload)

	udp_set_csum(nocheck, skb, src, dst, skb->len);		//UDP头csum计算

	return iptunnel_xmit(sk, rt, skb, src, dst, IPPROTO_UDP,	//IP层封装tunnel发送报文
			     tos, ttl, df, xnet);
}
2、udp_set_csum函数

/* Function 
补充dev_queue_xmit实现,请问哪个地方是调用vlan设备的发送函数? static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev) { struct net_device *dev = skb->dev; struct netdev_queue *txq; struct Qdisc *q; int rc = -ENOMEM; bool again = false; skb_reset_mac_header(skb); if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP)) __skb_tstamp_tx(skb, NULL, skb->sk, SCM_TSTAMP_SCHED); /* Disable soft irqs for various locks below. Also * stops preemption for RCU. */ rcu_read_lock_bh(); skb_update_prio(skb); qdisc_pkt_len_init(skb); #ifdef CONFIG_NET_CLS_ACT skb->tc_at_ingress = 0; # ifdef CONFIG_NET_EGRESS if (static_branch_unlikely(&egress_needed_key)) { skb = sch_handle_egress(skb, &rc, dev); if (!skb) goto out; } # endif #endif /* If device/qdisc don't need skb->dst, release it right now while * its hot in this cpu cache. */ if (dev->priv_flags & IFF_XMIT_DST_RELEASE) skb_dst_drop(skb); else skb_dst_force(skb); txq = netdev_core_pick_tx(dev, skb, sb_dev); q = rcu_dereference_bh(txq->qdisc); trace_net_dev_queue(skb); if (q->enqueue) { rc = __dev_xmit_skb(skb, q, dev, txq); goto out; } /* The device has no queue. Common case for software devices: * loopback, all the sorts of tunnels... * Really, it is unlikely that netif_tx_lock protection is necessary * here. (f.e. loopback and IP tunnels are clean ignoring statistics * counters.) * However, it is possible, that they rely on protection * made by us here. * Check this and shot the lock. It is not prone from deadlocks. *Either shot noqueue qdisc, it is even simpler 8) */ // 确保设备已启用,如ifconfig up if (dev->flags & IFF_UP) { int cpu = smp_processor_id(); /* ok because BHs are off */ /* Other cpus might concurrently change txq->xmit_lock_owner * to -1 or to their cpu id, but not to our id. */ if (READ_ONCE(txq->xmit_lock_owner) != cpu) { if (dev_xmit_recursion()) // 检查当前线程递归深度 goto recursion_alert; // 过深则放弃发送 skb = validate_xmit_skb(skb, dev, &again); if (!skb) goto out; HARD_TX_LOCK(dev, txq, cpu); if (!netif_xmit_stopped(txq)) { dev_xmit_recursion_inc(); // 增加递归计数器 skb = dev_hard_start_xmit(skb, dev, txq, &rc); // 执行实际发送 dev_xmit_recursion_dec(); if (dev_xmit_complete(rc)) { // 发送成功完成? HARD_TX_UNLOCK(dev, txq); goto out; } } HARD_TX_UNLOCK(dev, txq); net_crit_ratelimited("Virtual device %s asks to queue packet!\n", dev->name); } else { /* Recursion is detected! It is possible, * unfortunately 若当前 CPU 已是锁所有者(txq->xmit_lock_owner == cpu),说明发生递归调用,触发死锁警告 */ recursion_alert: net_crit_ratelimited("Dead loop on virtual device %s, fix it urgently!\n", dev->name); } } rc = -ENETDOWN; rcu_read_unlock_bh(); atomic_long_inc(&dev->tx_dropped); kfree_skb_list(skb); return rc; out: rcu_read_unlock_bh(); return rc; }
最新发布
11-25
<think> 我们正在分析内核函数`dev_queue_xmit`的代码流程,特别是当`skb->dev`指向一个VLAN设备(如eth1.120)时,如何调用到VLAN设备的发送函数。 根据之前的讨论,我们知道VLAN设备有自己的发送函数`vlan_dev_hard_start_xmit`。那么在`dev_queue_xmit`中是如何调用到这个函数的呢? 内核网络设备发送数据包的基本流程如下: 1. 调用`dev_queue_xmit`,它会调用`__dev_queue_xmit`。 2. 在`__dev_queue_xmit`中,会获取设备(`skb->dev`)的发送队列(`qdisc`),然后根据队列规则进行处理。 3. 如果没有队列规则(或者队列规则为空)并且设备处于运行状态,则直接调用`dev_hard_start_xmit`发送4. 在`dev_hard_start_xmit`中,会调用设备操作结构(`net_device_ops`)中的`ndo_start_xmit`函数指针。 对于VLAN设备,当它被创建时(通过`vlan_setup`),其`net_device_ops`被设置为`vlan_netdev_ops`,其中`ndo_start_xmit`指向`vlan_dev_hard_start_xmit`。 因此,调用链为: dev_queue_xmit -> __dev_queue_xmit -> ... -> dev_hard_start_xmit -> ndo_start_xmit (即 vlan_dev_hard_start_xmit) 现在,我们具体在`__dev_queue_xmit`函数中寻找调用`dev_hard_start_xmit`的位置。 在Linux内核源码(以4.1.12为例,但原理通用)中,`__dev_queue_xmit`函数的大致流程如下: 1. 检查设备是否有发送队列(qdisc)。 2. 如果有,则调用`__dev_xmit_skb`将skb放入队列,然后由队列调度算法决定何时发送。 3. 如果没有,则检查设备是否处于运行状态(`__netif_tx_trylock`),然后直接调用`dev_hard_start_xmit`。 但是,即使有队列,最终在队列调度后,还是会调用`sch_direct_xmit`,而在`sch_direct_xmit`中会调用`dev_hard_start_xmit`。 因此,无论是否有队列,最终都会通过`dev_hard_start_xmit`调用到设备的`ndo_start_xmit`。 所以,在`__dev_queue_xmit`函数中,调用VLAN设备发送函数的位置实际上是通过`dev_hard_start_xmit`间接调用的。 具体代码位置(以Linux 4.1.12为例): 在`__dev_queue_xmit`中,如果没有队列或者队列不需要排队(或者直接发送的情况),则: if (q->flags & TCQ_F_NOLOCK) { ... } else { // 加锁 ... if (!netif_tx_queue_stopped(txq) && !netif_tx_queue_frozen(txq) && !skb->xmit_more) { // 尝试发送 rc = netdev_start_xmit(skb, dev, txq, false); ... } } 或者在有队列的情况下,调用`__dev_xmit_skb`,而在`__dev_xmit_skb`中,如果队列为空,也会尝试直接发送(通过`sch_direct_xmit`)。 而`netdev_start_xmit`会调用`__netdev_start_xmit`,进而调用`dev_hard_start_xmit`。 所以,调用设备发送函数的实际位置是在`dev_hard_start_xmit`中,它会调用`skb->dev->netdev_ops->ndo_start_xmit`。 对于VLAN设备,这个函数指针就是`vlan_dev_hard_start_xmit`。 因此,在`dev_queue_xmit`函数实现中,调用VLAN设备发送函数的位置实际上是在`dev_hard_start_xmit`函数中(由`__dev_queue_xmit`函数调用)。 总结: - `dev_queue_xmit` -> `__dev_queue_xmit` - 在`__dev_queue_xmit`中,通过队列调度机制(可能会经过`sch_direct_xmit`)或直接调用`netdev_start_xmit`,最终调用`dev_hard_start_xmit`。 - `dev_hard_start_xmit`调用设备操作中的`ndo_start_xmit`,对于VLAN设备就是`vlan_dev_hard_start_xmit`。 所以,在`dev_queue_xmit`函数中并没有直接调用VLAN设备发送函数,而是通过设备操作结构间接调用,实际调用点是在`dev_hard_start_xmit`函数中。 </think>在 Linux 内核网络栈中,`dev_queue_xmit` 调用 VLAN 设备发送函数的关键位置位于**发送路径的硬件启动阶段**。具体流程如下: ### 调用位置分析 1. **`__dev_queue_xmit` 入口** 当上层协议调用 `dev_queue_xmit(skb)` 时,会进入核心函数 `__dev_queue_xmit`: ```c // net/core/dev.c int __dev_queue_xmit(struct sk_buff *skb) { struct net_device *dev = skb->dev; ... // 获取设备的发送队列 txq = netdev_pick_tx(dev, skb, NULL); ... // 进入队列调度或直接发送 q = rcu_dereference_bh(txq->qdisc); if (q->enqueue) { // 有队列策略时进入拥塞控制流程 rc = __dev_xmit_skb(skb, q, dev, txq); } else { // 无队列策略时直接发送 if (dev->flags & IFF_UP) { rc = dev_hard_start_xmit(skb, dev, txq); // 🔹关键调用点 } } } ``` 2. **`dev_hard_start_xmit` 分发** 在直接发送路径中,`dev_hard_start_xmit` 通过设备操作结构调用 VLAN 发送函数: ```c // net/core/dev.c struct netdev_queue *dev_hard_start_xmit(struct sk_buff *skb) { struct net_device *dev = skb->dev; const struct net_device_ops *ops = dev->netdev_ops; ... // 🔹调用设备特定的发送函数 rc = ops->ndo_start_xmit(skb, dev); // 对 VLAN 设备即 vlan_dev_hard_start_xmit ... } ``` 3. **VLAN 设备操作注册** VLAN 设备初始化时注册了专用操作函数: ```c // net/8021q/vlan_dev.c static const struct net_device_ops vlan_netdev_ops = { .ndo_start_xmit = vlan_dev_hard_start_xmit, // 🔹VLAN 发送入口 ... }; ``` ### 调用路径总结 完整调用链为: ``` dev_queue_xmit() └── __dev_queue_xmit() ├── [有队列策略] __dev_xmit_skb() → sch_direct_xmit() → dev_hard_start_xmit() └── [无队列策略] dev_hard_start_xmit() └── ops->ndo_start_xmit() = vlan_dev_hard_start_xmit() 🔹最终调用位置 ``` ### 关键验证点 在 VLAN 设备发送函数中会显式添加 VLAN 标签: ```c // net/8021q/vlan_dev.c static netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff *skb, ...) { if (!skb_vlan_tag_present(skb)) { // 🔹添加 VLAN 标签 skb = __vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), vlan_id); } skb->dev = vlan->real_dev; // 重定向到物理设备 dev_queue_xmit(skb); // 继续发送流程 } ``` ### 结论 在 `dev_queue_xmit` 函数实现中,VLAN 设备发送函数 `vlan_dev_hard_start_xmit` 的调用发生在: 1. **直接路径**:通过 `dev_hard_start_xmit()` → `ops->ndo_start_xmit()` 2. **队列路径**:通过 `sch_direct_xmit()` → `dev_hard_start_xmit()` → `ops->ndo_start_xmit()` 最终通过设备操作结构 `net_device_ops.ndo_start_xmit` 动态分发到 VLAN 设备的发送函数[^1][^3][^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值