linux协议栈中网卡相关的名词解释

摘自:http://blog.csdn.net/w_s_xin/article/details/11632089   和  http://blog.csdn.net/w_s_xin/article/details/11636651

这个文档介绍了Linux网络协议栈中一系列互补的技术。 

这些技术用来增加多处理器系统的并行性和改善性能

这些技术包括:

RSS: Receive Side Scaling (接收侧的缩放) 
RPS: Receive Packet Steering (接收端包的控制) 
RFS: Receive Flow Steering (接收端流的控制) 
Accelerated Receive Flow Steering (加速的接收端流的控制) 
XPS: Transmit Packet Steering(发送端包的控制)

(1)

RSS: Receive Side Scaling 
=========================

当代的NICs支持多个接收和传输队列,即多队列。接收的时候,一个网卡

能够发送不同的包到不同的队列,在不同的CPU之间分散处理。

NIC针对每一个包,通过一个过滤器来指定这个包属于哪一个流。

每个流中的数据包被控制在一个单独的接收队列中,CPU进行轮回

处理。这种机制就叫做RSS。RSS的目标和其他控制技术目的都是为了增加性能。

多队列也可以被用于流量优先控制,但那不是这些技术的目的。

 

RSS中的过滤器是一个基于L3和L4层头部的hash函数

例如,基于IP地址和TCP端口的4元组的hash函数。最常见的RSS硬件实现中,使用了128个间接表,

其中每个表存储一个队列号(注,网卡的队列数比较少,比如igb是8个,bnx2是5个)。

针对某个包而言,使用这个包计算出的hash值(hash是Toeplitz算法)的低7位先确定

间接表,再从间接表中的值访问队列。

一些高级的NICs允许使用可编程的过滤器来控制包属于哪个队列。

例如,绑定TCP端口80的webserver,数据包能被指向他们自己的队列。

“n-tuple”过滤器可以通过ethtool的 --config-ntuple来配置。(注: 2.6.36开始引入!)

 

==== RSS Configuration

多队列网卡的驱动提供了一个内核模块参数,用来指定硬件队列个数。

例如,bnx2x驱动使用的参数是num_queues. 如果设备支持足够多的队列,

一个典型的RSS配置中,最好的是一个CPU一个接收队列。或者至少每个内存域一个接收队列,

一个内存域包含一系列的CPU,并共享一个特殊的内存级别(L1,L2,NUMA节点等等)。

RSS设备的间接表,在驱动初始化的时候被映射。默认的映射是队列均匀的发布在间接表中。

但是,在运行的时候,使用ethtool命令 (--show-rxfh-indir and --set-rxfh-indir),

间接表可以被查看,也可以被修改。修改间接表,可以给不同的队列不同比例的权重。

 

== RSS IRQ Configuration

每个接收队列有一个单独的IRQ,即中断号。NIC通过IRQ来通知CPU什么时候新的数据包到达了指定的队列。

PCIe设备使用MSI-X来路由每个中断到CPU。有效的队列到IRQ的映射是由/proc/interrupts来制定的。

默认,一个中断能被任何一个CPU处理。因为一个重要的包处理部分发生在接收中断处理函数函数中,

在CPU中平分接收中断是有优点的。如果要手动的调节每个中断的亲和性,可以参考Documentation/IRQ-affinity.txt。

一些系统会运行irqbalance服务,这个服务会动态的优化IRQ的亲和性,因此会覆盖任何手动设置。

 

== Suggested Configuration

当关注低延时或者接收中断处理称为瓶颈时,应该启用RSS。分担负载在不同的CPU之间,

减少了队列长度。对于低延时的网络,最佳的设置是创建和CPU个数一样多的队列。

最高效的配置是拥有最少的队列,并且没有队列溢出。这是因为,默认下

中断聚合启用的情况下,中断的总数目户随着每个增加的队列而增加。

每个cpu的负载可以使用mpstat工具来观测到。但是,注意,启用超线程的处理器,

每一个超线程代笔了单独一个cpu。对于中断处理,在最初的测试中显示超线程

并没有产生优势。所以,根据CPU的核个数,而不是逻辑cpu个数,来限制队列数目

 


(2)

