700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Linux内核网络数据发送(六)——网络设备驱动

Linux内核网络数据发送(六)——网络设备驱动

时间:2019-08-28 10:20:52

相关推荐

Linux内核网络数据发送(六)——网络设备驱动

Linux内核网络数据发送(六)——网络设备驱动

1. 前言2. 驱动回调函数注册3. `ndo_start_xmit` 发送数据4. `igb_tx_map`

1. 前言

本文主要介绍设备通过 DMA 从 RAM 中读取数据并将其发送到网络,主要分析dev_hard_start_xmit通过调用ndo_start_xmit来发送数据的过程。

2. 驱动回调函数注册

驱动程序实现了一系列方法来支持设备操作,例如:

发送数据(ndo_start_xmit)获取统计信息(ndo_get_stats64)处理设备ioctlsndo_do_ioctl

这些方法通过一个struct net_device_ops实例导出。看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_netdev_ops变量在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 ... */}

3.ndo_start_xmit发送数据

上层的网络栈通过struct 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中,以下代码在整个执行过程中都 hold 着一个锁:

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宏来计算发送 skb 所需的描述符数量,用count变量表示。然后根据分片情况,对count进行相应调整。

if (igb_maybe_stop_tx(tx_ring, count + 3)) {/* this is a hard error */return NETDEV_TX_BUSY;}

然后驱动程序调用内部函数igb_maybe_stop_tx,检查 TX Queue 以确保有足够可用的描述符。如果没有,则返回NETDEV_TX_BUSY。这将导致 qdisc 将 skb 重新入队以便稍后重试。

/* 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;

然后,获取 TX Queue 中下一个可用缓冲区信息,用struct igb_tx_buffer *first表 示,这个信息稍后将用于设置缓冲区描述符。数据包skb指针及其大小skb->len也存储到first

skb_tx_timestamp(skb);

接下来代码调用skb_tx_timestamp,获取基于软件的发送时间戳。应用程序可以 使用发送时间戳来确定数据包通过网络栈的发送路径所花费的时间。某些设备还支持硬件时间戳,这允许系统将打时间戳任务 offload 到设备。程序员因此可以 获得更准确的时间戳,因为它更接近于硬件实际发送的时间。

某些网络设备可以使用Precision Time Protocol(PTP,精确时间协议)在硬件中为数据包加时间戳。驱动程序处理用户的硬件时间戳请求。现在看这个代码:

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变量稍后将被复制到缓冲区信息结构中。

当前的jiffies值赋给ptp_tx_start。驱动程序中的其他代码将使用这个值, 以确保 TX 硬件打时间戳不会 hang 住。最后,如果这是一个 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;

最后将tx_flagsprotocol值都保存到first变量里面。

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来 处理校验和 offload 信息(是否需要 offload,是否支持此协议的 offload)。igb_tx_csum函数将检查 skb 的属性,修改first变量中的一些标志位,以表示需要校验和 offload。

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;

发送结束之后,驱动要检查确保有足够的描述符用于下一次发送。如果不够,TX Queue 将被 关闭。最后返回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。发送没有成功,但驱动程序释放了相关资源,没有什么需要做的了。在这种情况下,此驱动程序不会增加 drop 计数,但或许它应该增加。

4.igb_tx_map

igb_tx_map函数处理将 skb 数据映射到 RAM 的 DMA 区域的细节。它还会更新设备 TX Queue 的 尾部指针,从而触发设备“被唤醒”,从 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)中计算 skb 头长度和数据长度调用dma_map_singleskb->data构造内存映射,以允许设备通过 DMA 从 RAM 中读取数据

接下来是驱动程序中的一个非常长的循环,用于为 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);

对最后一个描述符设置RSEOP位,以提示设备这是最后一个描述符了。

netdev_tx_sent_queue(txring_txq(tx_ring), first->bytecount);/* set the timestamp */first->time_stamp = jiffies;

调用netdev_tx_sent_queue函数,同时带着将发送的字节数作为参数。这个函数是 byte query limit(字节查询限制)功能的一部分,当前的 jiffies 存储到first的时间戳字段。

/* 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函数强制完成内存写入。这通常称作**“写屏障”**(write barrier) ,是通过 CPU 平台相关的特殊指令完成的。这对某些 CPU 架构非常重要,因为如果触发 设备启动 DMA 时不能确保所有内存写入已经完成,那设备可能从 RAM 中读取不一致 状态的数据。设置next_to_watch字段,它将在 completion 阶段后期使用更新计数,并且 TX Queue 的next_to_use字段设置为下一个可用的描述符。使用writel函数更新 TX Queue 的尾部。writel向内存映射 I/O地址写入一个long型数据 ,这里地址是tx_ring->tail(一个硬件地址),要写入的值是i。这次写操作会让 设备知道其他数据已经准备好,可以通过 DMA 从 RAM 中读取并写入网络最后,调用mmiowb函数。它执行特定于 CPU 体系结构的指令,对内存映射的 写操作进行排序。它也是一个写屏障,用于内存映射的 I/O 写

最后,代码包含了一些错误处理。只有 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;

参考资料:https://blog.packagecloud.io/eng//02/06/monitoring-tuning-linux-networking-stack-sending-data

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。