摘自: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/
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/
== 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/
== 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 功能
在代码中,同时也用 netif_set_gso_max_size 函数设置了 net_device 的 gso_max_size 字段。该字段表明网络接口一次能处理的最大 buffer 大小,一般该值为 64Kb,这意味着只要 TCP 的数据大小不超过 64Kb,就不用在内核中分片,而只需一次性的推送到网络接口,由网络接口去执行分片功能。
当一个 TCP 的 socket 被创建,其中一个职责是设置该连接的能力,在网络层的 socket 的表示是 struck sock,其中有一个字段 sk_route_caps 标示该连接的能力,在 TCP 的三路握手完成之后,将基于网络接口的能力和连接来设置该字段。
. 网路层对 TSO 功能支持的设定
代码中的 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 功能,当即将传递个网卡驱动的时候
对于支持 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 的核心结构体
在该结构体中:
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 的接口
其中,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)