Linux发送函数dev_queue_xmit

本文详细解析了Linux网络栈中的dev_queue_xmit函数工作流程,包括 skb 的线性化处理、校验和计算、发送队列的选择及入队操作,以及在不同场景下如何处理发送失败的情形。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当上层准备好一个包之后,交给下面这个函数处理:

int dev_queue_xmit(struct sk_buff *skb)     
{     
    struct net_device *dev = skb->dev;     
    struct netdev_queue *txq;     
    struct Qdisc *q;     
    int rc = -ENOMEM;     
    /* GSO will handle the following emulations directly. */    
    if (netif_needs_gso(dev, skb))     
        goto gso;     
    //首先判断skb是否被分段,如果分了段并且网卡不支持分散读的话需要将所有段重新组合成一个段     
    //这里__skb_linearize其实就是__pskb_pull_tail(skb, skb->data_len),这个函数基本上等同于pskb_may_pull     
    //pskb_may_pull的作用就是检测skb对应的主buf中是否有足够的空间来pull出len长度,     
    //如果不够就重新分配skb并将frags中的数据拷贝入新分配的主buff中,而这里将参数len设置为skb->datalen,     
    //也就是会将所有的数据全部拷贝到主buff中,以这种方式完成skb的线性化     
    if (skb_shinfo(skb)->frag_list &&     
        !(dev->features & NETIF_F_FRAGLIST) &&     
        __skb_linearize(skb))     
        goto out_kfree_skb;     
    /* Fragmented skb is linearized if device does not support SG,   
     * or if at least one of fragments is in highmem and device   
     * does not support DMA from it.   
     */    
     //如果上面已经线性化了一次,这里的__skb_linearize就会直接返回     
     //注意区别frags和frag_list,     
     //前者是将多的数据放到单独分配的页面中,sk_buff只有一个。而后者则是连接多个sk_buff     
    if (skb_shinfo(skb)->nr_frags &&     
        (!(dev->features & NETIF_F_SG) || illegal_highdma(dev, skb)) &&     
        __skb_linearize(skb))     
        goto out_kfree_skb;     
    /* 如果此包的校验和还没有计算并且驱动不支持硬件校验和计算,那么需要在这里计算校验和*/    
    if (skb->ip_summed == CHECKSUM_PARTIAL) {     
        skb_set_transport_header(skb, skb->csum_start -     
                          skb_headroom(skb));     
        if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb))     
            goto out_kfree_skb;     
    }     
gso:     
    /* Disable soft irqs for various locks below. Also   
     * stops preemption for RCU.   
     */    
    rcu_read_lock_bh();     
    //选择一个发送队列,如果设备提供了select_queue回调函数就使用它,否则由内核选择一个队列     
    //大部分驱动都不会设置多个队列,而是在调用alloc_etherdev分配net_device时将队列个数设置为1     
    //也就是只有一个队列     
    txq = dev_pick_tx(dev, skb);     
    //从netdev_queue结构上取下设备的qdisc     
    q = rcu_dereference(txq->qdisc);     
#ifdef CONFIG_NET_CLS_ACT     
    skb->tc_verd = SET_TC_AT(skb->tc_verd,AT_EGRESS);     
#endif     
    //上面说大部分驱动只有一个队列,但是只有一个队列也不代表设备准备使用它     
    //这里检查这个队列中是否有enqueue函数,如果有则说明设备会使用这个队列,否则需另外处理     
    //关于enqueue函数的设置,我找到dev_open->dev_activate中调用了qdisc_create_dflt来设置,     
    //不知道一般驱动怎么设置这个queue     
    //需要注意的是,这里并不是将传进来的skb直接发送,而是先入队,然后调度队列,     
    //具体发送哪个包由enqueue和dequeue函数决定,这体现了设备的排队规则     
    if (q->enqueue) {     
        spinlock_t *root_lock = qdisc_lock(q);     
        spin_lock(root_lock);     
        if (unlikely(test_bit(__QDISC_STATE_DEACTIVATED, &q->state))) {     
            kfree_skb(skb);     
            rc = NET_XMIT_DROP;     
        } else {     
            //将skb加入到设备发送队列中,然后调用qdisc_run来发送     
            rc = qdisc_enqueue_root(skb, q);     
            qdisc_run(q); //下面看     
        }     
        spin_unlock(root_lock);     
        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)   
     */    
    //要确定设备是开启的,下面还要确定队列是运行的。启动和停止队列由驱动程序决定     
    //详见ULNI中文版P251     
    //如上面英文注释所说,设备没有输出队列典型情况是回环设备     
    //我们所要做的就是直接调用驱动的hard_start_xmit将它发送出去     
    //如果发送失败就直接丢弃,因为没有队列可以保存它     
    if (dev->flags & IFF_UP) {     
        int cpu = smp_processor_id(); /* ok because BHs are off */    
        if (txq->xmit_lock_owner != cpu) {     
            HARD_TX_LOCK(dev, txq, cpu);     
            if (!netif_tx_queue_stopped(txq)) {     
                rc = 0;     
                //对于loopback设备,它的hard_start_xmit函数是loopback_xmit     
                //我们可以看到,在loopback_xmit末尾直接调用了netif_rx函数     
                //将带发送的包直接接收了回来     
                //这个函数下面具体分析,返回0表示成功,skb已被free     
                if (!dev_hard_start_xmit(skb, dev, txq)) {      
                    HARD_TX_UNLOCK(dev, txq);     
                    goto out;     
                }     
            }     
            HARD_TX_UNLOCK(dev, txq);     
            if (net_ratelimit())     
                printk(KERN_CRIT "Virtual device %s asks to "    
                       "queue packet!\n", dev->name);     
        } else {     
            /* Recursion is detected! It is possible,   
             * unfortunately */    
            if (net_ratelimit())     
                printk(KERN_CRIT "Dead loop on virtual device "    
                       "%s, fix it urgently!\n", dev->name);     
        }     
    }     
    rc = -ENETDOWN;     
    rcu_read_unlock_bh();     
