驱动操作注册
驱动程序为各种操作实现一系列功能,例如:
- 发送数据(
ndo_start_xmit
) - 获取统计信息(
ndo_get_stats64
) - 处理设备
ioctls
(ndo_do_ioctl
) - 还有更多。
函数被导出为一系列排列在结构中的函数指针。 让我们来看看 igb
驱动程序源代码中这些操作的结构定义:
static const struct net_device_ops igb_netdev_ops = { .ndo_open = igb_open, .ndo_stop = igb_close, .ndo_start_xmit = igb_xmit_frame, .ndo_get_stats64 = igb_get_stats64, /* ... more fields ... */ };
此结构在 igb_probe
函数中注册:
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { /* ... lots of other stuff ... */ netdev->netdev_ops = &igb_netdev_ops; /* ... more code ... */ }
正如我们在上一节中看到的,更高层的代码将获得对设备的 netdev_ops
结构的引用,并调用相应的函数。 如果你想了解更多关于 PCI 设备是如何启动的,以及何时/何地调用 igb_probe
的信息,请查看我们的其他博客文章中的驱动程序初始化部分。
使用 ndo_start_xmit
传输数据
网络栈的较高层使用 net_device_ops
结构调用驱动程序来执行各种操作。 正如我们前面看到的,qdisc 代码调用 ndo_start_xmit
传递数据给驱动程序进行传输。 对于大多数硬件设备,ndo_start_xmit
函数在锁被持有时被调用,正如我们上面看到的。
在 igb
设备驱动程序中,注册到 ndo_start_xmit
称为 igb_xmit_frame
,因此让我们从igb_xmit_frame
开始,了解此驱动程序如何传输数据。 进入 ./drivers/net/ethernet/intel/igb/igb_main.c ,并记住,在执行以下代码的整个过程中,都会持有一个锁:
netdev_tx_t igb_xmit_frame_ring(struct sk_buff *skb, struct igb_ring *tx_ring) { struct igb_tx_buffer *first; int tso; u32 tx_flags = 0; u16 count = TXD_USE_COUNT(skb_headlen(skb)); __be16 protocol = vlan_get_protocol(skb); u8 hdr_len = 0; /* need: 1 descriptor per page * PAGE_SIZE/IGB_MAX_DATA_PER_TXD, * + 1 desc for skb_headlen/IGB_MAX_DATA_PER_TXD, * + 2 desc gap to keep tail from touching head, * + 1 desc for context descriptor, * otherwise try next time */ if (NETDEV_FRAG_PAGE_MAX_SIZE > IGB_MAX_DATA_PER_TXD) { unsigned short f; for (f = 0; f < skb_shinfo(skb)->nr_frags; f++) count += TXD_USE_COUNT(skb_shinfo(skb)->frags[f].size); } else { count += skb_shinfo(skb)->nr_frags; }
该函数开始使用 TXD_USER_COUNT
宏来确定需要多少个传输描述符来传输传入的数据。 count
值初始化为适合 skb 的描述符数量。 然后考虑需要传输的任何附加片段,对其进行调整。
if (igb_maybe_stop_tx(tx_ring, count + 3)) { /* this is a hard error */ return NETDEV_TX_BUSY; }
然后驱动程序调用一个内部函数 igb_maybe_stop_tx
,该函数检查所需的描述符数量,以确保传输队列有足够的可用资源。 如果没有,则在此处返回 NETDEV_TX_BUSY
。 正如我们前面在 qdisc 代码中看到的,这将导致 qdisc 重新排队数据以便稍后重试。
/* record the location of the first descriptor for this packet */ first = &tx_ring->tx_buffer_info[tx_ring->next_to_use]; first->skb = skb; first->bytecount = skb->len; first->gso_segs = 1;
然后,代码获得对传输队列中的下一个可用缓冲区信息的引用。 此结构将跟踪稍后设置缓冲区描述符所需的信息。 对数据包的引用及其大小被复制到缓冲区信息结构中。
skb_tx_timestamp(skb);
上面的代码调用 skb_tx_timestamp
获得基于软件的发送时间戳。 应用程序可以使用发送时间戳来确定数据包通过网络栈的传输路径所花费的时间量。
一些设备还支持为在硬件中传输的数据包生成时间戳。 这允许系统卸载时间戳到设备,并且它允许程序员获得更准确的时间戳,因为它将更接近硬件的实际传输发生的时间。 现在我们来看看这段代码:
if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)) { struct igb_adapter *adapter = netdev_priv(tx_ring->netdev); if (!(adapter->ptp_tx_skb)) { skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS; tx_flags |= IGB_TX_FLAGS_TSTAMP; adapter->ptp_tx_skb = skb_get(skb); adapter->ptp_tx_start = jiffies; if (adapter->hw.mac.type == e1000_82576) schedule_work(&adapter->ptp_tx_work); } }
一些网络设备可以使用精确时间协议在硬件中对数据包加时间戳。 当用户请求硬件时间戳时,驱动程序代码将在此处处理此问题。
上面的 if
语句检查 SKBTX_HW_TSTAMP
标志。 此标志指示用户请求了硬件时间戳。 如果用户请求了硬件时间戳,代码接下来检查是否设置 ptp_tx_skb
。 一次可以对一个数据包加时间戳,,因此在此处获取正在进行时间戳的数据包的引用,并在 skb 上设置 SKBTX_IN_PROGRESS
标志。 更新 tx_flags
以标记 IGB_TX_FLAGS_TSTAMP
标志。 变量稍后复制 tx_flags
到 buffer info 结构中。
获取 skb 的引用,复制当前 jiffies 计数到 ptp_tx_start
。驱动程序中的其他代码将使用此值来确保 TX 硬件时间戳不会挂起。最后,如果这是一个 82576
以太网硬件适配器,则使用 schedule_work
函数来启动 工作队列。
if (vlan_tx_tag_present(skb)) { tx_flags |= IGB_TX_FLAGS_VLAN; tx_flags |= (vlan_tx_tag_get(skb) << IGB_TX_FLAGS_VLAN_SHIFT); }
上面的代码检查是否设置了 skb 的 vlan_tci
字段。 如果已设置,则启用IGB_TX_FLAGS_VLAN
标志并存储 vlan ID。
/* record initial flags and protocol */ first->tx_flags = tx_flags; first->protocol = protocol;
标志和协议被记录到缓冲区信息结构。
tso = igb_tso(tx_ring, first, &hdr_len); if (tso < 0) goto out_drop; else if (!tso) igb_tx_csum(tx_ring, first);
接下来,驱动程序调用其内部函数 igb_tso
。 此函数确定 skb 是否需要分段。 如果是,则缓冲器信息引用(first
)更新其标志以向硬件指示需要 TSO。
如果 tso 不必要,igb_tso
将返回 0
,否则返回 1
。 如果返回 0
,igb_tx_csum
来处理启用校验和卸载(如果需要并且该协议支持)。 igb_tx_csum
函数检查 skb 的属性,并首先翻转 缓冲区 first
中的一些标志位,以指示需要卸载校验和。
igb_tx_map(tx_ring, first, hdr_len);
调用 igb_tx_map
函数来准备设备要消耗的数据以进行传输。 接下来我们将详细研究这个函数。
/* Make sure there is space in the ring for the next send. */ igb_maybe_stop_tx(tx_ring, DESC_NEEDED); return NETDEV_TX_OK;
传输完成后,驱动程序进行检查,以确保有足够的空间可用于另一次传输。 如果没有,则关闭队列。 在任何一种情况下,NETDEV_TX_OK
都会返回到更高层(qdisc 代码)。
out_drop: igb_unmap_and_free_tx_resource(tx_ring, first); return NETDEV_TX_OK; }
最后是一些错误处理代码。 这段代码只在 igb_tso
遇到某种错误时才被命中。 igb_unmap_and_free_tx_resource
清理数据。在这种情况下也返回 NETDEV_TX_OK
。 传输不成功,但驱动程序释放了关联的资源,没有什么可做的了。 请注意,在这种情况下,此驱动程序不会增加数据包丢弃,但它可能应该这样做。
igb_tx_map
igb_tx_map
函数处理映射 skb 数据到 RAM 的可 DMA 区域的细节。 它还更新设备上的传输队列的尾指针,这是触发设备“唤醒”、从 RAM 获取数据,并开始传输数据。
让我们简单地看看这个函数是如何工作的:
static void igb_tx_map(struct igb_ring *tx_ring, struct igb_tx_buffer *first, const u8 hdr_len) { struct sk_buff *skb = first->skb; /* ... other variables ... */ u32 tx_flags = first->tx_flags; u32 cmd_type = igb_tx_cmd_type(skb, tx_flags); u16 i = tx_ring->next_to_use; tx_desc = IGB_TX_DESC(tx_ring, i); igb_tx_olinfo_status(tx_ring, tx_desc, tx_flags, skb->len - hdr_len); size = skb_headlen(skb); data_len = skb->data_len; dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);
上面的代码做了几件事:
- 声明一组变量并初始化它们。
- 使用
IGB_TX_DESC
宏确定获取下一个可用描述符的引用。 igb_tx_olinfo_status
更新tx_flags
并复制其到描述符(tx_desc
)中。- 捕获大小和数据长度,以便稍后使用。
dma_map_single
构造获得skb->data
数据的 DMA 可访问地址所需的任何内存映射。 这样做使得设备可以从存储器读取数据包数据。
接下来是驱动程序中的一个非常密集的循环,为 skb 的每个片段生成有效的映射。 具体如何发生这种情况的细节并不特别重要,但值得一提:
- 驱动程序遍历数据包片段的集合。
- 当前描述符中填入数据的 DMA 地址。
- 如果片段的大小大于单个IGB描述符可以传输的大小,则构造多个描述符以指向可DMA区域的块,直到描述符指向整个片段。
- 增加描述符迭代器。
- 减少剩余长度。
- 当出现以下情况时,循环终止:没有剩余片段或者整个数据长度已经被消耗。
以下提供循环的代码,以供参考以上描述。 这应该进一步向读者说明,如果可能的话,避免碎片化是一个好主意。 需要在堆栈的每一层运行大量额外的代码来处理它,包括驱动程序。
tx_buffer = first; for (frag = &skb_shinfo(skb)->frags[0];; frag++) { if (dma_mapping_error(tx_ring->dev, dma)) goto dma_error; /* record length, and DMA address */ dma_unmap_len_set(tx_buffer, len, size); dma_unmap_addr_set(tx_buffer, dma, dma); tx_desc->read.buffer_addr = cpu_to_le64(dma); while (unlikely(size > IGB_MAX_DATA_PER_TXD)) { tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type ^ IGB_MAX_DATA_PER_TXD); i++; tx_desc++; if (i == tx_ring->count) { tx_desc = IGB_TX_DESC(tx_ring, 0); i = 0; } tx_desc->read.olinfo_status = 0; dma += IGB_MAX_DATA_PER_TXD; size -= IGB_MAX_DATA_PER_TXD; tx_desc->read.buffer_addr = cpu_to_le64(dma); } if (likely(!data_len)) break; tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type ^ size); i++; tx_desc++; if (i == tx_ring->count) { tx_desc = IGB_TX_DESC(tx_ring, 0); i = 0; } tx_desc->read.olinfo_status = 0; size = skb_frag_size(frag); data_len -= size; dma = skb_frag_dma_map(tx_ring->dev, frag, 0, size, DMA_TO_DEVICE); tx_buffer = &tx_ring->tx_buffer_info[i]; }
一旦所有必要的描述符都已构建,并且所有 skb 的数据都已映射到 DMA 地址,驱动程序将继续执行其最后步骤以触发传输:
/* write last descriptor with RS and EOP bits */ cmd_type |= size | IGB_TXD_DCMD; tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type);
写入终止描述符以向设备指示它是最后一个描述符。
netdev_tx_sent_queue(txring_txq(tx_ring), first->bytecount); /* set the timestamp */ first->time_stamp = jiffies;
调用 netdev_tx_sent_queue
函数时,会添加字节数到此传输队列。 这个函数是字节查询限制特性的一部分,我们稍后会详细介绍。 当前 jiffies 被存储在第一缓冲器信息结构中。
接下来,有一点棘手:
/* Force memory writes to complete before letting h/w know there * are new descriptors to fetch. (Only applicable for weak-ordered * memory model archs, such as IA-64). * * We also need this memory barrier to make certain all of the * status bits have been updated before next_to_watch is written. */ wmb(); /* set next_to_watch value indicating a packet is present */ first->next_to_watch = tx_desc; i++; if (i == tx_ring->count) i = 0; tx_ring->next_to_use = i; writel(i, tx_ring->tail); /* we need this if more than one processor can write to our tail * at a time, it synchronizes IO on IA64/Altix systems */ mmiowb(); return;
上面的代码正在执行一些重要的操作:
- 首先调用
wmb
函数强制完成内存写入。这将作为适用于 CPU 平台的特殊指令执行,通常称为“写屏障”。这在某些 CPU 架构上很重要,因为如果我们在没有确保所有更新内部状态的内存写入都已完成之前触发设备启动 DMA,则设备可能会从 RAM 中读取不一致状态的数据。这篇文章 和这个 讲座 深入探讨了有关内存排序的细节。 - 设置
next_to_watch
字段。它将在完成阶段后使用。 - 增加计数器,并更新传输队列的
next_to_use
字段为下一个可用描述符。 - 使用
writel
函数更新传输队列的尾部。writel
将一个 “long” 写入 内存映射 I/O 地址。在这种情况下,地址是tx_ring->tail
(这是一个硬件地址),要写入的值是i
。此写入会触发设备,让它知道有更多数据准备好从 RAM 进行 DMA 并写入网络。 - 最后,调用
mmiowb
函数。此函数将执行适用于 CPU 架构的指令,使内存映射写入操作有序。它也是一个写屏障,但用于内存映射 I/O 写入。
如果您想了解更多关于 wmb
、mmiowb
以及何时使用它们,可以阅读 Linux 内核中包含的一些出色的 关于内存屏障的文档。
最后,只有当从 DMA API 返回错误时(当尝试映射 skb 数据地址到可 DMA 地址时),才会执行此代码。
dma_error: dev_err(tx_ring->dev, "TX DMA map failed\n"); /* clear dma mappings for failed tx_buffer_info map */ for (;;) { tx_buffer = &tx_ring->tx_buffer_info[i]; igb_unmap_and_free_tx_resource(tx_ring, tx_buffer); if (tx_buffer == first) break; if (i == 0) i = tx_ring->count; i--; } tx_ring->next_to_use = i;
在继续传输完成之前,让我们检查一下上面传递的内容:动态队列限制。
动态队列限制(DQL)
正如你在这篇文章中看到的那样,随着网络数据越来越靠近传输设备,它会在不同阶段花费大量时间排队。随着队列大小的增加,数据包在未传输的队列中停留的时间更长,即数据包传输延迟随着队列大小增加而增加。
对抗这种情况的一种方法是背压。动态队列限制(DQL)系统是一种机制,设备驱动程序可以使用该机制向网络系统施加背压,
要使用此系统,网络设备驱动程序需要在其传输和完成例程期间进行一些简单的 API 调用。 DQL 系统内部使用一种算法来确定何时有足够的数据传输。 一旦达到此限制,传输队列将暂时禁用。 这种队列禁用是对网络系统产生背压的原因。当DQL系统确定有足够的数据完成传输时,队列将自动重新启用。
查看这组关于 DQL 系统的优秀幻灯片,了解一些性能数据和 DQL 内部算法的解释。
我们刚才看到的代码中调用的函数 netdev_tx_sent_queue
是 DQL API 的一部分。 当数据排队到设备进行传输时,会调用此函数。 传输完成后,驱动程序调用 就会调用 netdev_tx_completed_queue
。 在内部,这两个函数都将调用 DQL 库(位于 ./lib/dynamic_queue_limits.c 和 ./include/linux/dynamic_queue_limits.h 中),以确定传输队列是否应该被禁用、重新启用或保持原样。
DQL 在 sysfs 中导出统计信息和调优旋钮。 调优 DQL 应该是不必要的;该算法将随时间调整其参数。 不过,为了完整起见,我们将在后面看到如何监控和调优 DQL。
传输完成
一旦设备传输了数据,它将产生一个中断信号,表示传输完成。 然后设备驱动程序可以安排一些长时间运行的工作来完成,比如取消映射内存区域和释放数据。 具体如何工作取决于设备。 在 igb
驱动程序(及其相关设备)的情况下,发射相同的 IRQ 以完成传输和接收数据包。 这意味着对于 igb
驱动程序,NET_RX
处理发送完成和传入数据包接收。
让我重申这一点,以强调其重要性:您的设备可能会在接收数据包时发出与发送数据包完成信号相同的中断。如果是,NET_RX
软中断将运行处理传入数据包和传输完成。
由于两个操作共享同一个 IRQ,因此只能注册一个 IRQ 处理函数,并且它必须处理两种可能的情况。 当接收到网络数据时,调用以下流程:
- 接收网络数据。
- 网络设备引发 IRQ。
- 设备驱动程序的 IRQ 处理程序执行,清除 IRQ 并确保 softIRQ 被调度运行(如果尚未运行)。 这里触发的软中断是
NET_RX
软中断。 - 软中断本质上是作为一个单独的内核线程执行的。 它运行并实现 NAPI 轮询循环。
- NAPI 轮询循环只是一段代码,只要有足够的预算,它就在循环中执行,收集数据包。
- 每次处理数据包时,预算都会减少,直到没有更多的数据包要处理,预算达到 0,或者时间片到期为止。
igb
驱动程序(和 ixgbe
驱动程序[greetings,tyler])中的上述步骤 5 在处理传入数据之前处理传输完成。 请记住,根据驱动程序的实现,传输完成和传入数据的处理功能可能共享相同的处理预算。 igb
和 ixgbe
驱动器分别跟踪传输完成和传入数据包预算,因此处理传输完成将不一定耗尽传入预算。
也就是说,整个 NAPI 轮询循环在硬编码的时间片内运行。 这意味着,如果要处理大量的传输完成处理,传输完成可能会比处理传入数据占用更多的时间片。 对于那些在非常高的负载环境中运行网络硬件的人来说,这可能是一个重要的考虑因素。
让我们看看 igb
驱动程序在实践中是如何做到这一点的。
传输完成 IRQ
这篇文章将不再重复Linux 内核接收端网络博客文章中已经涵盖的信息,而是按顺序列出步骤,并链接到接收端博客文章中的相应部分,直到传输完成。
所以,让我们从头开始:
- 网络设备启动。
- IRQ 处理程序已注册。
- 用户程序发送数据到网络套接字。 数据在网络栈中传输,直到设备从内存中获取数据并将其传输。
- 设备完成数据传输并引发 IRQ 以通知传输完成。
- 驱动程序的IRQ 处理程序执行以处理中断。
- IRQ 处理程序调用
napi_schedule
来响应 IRQ。 - NAPI 代码 触发
NET_RX
软中断执行。 NET_RX
软中断函数net_rx_action
开始执行。net_rx_action
函数调用驱动程序注册的 NAPI 轮询函数。- 执行 NAPI 轮询函数
igb_poll
。
轮询函数 igb_poll
是代码分离并处理传入数据包和传输完成的地方。 让我们深入研究这个函数的代码,看看它在哪里发生的。
igb_poll
让我们来看看 igb_poll
(来自 ./drivers/net/ethernet/intel/igb/igb_main.c):
/** * igb_poll - NAPI Rx polling callback * @napi: napi polling structure * @budget: count of how many packets we should handle **/ static int igb_poll(struct napi_struct *napi, int budget) { struct igb_q_vector *q_vector = container_of(napi, struct igb_q_vector, napi); bool clean_complete = true; #ifdef CONFIG_IGB_DCA if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED) igb_update_dca(q_vector); #endif if (q_vector->tx.ring) clean_complete = igb_clean_tx_irq(q_vector); if (q_vector->rx.ring) clean_complete &= igb_clean_rx_irq(q_vector, budget); /* If all work not completed, return budget and keep polling */ if (!clean_complete) return budget; /* If not enough Rx work done, exit the polling mode */ napi_complete(napi); igb_ring_irq_enable(q_vector); return 0; }
此函数执行几个操作,顺序如下:
- 如果在内核中启用了直接缓存访问(DCA)支持,则 CPU 缓存将预热,以便对 RX 环的访问将命中 CPU 缓存。 您可以在接收端网络帖子的附加部分阅读有关 DCA 的更多信息。
- 调用
igb_clean_tx_irq
,执行发送完成操作。 - 接下来调用
igb_clean_rx_irq
,其执行传入数据包处理。 - 最后,检查
clean_complete
以确定是否还有更多的工作可以完成。 如果是,则返回budget
。 如果发生这种情况,net_rx_action
移动这个 NAPI 结构到轮询列表的末尾,以便稍后再次处理。
要了解更多关于 igb_clean_rx_irq
工作原理,请阅读上一篇博客文章的这一部分。
这篇博客文章主要关注发送端,所以我们将继续研究上面的 igb_clean_tx_irq
是如何工作的。
igb_clean_tx_irq
请查看 ./drivers/net/ethernet/intel/igb/igb_main.c 中此函数的源代码。
它有点长,所以我们把它分成块并研究它:
static bool igb_clean_tx_irq(struct igb_q_vector *q_vector) { struct igb_adapter *adapter = q_vector->adapter; struct igb_ring *tx_ring = q_vector->tx.ring; struct igb_tx_buffer *tx_buffer; union e1000_adv_tx_desc *tx_desc; unsigned int total_bytes = 0, total_packets = 0; unsigned int budget = q_vector->tx.work_limit; unsigned int i = tx_ring->next_to_clean; if (test_bit(__IGB_DOWN, &adapter->state)) return true;
该函数首先初始化一些有用的变量。 一个重要的考虑因素是 budget
。 正如你在上面看到的budget
被初始化为这个队列的 tx.work_limit
。 在 igb
驱动程序中,tx.work_limit
被初始化为硬编码值 IGB_DEFAULT_TX_WORK
(128)。
值得注意的是,虽然我们现在看到的传输完成代码与接收处理在相同的 NET_RX
软中断中运行,但 TX 和 RX 函数在 igb
驱动程序中并不共享处理预算 。由于整个 poll 函数在相同的时间片内运行,因此单次运行 igb_poll
函数不可能使传入的数据包处理或传输完成饿死。只要调用igb_poll
,两者都会被处理。
接下来,上面的代码片段以检查网络设备是否关闭结束。如果是,则返回 true
并退出igb_clean_tx_irq
。
tx_buffer = &tx_ring->tx_buffer_info[i]; tx_desc = IGB_TX_DESC(tx_ring, i); i -= tx_ring->count;
tx_buffer
变量被初始化为位于tx_ring->next_to_clean
(其本身被初始化为0
)的传输缓冲区信息结构。- 获得相关联的描述符的引用,并将其存储在
tx_desc
。 - 计数器
i
减少发送队列的大小。 这个值可以调整(正如我们将在调优部分看到的那样),但是被初始化为IGB_DEFAULT_TXD
(256)。
接下来,循环开始。 它包括一些有用的注释,以解释每个步骤中发生的事情:
do { union e1000_adv_tx_desc *eop_desc = tx_buffer->next_to_watch; /* if next_to_watch is not set then there is no work pending */ if (!eop_desc) break; /* prevent any other reads prior to eop_desc */ read_barrier_depends(); /* if DD is not set pending work has not been completed */ if (!(eop_desc->wb.status & cpu_to_le32(E1000_TXD_STAT_DD))) break; /* clear next_to_watch to prevent false hangs */ tx_buffer->next_to_watch = NULL; /* update the statistics for this packet */ total_bytes += tx_buffer->bytecount; total_packets += tx_buffer->gso_segs; /* free the skb */ dev_kfree_skb_any(tx_buffer->skb); /* unmap skb header data */ dma_unmap_single(tx_ring->dev, dma_unmap_addr(tx_buffer, dma), dma_unmap_len(tx_buffer, len), DMA_TO_DEVICE); /* clear tx_buffer data */ tx_buffer->skb = NULL; dma_unmap_len_set(tx_buffer, len, 0);
- 首先,
eop_desc
被设置为缓冲区的next_to_watch
字段。这是在我们之前看到的传输代码中设置的。 - 如果
eop_desc
(eop = 数据包结束)为NULL
,则没有工作待处理。 - 调用
read_barrier_depends
函数,该函数将为此 CPU 架构执行适当的 CPU 指令,以防止读取被重新排序到此屏障之前。 - 接下来,在数据包结束描述符
eop_desc
中检查一个状态位。如果未设置E1000_TXD_STAT_DD
位,则传输尚未完成,因此从循环中退出。 - 清除
tx_buffer->next_to_watch
。驱动程序中的看门狗定时器将监视此字段以确定传输是否挂起。清除此字段将防止看门狗触发。 - 更新发送的总字节数和数据包数的统计计数器。一旦处理完所有描述符,复制这些到驱动程序读取的统计计数器中。
- 释放 skb。
- 使用
dma_unmap_single
取消映射 skb 数据区域。 - 设置
tx_buffer->skb
为NULL
并取消映射tx_buffer
。