RPS: Receive Packet Steering 
============================

RPS,逻辑上是一种以软件的方式来实现RSS。在数据路径上,稍后被调用。

介于RSS选择了队列和CPU(这个cpu会处理硬中断),RPS选择CPU来执行硬件中断处理之后的协议处理。

通过把数据包放在目标CPU的backlog队列,并唤醒CPU来处理。

RPS相比RSS有几个好处:

1) RPS能够被任何NIC使用。

2) 软件过滤器能够轻易的被添加,用来hash新的协议。

3) 它不会增加硬件设备的中断。尽管,引入了IPIs(inter-processor interrupts)。

 

当一个设备使用 netif_rx() 函数和netif_receive_skb()函数,(从网卡驱动)向网络协议栈传递数据包时,

RPS在底半环境(通过软中断来实现的,在硬中断处理函数之后。)中被调用。

这2个函数调用get_rps_cpu() 函数,来选择应该执行包的队列。

 

决定目标CPU的第一步是基于包的地址和端口(有的协议是2元组,有的协议是4元组)

来计算hash值。这个值与这个包的流保持一致。这个hash值要么是由硬件来提供的,

要么是由协议栈来计算的。厉害的硬件能够在包的接收描述符中传递hash值,这个值

与RSS计算的值是相等的。这个hash值保存在skb->rx_hash中,并且这个值可以作为流的hash值

可以被使用在栈的其他任何地方。

 

每一个接收硬件队列有一个相关的CPU列表,RPS可以将包放到这个队列中进行处理。

对于每一个接收到的包,指向这个列表的索引是通过流hash值对列表大小取模来计算的。

被指向的CPU是处理 数据包的目标CPU,并且这个包被加到CPU的backlog队列的尾部。

最底半处理的最后,IPI被发送到这个包所插到的那个CPU。IPI唤醒远程CPU来处理backlog队列,

之后队列中数据包被发送到网络协议栈进行处理。

 

==== RPS Configuration

RPS要求内核编译了CONFIG_RPS选项(SMP上默认是打开的)。尽管编译到内核,直到

被配置了才能启用。对于某个接收队列,RPS可以转发流量到哪个CPU,是由

/sys/class/net//queues/rx-/rps_cpus来控制的。这个文件实现了

CPU的位图。默认,当值是0,RPS是无效的,数据包是由中断的CPU来处理的。

Documentation/IRQ-affinity.txt 解释了CPU是怎么由位图来设置的。

 

== Suggested Configuration

对于单个队列的设备,一个典型的RPS配置是设置rps_cpus指向与中断CPU属于相同内存域的

CPU列表。如果NUMA位置不是一个问题,也可以设置所有的CPUs。如果高中断率,

从cpu位图中排除高中断率的CPU是明智的,因为那个CPU已经执行了太多的工作。

对于一个多队列的系统,如果RSS已经配置了,导致一个硬件接收队列已经映射到每一个CPU。

那么RPS就是多余的和不必要的。如果只有很少的硬件中断队列(比CPU个数少),每个队列

的rps_cpus 指向的CPU列表与这个队列的中断CPU共享相同的内存域,那RPS将会是有效的。


(3)

RFS: Receive Flow Steering
===============

===========

RPS只依靠hash来控制数据包,提供了好的负载平衡,但是它没有考虑应用程序的位置(注:这个位置是指程序在哪个cpu上执行)。RFS则考虑到了应用程序的位置。RFS的目标是通过指派应用线程正在运行的CPU来进行数据包处理,以此来增加数据缓存的命中率。RFS依靠RPS的机制插入数据包到指定CPU的backlog队列,并唤醒那个CPU来执行。


RFS中,数据包并不会直接的通过数据包的hash值被转发,但是hash值将会作为流查询表的索引。这个表映射数据流与处理这个流的CPU。这个数据流的hash值(就是这个流中的数据包的hash值)将被用来计算这个表的索引。流查询表的每条记录中所记录的CPU是上次处理数据流的CPU。如果记录中没有CPU,那么数据包将会使用RPS来处理。多个记录会指向相同的CPU。确实,当流很多而CPU很少时,很有可能一个应用线程处理多个不同hash值的数据流。