out_kfree_skb:     
    kfree_skb(skb);     
    return rc;     
out:     
    rcu_read_unlock_bh();     
    return rc;     
}     

从此函数可以看出,当驱动使用发送队列的时候会循环从队列中取出包发送
而不使用队列的时候只发送一次,如果没发送成功就直接丢弃

int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,     
            struct netdev_queue *txq)     
{     
    if (likely(!skb->next)) {     
        //从这里可以看出,对于每一个发送的包也会给ptype_all一份,     
        //而packet套接字创建时对于proto为ETH_P_ALL的会在ptype_all中注册一个成员     
        //因此对于协议号为ETH_P_ALL的packet套接字来说,发送和接受的数据都能收到     
        //而其他成员似乎不行,这个要回去试试     
        if (!list_empty(&ptype_all))       
            dev_queue_xmit_nit(skb, dev);     
        if (netif_needs_gso(dev, skb)) {     
            if (unlikely(dev_gso_segment(skb)))     
                goto out_kfree_skb;     
            if (skb->next)     
                goto gso;     
        }     
        //这个就是驱动提供的发送回调函数了     
        return dev->hard_start_xmit(skb, dev);     
    }     
gso:     
    do {     
        struct sk_buff *nskb = skb->next;     
        int rc;     
        skb->next = nskb->next;     
        nskb->next = NULL;     
        rc = dev->hard_start_xmit(nskb, dev);     
        if (unlikely(rc)) {     
            nskb->next = skb->next;     
            skb->next = nskb;     
            return rc;     
        }     
        if (unlikely(netif_tx_queue_stopped(txq) && skb->next))     
            return NETDEV_TX_BUSY;     
    } while (skb->next);     
    skb->destructor = DEV_GSO_CB(skb)->destructor;     
out_kfree_skb:     
    kfree_skb(skb);     
    return 0;     
}    

qdisc_run和__qdisc_run的功能很简单,就是检查队列是否处于运行状态

static inline void qdisc_run(struct Qdisc *q)     
{     
    if (!test_and_set_bit(__QDISC_STATE_RUNNING, &q->state))     
        __qdisc_run(q);     
}     
void __qdisc_run(struct Qdisc *q)     
{     
    unsigned long start_time = jiffies;     
    //真正的操作在这个函数里面     
    while (qdisc_restart(q)) {     
        /*   
         * Postpone processing if   
         * 1. another process needs the CPU;   
         * 2. we've been doing it for too long.   
         */    
        if (need_resched() || jiffies != start_time) {     
            //当需要进行调度或者时间超过了1个时间片的时候就退出循环,退出之前发出软中断请求     
            __netif_schedule(q);     
            break;     
        }     
    }     
    clear_bit(__QDISC_STATE_RUNNING, &q->state);     
}    

