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
) - 处理设备
ioctls
(ndo_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_flags
和 protocol
值都保存到 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_single
为skb->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);
对最后一个描述符设置 RS
和 EOP
位,以提示设备这是最后一个描述符了。
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/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data
Linux内核网络数据发送(六)——网络设备驱动相关推荐
- Linux内核网络数据发送(五)——排队规则
Linux内核网络数据发送(五)--排队规则 1. 前言 2. `qdisc_run_begin()` and `qdisc_run_end()`:仅设置 qdisc 状态位 3. `__qdisc_ ...
- Linux内核网络数据包发送(四)——Linux netdevice 子系统
Linux内核网络数据包发送(四)--Linux netdevice 子系统 1. 前言 2. `dev_queue_xmit` and `__dev_queue_xmit` 2.1 `netdev_ ...
- Linux内核网络数据包发送(三)——IP协议层分析
Linux内核网络数据包发送(三)--IP协议层分析 1. 前言 2. `ip_send_skb` 3. `ip_local_out` and `__ip_local_out` 3.1 netfilt ...
- Linux内核网络数据包发送(二)——UDP协议层分析
Linux内核网络数据包发送(二)--UDP协议层分析 1. 前言 2. `udp_sendmsg` 2.1 UDP corking 2.2 获取目的 IP 地址和端口 2.3 Socket 发送:b ...
- Linux内核网络数据包发送(一)
Linux内核网络数据包发送(一) 1. 前言 2. 数据包发送宏观视角 3. 协议层注册 4. 通过 socket 发送网络数据 4.1 `sock_sendmsg`, `__sock_sendms ...
- Linux内核网络数据包处理流程
Linux内核网络数据包处理流程 from kernel-4.9: 0. Linux内核网络数据包处理流程 - 网络硬件 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO. ...
- linux内核网络协议栈--发送流程及函数(十)
本章会一步一步的分析,在linux内核中,数据是如何从网络中接收并最后到达应用程序的. 用户数据的发送流程如下图所示,不管是tfp,telnet,http都是类似的.当然我们在使用应用的时候,根本不会 ...
- linux 内核 发送数据,linux 内核tcp数据发送的实现
在分析之前先来看下SO_RCVTIMEO和SO_SNDTIMEO套接口吧,前面分析代码时没太注意这两个.这里算是个补充. SO_RCVTIMEO和SO_SNDTIMEO套接口选项可以给套接口的读和写, ...
- Linux内核网络协议栈:udp数据包发送(源码解读)
<监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...
最新文章
- Spring+SpringMVC +MyBatis整合配置文件案例66666
- matlab转向梯形优化设计,转向梯形优化设计matlab程序.doc
- Java之美[从菜鸟到高手演变]之Spring源码学习 - 环境搭建
- 技术周刊(2019-02-11 Serverless)
- linux服务器健康检查,Linux 检查硬盘健康状态
- C/C++线程与多线程工作笔记003---C++指针引用和解引用
- 和could的区别用法_高考英语语法情态动词用法指南
- java常见异常_译文最常见的10种Java异常问题
- java左移负数位_java的左移运算符和右移运算符
- [收藏】正确使用SqlConnection对象,兼谈数据库连接池
- 按键精灵手机助手界面三级联动
- 【Markdown】希腊字母表
- 生物信息学常用的在线网站及数据库汇总
- Verdi调整字体大小
- 点云配准算法ICP及其各种变体
- win10平板读写TF卡异常的解决记录
- Scala 中的 特质(trait)
- 如何查看CentOS7的版本信息
- 杰理之关于 SPI 主机配置参数的几个说明【篇】
- 程序员入门有年龄限制吗?