rps_sock_flow_table是一个全局的数据流表,这个表中包含了数据流渴望运行的CPU。这个CPU是当前正在用户层处理流的CPU。每个数据流表项的值是CPU号,这个会在调recvmsg,sendmsg (特别是inet_accept(), inet_recvmsg(), inet_sendmsg(), inet_sendpage() and tcp_splice_read()),被更新。(注:使用sock_rps_record_flow()来记录rps_sock_flow_table表中每个数据流表项的CPU号。)


当调度器移动一个线程到一个新的CPU,而内核正在旧的CPU上处理接收到的数据包,这会导致数据包的乱序。为了避免这个, RFS使用了第二个数据流表来为每个数据流跟踪数据包:rps_dev_flow_table 是一个表,被指定到每个设备的每个硬件接收队列。每个表值存储了CPU号和一个计数值。这个CPU号表示了数据流中的数据包将被内核进一步处理的CPU。理想状态下,内核和用户处理发生正在同一个CPU上,由此在这两个表中这个CPU号是相同的。如果调度器已经迁移用户进程,而内核仍然有数据包被加到旧的CPU上,那么这两个值就不等了。


当这个流中的数据包最终被加到队列中, rps_dev_flow_table中的计数值记录了当前CPU的backlog队列的长度。每个backlog队列有一个队列头,当数据包从队列中出去后,这个队列头就会增加。队列尾部则等于队列头加上队列长度。换句话说,rps_dev_flow[i] 中的计数值记录了流i中的最后一个数据包,这个数据包已经添加到了目标CPU的backlog队列。当然,流i是由hash值选择的,并且多个数据流可以hash到同一个流i.

 

下面描述避免数据包乱序的技巧,当从get_rps_cpu()选择CPU来进行数据包处理,rps_sock_flow 和rps_dev_flow 将会进行比较。如果数据流的理想CPU(found in therps_sock_flow table)和当前CPU(found in the rps_dev_flow table)匹配,这个包将会加到这个CPU的backlog队列。如果他们不同,并且下面规则中任一个为真,则当前的CPU将会被更新,去匹配理想CPU。

- 当前CPU的队列头部大于等于rps_dev_flow[i]中记录的尾部计数值,这个计数值指向了CPU的队列的尾部。(说明当前cpu中没有多余的数据包未处理。)

- 当前CPU是未设置的。(等于NR_CPUS,RPS_NO_CPU=0xffff)

- 当前CPU是离线的。(注:应该是没有启用。)

 

(注:如果他们不同,并且当前CPU是有效的,则会继续用当前的CPU来处理。)检查了之后,数据包被发送到(可能)更新后的CPU.这些规则目标是当旧的CPU上没有接收到的数据包,才会移动数据流移动到一个新的CPU上。接收到的数据包能够在新的CPU切换后到达。


==== RFS Configuration

RFS需要内核编译CONFIG_RPS选项,直到明显的配置,RFS才起作用。全局数据流表(rps_sock_flow_table)的总数可以通过下面的参数来设置:

 /proc/sys/net/core/rps_sock_flow_entries

每个队列的数据流表总数可以通过下面的参数来设置:

 /sys/class/net//queues/rx-/rps_flow_cnt


== Suggested Configuration

针对每个接收队列启用RFS,上面的两个参数需要被设置。参数的值会被进位到最近的2的幂次方值。(参数的值是7,则实际有效值是8. 参数是值32,则实际值就是32.)建议的流计数依赖于期待的有效的连接数,这个值显著的小于连接总数。我们发现rps_sock_flow_entries设置成32768,在中等负载的服务器上,工作的很好。对于单队列设备,单队列的rps_flow_cnt值被配置成与 rps_sock_flow_entries相同。对于一个多队列设备,每个队列的rps_flow_cnt被配置成rps_sock_flow_entries/N, N是队列总数。例如,如果rps_sock_flow_entries设置成32768,并且有16个接收队列,每个队列的rps_flow_cnt最好被配置成2048.


(4)

Accelerated RFS(加速RFS)

===============