然后循环调用qdisc_restart发送数据
下面这个函数qdisc_restart是真正发送数据包的函数
它从队列上取下一个帧,然后尝试将它发送出去
若发送失败则一般是重新入队。
此函数返回值为:发送成功时返回剩余队列长度
发送失败时返回0(若发送成功且剩余队列长度为0也返回0)

 static inline int qdisc_restart(struct Qdisc *q)     
{     
    struct netdev_queue *txq;     
    int ret = NETDEV_TX_BUSY;     
    struct net_device *dev;     
    spinlock_t *root_lock;     
    struct sk_buff *skb;     
    /* Dequeue packet */    
    if (unlikely((skb = dequeue_skb(q)) == NULL))     
        return 0;     
    root_lock = qdisc_lock(q);     
    /* And release qdisc */    
    spin_unlock(root_lock);     
    dev = qdisc_dev(q);     
    txq = netdev_get_tx_queue(dev, skb_get_queue_mapping(skb));     
    HARD_TX_LOCK(dev, txq, smp_processor_id());     
    if (!netif_tx_queue_stopped(txq) &&     
        !netif_tx_queue_frozen(txq))     
        ret = dev_hard_start_xmit(skb, dev, txq);     
    HARD_TX_UNLOCK(dev, txq);     
    spin_lock(root_lock);     
    switch (ret) {     
    case NETDEV_TX_OK:     
        /* Driver sent out skb successfully */    
        ret = qdisc_qlen(q);     
        break;     
    case NETDEV_TX_LOCKED:     
        /* Driver try lock failed */    
        //有可能其他CPU正占有这个锁,这里处理冲突     
        //函数就不看了,里面的处理流程简单说下     
        //分两种情况,如果占有锁的cpu就是当前cpu,那么释放这个包同时打印一条警告     
        //否则将包重新入队     
        ret = handle_dev_cpu_collision(skb, txq, q);     
        break;     
    default:     
        /* Driver returned NETDEV_TX_BUSY - requeue skb */    
        //当发送队列处于停止状态并且队列中有数据待发送时会返回NETDEV_TX_BUSY     
        //代表发送忙,这种情况下就将包重新入队     
        if (unlikely (ret != NETDEV_TX_BUSY && net_ratelimit()))     
            printk(KERN_WARNING "BUG %s code %d qlen %d\n",     
                   dev->name, ret, q->q.qlen);     
        ret = dev_requeue_skb(skb, q);     
        break;     
    }     
    if (ret && (netif_tx_queue_stopped(txq) ||     
            netif_tx_queue_frozen(txq)))     
        ret = 0;     
    return ret;     
}    

至此,dev_queue_xmit到驱动层的发送流程就分析完了。
已经有了dev_queue_xmit函数,为什么还需要软中断来发送呢?
我们可以看到在dev_queue_xmit中将skb进行了一些处理(比如合并成一个包,计算校验和等)
处理完的skb是可以直接发送的了,这时dev_queue_xmit也会先将skb入队(skb一般都是在这个函数中入队的)
并且调用qdisc_run尝试发送,但是有可能发送失败,这时就将skb重新入队,调度软中断,并且自己直接返回。
软中断只是发送队列中的skb以及释放已经发送的skb,它无需再对skb进行线性化或者校验和处理
另外在队列被停止的情况下,dev_queue_xmit仍然可以把包加入队列,但是不能发送
这样在队列被唤醒的时候就需要通过软中断来发送停止期间积压的包
简而言之,dev_queue_xmit是对skb做些最后的处理并且第一次尝试发送,软中断是将前者发送失败或者没发完的包发送出去。
(其实发送软中断还有一个作用,就是释放已经发送的包,因为某些情况下发送是在硬件中断中完成的,
为了提高硬件中断处理效率,内核提供一种方式将释放skb放到软中断中进行,
这时只要调用dev_kfree_skb_irq,它将skb加入softnet_data的completion_queue中,然后开启发送软中断,
net_tx_action会在软中断中将completion_queue中的skb全部释放掉)

