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

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

1. 前言

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

2. 驱动回调函数注册

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

  1. 发送数据(ndo_start_xmit
  2. 获取统计信息(ndo_get_stats64
  3. 处理设备 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);

上面的代码所做的一些事情:

  1. 声明变量并初始化
  2. 使用 IGB_TX_DESC 获取下一个可用描述符的指针
  3. igb_tx_olinfo_status 函数更新 tx_flags,并将它们复制到描述符(tx_desc)中
  4. 计算 skb 头长度和数据长度
  5. 调用 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;

上面的代码做了一些重要的事情:

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

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

  1. Linux内核网络数据发送(五)——排队规则

    Linux内核网络数据发送(五)--排队规则 1. 前言 2. `qdisc_run_begin()` and `qdisc_run_end()`:仅设置 qdisc 状态位 3. `__qdisc_ ...

  2. Linux内核网络数据包发送(四)——Linux netdevice 子系统

    Linux内核网络数据包发送(四)--Linux netdevice 子系统 1. 前言 2. `dev_queue_xmit` and `__dev_queue_xmit` 2.1 `netdev_ ...

  3. Linux内核网络数据包发送(三)——IP协议层分析

    Linux内核网络数据包发送(三)--IP协议层分析 1. 前言 2. `ip_send_skb` 3. `ip_local_out` and `__ip_local_out` 3.1 netfilt ...

  4. Linux内核网络数据包发送(二)——UDP协议层分析

    Linux内核网络数据包发送(二)--UDP协议层分析 1. 前言 2. `udp_sendmsg` 2.1 UDP corking 2.2 获取目的 IP 地址和端口 2.3 Socket 发送:b ...

  5. Linux内核网络数据包发送(一)

    Linux内核网络数据包发送(一) 1. 前言 2. 数据包发送宏观视角 3. 协议层注册 4. 通过 socket 发送网络数据 4.1 `sock_sendmsg`, `__sock_sendms ...

  6. Linux内核网络数据包处理流程

    Linux内核网络数据包处理流程 from kernel-4.9: 0. Linux内核网络数据包处理流程 - 网络硬件 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO. ...

  7. linux内核网络协议栈--发送流程及函数(十)

    本章会一步一步的分析,在linux内核中,数据是如何从网络中接收并最后到达应用程序的. 用户数据的发送流程如下图所示,不管是tfp,telnet,http都是类似的.当然我们在使用应用的时候,根本不会 ...

  8. linux 内核 发送数据,linux 内核tcp数据发送的实现

    在分析之前先来看下SO_RCVTIMEO和SO_SNDTIMEO套接口吧,前面分析代码时没太注意这两个.这里算是个补充. SO_RCVTIMEO和SO_SNDTIMEO套接口选项可以给套接口的读和写, ...

  9. Linux内核网络协议栈:udp数据包发送(源码解读)

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

最新文章

  1. Spring+SpringMVC +MyBatis整合配置文件案例66666
  2. matlab转向梯形优化设计,转向梯形优化设计matlab程序.doc
  3. Java之美[从菜鸟到高手演变]之Spring源码学习 - 环境搭建
  4. 技术周刊(2019-02-11 Serverless)
  5. linux服务器健康检查,Linux 检查硬盘健康状态
  6. C/C++线程与多线程工作笔记003---C++指针引用和解引用
  7. 和could的区别用法_高考英语语法情态动词用法指南
  8. java常见异常_译文最常见的10种Java异常问题
  9. java左移负数位_java的左移运算符和右移运算符
  10. [收藏】正确使用SqlConnection对象,兼谈数据库连接池
  11. 按键精灵手机助手界面三级联动
  12. 【Markdown】希腊字母表
  13. 生物信息学常用的在线网站及数据库汇总
  14. Verdi调整字体大小
  15. 点云配准算法ICP及其各种变体
  16. win10平板读写TF卡异常的解决记录
  17. Scala 中的 特质(trait)
  18. 如何查看CentOS7的版本信息
  19. 杰理之关于 SPI 主机配置参数的几个说明【篇】
  20. 程序员入门有年龄限制吗?

热门文章

  1. 对使用Jolt调用Tuxedo的一个简单的封装
  2. Thinking in C++ Notes 名字控制
  3. 【转】txt中导入数据,matlab画图问题
  4. 网络犯罪:令牌化和区块链将终结数据盗窃行为?
  5. Residual Networks
  6. Android混淆代码
  7. FuncT,TResult的使用方法(转载)
  8. Flex的NumericStepper控件
  9. 微软Citrix合作虚拟化联手对抗VMware
  10. java安全编码实践总结