加速RFS对于RFS而言,就像RSS对于RPS。 加速RFS是一个硬件加速的负载平衡机制。加速RFS基于应用线程正在运行的CPU,使用“soft state”来控制流。加速RFS应该比RFS执行的好,因为数据包直接发送到CPU,而消耗数据包的线程也在这个cpu上。目标CPU要么是和应用线程相同的CPU,要么至少是和应用线程在同一缓存层次的CPU(注:意思可能是共享同个cache的其他CPU)。


要启用加速RFS,网络协议栈调用ndo_rx_flow_steer驱动函数为数据包通讯理想的硬件队列,这个队列匹配数据流。当rps_dev_flow_table中的每个流被更新了,网络协议栈自动调用这个函数。驱动轮流地使用一种设备特定的方法指定NIC去控制数据包。


一个数据流的硬件队列是从rps_dev_flow_table的CPU记录中推断出来的。协议栈需要向NIC驱动咨询CPU到硬件队列的映射,因为这个映射是由NIC驱动来维护的。这个是自动从IRQ亲和性表(通过/proc/interrupts显示)生成的反转表。驱动可以使用cpu_rmap (“CPU affinity reverse map”) 内核库函数来填充这个映射。For each CPU, the corresponding queue in the map isset to be one whose processing CPU is closest in cache locality.(不知道怎么翻译了 :-0)


==== Accelerated RFS Configuration

加速RFS需要内核编译CONFIG_RFS_ACCEL,并且需要NIC设备和驱动都支持。并且要求ntuple过滤已经通过ethtool启用。CPU到队列的映射是自动从每个接收队列的IRQ亲和性配置推断出来的,所以无需格外的配置。

 

== Suggested Configuration

不管什么时候,只要你想用RFS并且NIC支持硬件加速,这个技术都需要被启用。

(支持这个的硬件有哪些??)


(5)

XPS: Transmit Packet Steering
=============================

XPS 是一种机制,用来智能的选择多队列设备的队列来发送数据包。为了达到这个目标,从CPU到硬件队列的映射需要被记录。这个映射的目标是专门地分配队列到一个CPU列表,这些CPU列表中的某个CPU来完成队列中的数据传输。这个有两点优势,第一点,设备队列上的锁竞争会被减少,因为只有很少的CPU对相同的队列进行竞争。(如果每个CPU只有自己的传输队列,锁的竞争就完全没有了。)第二点,传输时的缓存不命中的概率就减少,特别是持有sk_buff的数据缓存。

 

XPS通过设置使用队列进行传输的CPU位图,对每一个队列进行配置。相反的映射,从CPU到传输队列,是由网络设备计算并维护的。当传输数据流的第一个数据包时,函数get_xps_queue()被调用来选择一个队列。这个函数使用正在运行的CPU的ID号作为指向CPU-到-队列的查找表的key值。如果这个ID匹配一个单独的队列,那么这个队列被用来传输。如果多个队列被匹配,通过数据流的hash值作为key值来选择队列。


选择传输特殊数据流的队列被存储在相应的数据流的socket结构体(sk_tx_queue_mapping)。

这个传输队列被用来传输接下来的数据包,以防乱序(OOO)的包。这个选择也分担了为这个流中的所有数据包调用 get_xps_queues() 的开销。为了避免乱序的包,只有这个数据流中的某个包的skb->ooo_okay标志被设置了,这个数据流所使用的队列才能改变。这个标志表示数据流中没有待解决的数据包(注:被解决的数据包应该是指tcp_packets_in_flight()等于0。也就是说发送出去的数据包都被应答了),所以,这个传输队列才能安全的改变,而不会有产生乱序包的危险。传输层即L4层相应地有责任来设置ooo_okay标志位。例如,当一个连接的所有数据包被应答了,tcp才设置这个标志位。(UDP协议没有流的概念,所以没有必要设置这个标志。)


==== XPS Configuration

XPS要求内核编译了CONFIG_XPS选项(SMP上默认是打开的)。尽管编译到内核,直到被配置了才能启用。为了使用XPS,需要使用sysfs来配置传输队列的CPU位图:

/sys/class/net//queues/tx-/xps_cpus

 

== Suggested Configuration