<think>好的,用户想让我分析一下Linux 6.6内核中stmmac_main.c文件里的stmmac_xmit函数。首先,我需要回忆一下stmmac驱动的作用。stmmac是Synopsys设计的以太网控制器驱动,用于处理网络数据包的发送和接收。xmit函数通常是网络驱动中负责数据包发送的关键函数。 接下来,我应该确认stmmac_xmit函数的具体职责。通常,这个函数会被网络子系统调用,当有数据包需要发送时触发。它的主要任务可能包括准备描述符、处理DMA映射、检查资源是否足够,以及处理发送完成后的资源释放。 用户可能是在开发或调试网络驱动时遇到了问题,需要了解这个函数的工作流程,或者在进行性能优化。也有可能是学习内核网络驱动的实现方式。所以,我需要详细说明函数的结构、关键步骤以及可能涉及的子函数。 我需要考虑这个函数Linux 6.6中的具体实现是否有变化。例如,是否引入了新的特性或优化,比如使用Page Pool API来管理内存,或者对多队列的支持是否有改进。此外,是否需要关注并发处理,比如锁的使用,以确保在多核环境下的正确性。 然后,我应该分步骤分析这个函数。首先,函数可能从网络设备结构体中获取私有数据,然后检查描述符是否可用。如果队列已满,可能会停止队列。接着,处理数据包的分片情况,尤其是TSO(TCP Segmentation Offloading)的情况。然后,设置描述符的各个字段,如缓冲区地址、长度、OWN位等。最后,更新队列的索引,并处理可能的唤醒逻辑。 还需要注意DMA的相关操作,比如dma_map_single或dma_map_page的使用,确保数据正确传输到硬件。同时,错误处理部分,比如DMA映射失败时的处理,是否需要释放SKB或报告错误。 此外,需要提到可能调用的辅助函数,例如get_tx_timestamp_csum_status,用于处理时间戳或校验和卸载。还有,可能涉及的统计信息更新,比如发送成功或失败的计数。 用户可能还想知道这个函数在性能方面的考虑,比如如何减少锁竞争,是否使用NAPI机制,或者如何利用硬件特性来提高吞吐量。同时,可能存在的调试方法,比如使用tracepoints或动态调试来跟踪数据包的发送过程。 最后,总结一下这个函数的作用和重要性,确保用户能够理解它在整个驱动中的位置和功能。如果有必要,可以建议用户查阅相关的内核文档或源码注释,以获取更详细的信息。</think>以下是对Linux 6.6内核中`stmmac_xmit`函数的分析(位于`drivers/net/ethernet/stmicro/stmmac/stmmac_main.c`): --- ### 一、函数作用 `stmmac_xmit`是Synopsys以太网控制器驱动的核心数据发送函数,主要负责: 1. 将skb数据包映射到DMA传输描述符 2. 处理TCP分段卸载(TSO)和校验和计算卸载 3. 管理发送队列和描述符状态 4. 协调DMA引擎启动传输 --- ### 二、函数调用流程 ```c stmmac_xmit() ├── stmmac_get_tx_timestamp_csum_status() // 获取时间戳和校验状态 ├── stmmac_tso_xmit() // TSO分段处理 ├── stmmac_prepare_tx_desc() // 准备描述符 ├── dma_map_single()/dma_map_page() // DMA映射 ├── stmmac_set_tx_owner() // 设置描述符所有权 ├── netdev_tx_sent_queue() // 更新队列统计 └── stmmac_tx_timer_arm() // 启动传输定时器 ``` --- ### 三、关键代码分析 #### 1. 描述符获取 ```c entry = priv->tx_cur; if (priv->tx_path_in_lpi) stmmac_disable_eee_mode(priv, priv->ioaddr); ``` - 使用环形缓冲区管理发送队列 - 处理低功耗模式下发送的特殊情况 #### 2. TSO处理 ```c if (skb_is_gso(skb)) { if (stmmac_tso_xmit(skb, dev, desc, entry, (void *)desc) < 0) goto dma_map_err; } ``` - 当检测到GSO数据包时,调用分段处理 - 利用硬件TSO加速大包分片 #### 3. DMA映射 ```c mapped_addr = dma_map_single(priv->device, skb->data, copy_len, DMA_TO_DEVICE); if (dma_mapping_error(priv->device, mapped_addr)) goto dma_map_err; ``` - 使用`dma_map_single`或`dma_map_page`进行地址映射 - 处理可能的DMA映射错误 #### 4. 描述符配置 ```c stmmac_prepare_tx_desc(priv, desc, 1, len, csum_insertion, (skb->len == copy_len), mode, skb_shinfo(skb)->tx_flags, NS_TSO_EN); ``` - 设置分段标志、校验和插入位 - 配置时间戳请求位(PTP相关) --- ### 四、重要优化点 1. **零拷贝支持**:当数据包满足page大小对齐时,直接使用`dma_map_page`避免内存复制 2. **描述符重用**:采用环形缓冲区设计,描述符循环使用 3. **批处理优化**:支持多描述符连续填充,减少中断次数 4. **硬件加速**:充分利用MAC控制器的TSO、校验和卸载能力 --- ### 五、典型问题处理 1. **DMA映射失败**:触发`dma_map_err`分支,停止队列并返回`NETDEV_TX_BUSY` 2. **队列满检测**:通过`stmmac_tx_avail`检查可用描述符 3. **时间戳处理**:通过`skb_shinfo(skb)->tx_flags`判断是否需要硬件时间戳 --- ### 六、性能相关 1. 采用`NETIF_F_TSO`标志开启硬件分段 2. 使用`netdev_tx_sent_queue`进行BQL(Byte Queue Limits)管理 3. 通过`stmmac_tx_timer_arm`实现超时检测机制 建议结合`perf`工具分析函数执行热点,重点关注DMA映射和描述符操作的耗时。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值