对于只有一个传输队列的网络设置而言,XPS的配置没有任何效果,因为这种情况下没有选择。对于一个多队列系统,XPS更好的配置是每个CPU映射到一个队列中。如果有CPU一样多的队列,那么每个队列可以映射到每个CPU上,这就导致没有竞争的专一配对。如果队列比CPU少,共享指定队列的CPU最好是与处理传输硬中断(这个中断用来清理队列传输结束后的工作)的CPU共享缓存的CPU。


Further Information
===================

RPS和RFS在内核2.6.35中被引入。XPS在2.6.38中被引入。原始的patches是由Tom Herbert
([email protected])来提交的。

加速RFS在2.6.35中被引入,原始的patches是由Ben Hutchings ([email protected])提交的


--------------------------------------------------------------------------------------------

TSO (TCP Segmentation Offload)

  TSO (TCP Segmentation Offload) 是一种利用网卡分割大数据包,减小 CPU 负荷的一种技术,也被叫做 LSO (Large segment offload) ,如果数据包的类型只能是 TCP,则被称之为 TSO,如果硬件支持 TSO 功能的话,也需要同时支持硬件的 TCP 校验计算和分散 - 聚集 (Scatter Gather) 功能。

  可以看到 TSO 的实现,需要一些基本条件,而这些其实是由软件和硬件结合起来完成的,对于硬件,具体说来,硬件能够对大的数据包进行分片,分片之后,还要能够对每个分片附着相关的头部。TSO 的支持主要有需要以下几步:

  如果网路适配器支持 TSO 功能,需要声明网卡的能力支持 TSO,这是通过以 NETIF_F_TSO 标志设置 net_device structure 的 features 字段来表明,

例如,在 benet(drivers/net/benet/be_main.c) 网卡的驱动程序中,设置 NETIF_F_TSO 的代码如下:

  benet 网卡驱动声明支持 TSO 功能

[cpp]  view plain copy
  1.   static void be_netdev_init(struct net_device *netdev)   
  2. {  
  3.  struct be_adapter *adapter = netdev_priv(netdev);   
  4. netdev->features |= NETIF_F_SG | NETIF_F_HW_VLAN_RX | NETIF_F_TSO | NETIF_F_HW_VLAN_TX | NETIF_F_HW_VLAN_FILTER | NETIF_F_HW_CSUM | NETIF_F_GRO | NETIF_F_TSO6;   
  5. netdev->vlan_features |= NETIF_F_SG | NETIF_F_TSO | NETIF_F_HW_CSUM; netdev->flags |= IFF_MULTICAST;   
  6. adapter->rx_csum = true/* Default settings for Rx and Tx flow control */   
  7. adapter->rx_fc = true;   
  8. adapter->tx_fc = true;   
  9. netif_set_gso_max_size(netdev, 65535);  
  10.  BE_SET_NETDEV_OPS(netdev, &be_netdev_ops);   
  11. SET_ETHTOOL_OPS(netdev, &be_ethtool_ops);   
  12. netif_napi_add(netdev, &adapter->rx_eq.napi, be_poll_rx, BE_NAPI_WEIGHT);   
  13. netif_napi_add(netdev, &adapter->tx_eq.napi, be_poll_tx_mcc, BE_NAPI_WEIGHT);   
  14. netif_carrier_off(netdev);   
  15. netif_stop_queue(netdev);  
  16.  }  


  在代码中,同时也用 netif_set_gso_max_size 函数设置了 net_device 的 gso_max_size 字段。该字段表明网络接口一次能处理的最大 buffer 大小,一般该值为 64Kb,这意味着只要 TCP 的数据大小不超过 64Kb,就不用在内核中分片,而只需一次性的推送到网络接口,由网络接口去执行分片功能。

  当一个 TCP 的 socket 被创建,其中一个职责是设置该连接的能力,在网络层的 socket 的表示是 struck sock,其中有一个字段 sk_route_caps 标示该连接的能力,在 TCP 的三路握手完成之后,将基于网络接口的能力和连接来设置该字段。

网路层对 TSO 功能支持的设定

[cpp]  view plain copy
  1.  /* This will initiate an outgoing connection. */   
  2. nt tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)  
  3. { …… /* OK, now commit destination to socket. */   
  4.   sk->sk_gso_type = SKB_GSO_TCPV4;   
  5.   sk_setup_caps(sk, &rt->dst); ……   


  代码中的 sk_setup_caps() 函数则设置了上面所说的 sk_route_caps 字段,同时也检查了硬件是否支持分散 - 聚集功能和硬件校验计算功能。需要这 2 个功能的原因是:Buffer 可能不在一个内存页面上,所以需要分散 - 聚集功能,而分片后的每个分段需要重新计算 checksum,因此需要硬件支持校验计算。

  现在,一切的准备工作都已经做好了,当实际的数据需要传输时,需要使用我们设置好的 gso_max_size,我们知道,TCP 向 IP 层发送数据会考虑 mss,使得发送的 IP 包在 MTU 内,不用分片。而 TSO 设置的 gso_max_size 就影响该过程,这主要是在计算 mss_now 字段时使用。如果内核不支持 TSO 功能,mss_now 的最大值为“MTU – HLENS”,而在支持 TSO 的情况下,mss_now 的最大值为“gso_max_size -HLENS”,这样,从网络层带驱动的路径就被打通了。

(2)

  GSO (Generic Segmentation Offload)

  TSO 是使得网络协议栈能够将大块 buffer 推送至网卡,然后网卡执行分片工作,这样减轻了 CPU 的负荷,但 TSO 需要硬件来实现分片功能;而性能上的提高,主要是因为延缓分片而减轻了 CPU 的负载,因此,可以考虑将 TSO 技术一般化,因为其本质实际是延缓分片,这种技术,在 Linux 中被叫做 GSO(Generic Segmentation Offload),它比 TSO 更通用,原因在于它不需要硬件的支持分片就可使用,对于支持 TSO 功能的硬件,则先经过 GSO 功能,然后使用网卡的硬件分片能力执行分片;而对于不支持 TSO 功能的网卡,将分片的执行,放在了将数据推送的网卡的前一刻,也就是在调用驱动的 xmit 函数前。

  我们再来看看内核中数据包的分片都有可能在哪些时刻:

  在传输协议中,当构造 skb 用于排队的时候

  在传输协议中,但是使用了 NETIF_F_GSO 功能,当即将传递个网卡驱动的时候

[cpp]  view plain copy
  1.   int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq)   
  2. {   
  3. …… if (netif_needs_gso(dev, skb))   
  4. if (unlikely(dev_gso_segment(skb)))   
  5. goto out_kfree_skb;  
  6.  if (skb->next) goto gso; }  
  7.  else { …… }  
  8.  …… }  

  在驱动程序里,此时驱动支持 TSO 功能 ( 设置了 NETIF_F_TSO 标志 )

  对于支持 GSO 的情况,主要使用了情况 2 或者是情况 2.、3,其中情况二是在硬件不支持 TSO 的情况下,而情况 2、3 则是在硬件支持 TSO 的情况下。

  代码中是在 dev_hard_start_xmit 函数里调用 dev_gso_segment 执行分片,这样尽量推迟分片的时间以提高性能:

  清单 8. GSO 中的分片


(4)

  接收路径上的优化

  LRO (Large Receive Offload)

  Linux 在 2.6.24 中加入了支持 IPv4 TCP 协议的 LRO (Large Receive Offload) ,它通过将多个 TCP 数据聚合在一个 skb 结构,在稍后的某个时刻作为一个大数据包交付给上层的网络协议栈,以减少上层协议栈处理 skb 的开销,提高系统接收 TCP 数据包的能力。

  当然,这一切都需要网卡驱动程序支持。理解 LRO 的工作原理,需要理解 sk_buff 结构体对于负载的存储方式,在内核中,sk_buff 可以有三种方式保存真实的负载:

  数据被保存在 skb->data 指向的由 kmalloc 申请的内存缓冲区中,这个数据区通常被称为线性数据区,数据区长度由函数 skb_headlen 给出

  数据被保存在紧随 skb 线性数据区尾部的共享结构体 skb_shared_info 中的成员 frags 所表示的内存页面中,skb_frag_t 的数目由 nr_frags 给出,skb_frags_t 中有数据在内存页面中的偏移量和数据区的大小

  数据被保存于 skb_shared_info 中的成员 frag_list 所表示的 skb 分片队列中

  合并了多个 skb 的超级 skb,能够一次性通过网络协议栈,而不是多次,这对 CPU 负荷的减轻是显然的。

  LRO 的核心结构体如下:

  . LRO 的核心结构体

  

[cpp]  view plain copy
  1. /* * Large Receive Offload (LRO) Manager * * Fields must be set by driver */   
  2. struct net_lro_mgr {   
  3. struct net_device *dev;  
  4.  struct net_lro_stats stats; /* LRO features */   
  5. unsigned long features;   
  6. #define LRO_F_NAPI 1 /* Pass packets to stack via NAPI */  
  7. #define LRO_F_EXTRACT_VLAN_ID 2 /* Set flag if VLAN IDs are extracted from received packets and eth protocol is still ETH_P_8021Q */  
  8.  /* * Set for generated SKBs that are not added to * the frag list in fragmented mode */  
  9. u32 ip_summed;   
  10. u32 ip_summed_aggr; /* Set in aggregated SKBs: CHECKSUM_UNNECESSARY * or CHECKSUM_NONE */   
  11. int max_desc; /* Max number of LRO descriptors */   
  12. int max_aggr; /* Max number of LRO packets to be aggregated */   
  13. int frag_align_pad; /* Padding required to properly align layer 3 * headers in generated skb when using frags */   
  14. struct net_lro_desc *lro_arr; /* Array of LRO descriptors */   
  15.   
  16. /* * Optimized driver functions * * get_skb_header: returns tcp and ip header for packet in SKB */   
  17. int (*get_skb_header)(struct sk_buff *skb, void **ip_hdr, void **tcpudp_hdr, u64 *hdr_flags, void *priv); /* hdr_flags: */ #define LRO_IPV4 1   
  18.   
  19. /* ip_hdr is IPv4 header */   
  20. #define LRO_TCP 2 /* tcpudp_hdr is TCP header */   
  21.   
  22. /* * get_frag_header: returns mac, tcp and ip header for packet in SKB * * @hdr_flags: Indicate what kind of LRO has to be done * (IPv4/IPv6/TCP/UDP) */   
  23. int (*get_frag_header)(struct skb_frag_struct *frag, void **mac_hdr, void **ip_hdr, void **tcpudp_hdr, u64 *hdr_flags, void *priv); };  

  在该结构体中:

  dev:指向支持 LRO 功能的网络设备

  stats:包含一些统计信息,用于查看 LRO 功能的运行情况

  features:控制 LRO 如何将包送给网络协议栈,其中的 LRO_F_NAPI 表明驱动是 NAPI 兼容的,应该使用 netif_receive_skb() 函数,而 LRO_F_EXTRACT_VLAN_ID 表明驱动支持 VLAN

  ip_summed:表明是否需要网络协议栈支持 checksum 校验

  ip_summed_aggr:表明聚集起来的大数据包是否需要网络协议栈去支持 checksum 校验

  max_desc:表明最大数目的 LRO 描述符,注意,每个 LRO 的描述符描述了一路 TCP 流,所以该值表明了做多同时能处理的 TCP 流的数量

  max_aggr:是最大数目的包将被聚集成一个超级数据包

  lro_arr:是描述符数组,需要驱动自己提供足够的内存或者在内存不足时处理异常

  get_skb_header()/get_frag_header():用于快速定位 IP 或者 TCP 的头,一般驱动只提供其中的一个实现

  一般在驱动中收包,使用的函数是 netif_rx 或者 netif_receive_skb,但在支持 LRO 的驱动中,需要使用下面的函数,这两个函数将进来的数据包根据 LRO 描述符进行分类,如果可以进行聚集,则聚集为一个超级数据包,否者直接传递给内核,走正常途径。需要 lro_receive_frags 函数的原因是某些驱动直接将数据包放入了内存页,之后去构造 sk_buff,对于这样的驱动,应该使用下面的接口:

  LRO 收包函数

  void lro_receive_skb(struct net_lro_mgr *lro_mgr, struct sk_buff *skb, void *priv); 

   void lro_receive_frags(struct net_lro_mgr *lro_mgr, struct skb_frag_struct *frags, int len, int true_size, void *priv, __wsum sum);

  因为 LRO 需要聚集到 max_aggr 数目的数据包,但有些情况下可能导致延迟比较大,这种情况下,可以在聚集了部分包之后,直接传递给网络协议栈处理,这时可以使用下面的函数,也可以在收到某个特殊的包之后,不经过 LRO,直接传递个网络协议栈:

  . LRO flush 函数

  void lro_flush_all(struct net_lro_mgr *lro_mgr); 

void lro_flush_pkt(struct net_lro_mgr *lro_mgr, struct iphdr *iph, struct tcphdr *tcph);

(4)  

GRO (Generic Receive Offload)

  前面的 LRO 的核心在于:在接收路径上,将多个数据包聚合成一个大的数据包,然后传递给网络协议栈处理,但 LRO 的实现中存在一些瑕疵:

  数据包合并可能会破坏一些状态

  数据包合并条件过于宽泛,导致某些情况下本来需要区分的数据包也被合并了,这对于路由器是不可接收的

  在虚拟化条件下,需要使用桥接功能,但 LRO 使得桥接功能无法使用

  实现中,只支持 IPv4 的 TCP 协议

  而解决这些问题的办法就是新提出的 GRO(Generic Receive Offload),首先,GRO 的合并条件更加的严格和灵活,并且在设计时,就考虑支持所有的传输协议,因此,后续的驱动,都应该使用 GRO 的接口,而不是 LRO,内核可能在所有先有驱动迁移到 GRO 接口之后将 LRO 从内核中移除。而 Linux 网络子系统的维护者 David S. Miller 就明确指出,现在的网卡驱动,有 2 个功能需要使用,一是使用 NAPI 接口以使得中断缓和 (interrupt mitigation) ,以及简单的互斥,二是使用 GRO 的 NAPI 接口去传递数据包给网路协议栈

  在 NAPI 实例中,有一个 GRO 的包的列表 gro_list,用堆积收到的包,GRO 层用它来将聚集的包分发到网络协议层,而每个支持 GRO 功能的网络协议层,则需要实现 gro_receive 和 gro_complete 方法。

  协议层支持 GRO/GSO 的接口

 

[cpp]  view plain copy
  1.  struct packet_type {   
  2. __be16 type; /* This is really htons(ether_type)。 */   
  3. struct net_device *dev; /* NULL is wildcarded here */   
  4. int (*func) (struct sk_buff *, struct net_device *, struct packet_type *, struct net_device *);   
  5. struct sk_buff *(*gso_segment)(struct sk_buff *skb, int features);   
  6. int (*gso_send_check)(struct sk_buff *skb);   
  7. struct sk_buff **(*gro_receive)(struct sk_buff **head, struct sk_buff *skb);  
  8.  int (*gro_complete)(struct sk_buff *skb);   
  9. void *af_packet_priv; struct list_head list;   
  10. };  

  其中,gro_receive 用于尝试匹配进来的数据包到已经排队的 gro_list 列表,而 IP 和 TCP 的头部则在匹配之后被丢弃;而一旦我们需要向上层协议提交数据包,则调用 gro_complete 方法,将 gro_list 的包合并成一个大包,同时 checksum 也被更新。在实现中,并没要求 GRO 长时间的去实现聚合,而是在每次 NAPI 轮询操作中,强制传递 GRO 包列表跑到上层协议。GRO 和 LRO 的最大区别在于,GRO 保留了每个接收到的数据包的熵信息,这对于像路由器这样的应用至关重要,并且实现了对各种协议的支持。以 IPv4 的 TCP 为例,匹配的条件有:

  源 / 目的地址匹配

  TOS/ 协议字段匹配

  源 / 目的端口匹配

  而很多其它事件将导致 GRO 列表向上层协议传递聚合的数据包,例如 TCP 的 ACK 不匹配或者 TCP 的序列号没有按序等等。

  GRO 提供的接口和 LRO 提供的接口非常的类似,但更加的简洁,对于驱动,明确可见的只有 GRO 的收包函数了 , 因为大部分的工作实际是在协议层做掉了:

   GRO 收包接口

  gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb) gro_result_t napi_gro_frags(struct napi_struct *napi)




你可能感兴趣的:(linux网卡,linux网络协议栈)