查看全文引用文章: VFIO Introduction
DPDK(version 2002)函数rte_pci_map_device用来映射pci device resource到用户态:
这个函数执行前,DPDK已经通过扫描sys文件系统,获取了pci设备绑定的driver,当设备绑定到vfio-pci或者igb_uio的时候,会使用不同的方式映射设备。以下分别介绍:
当设备绑定到vfio-pci时,调用函数pci_vfio_map_resource
我们在此对函数pci_vfio_map_resource_primary的主要部分进行分析。
此函数的主要工作内容如下:
通过读取设备的PCI配置空间,读取的方法是通过上一步取得的设备句柄,获取msix的配置信息。并保存到vfio_res结构体中。
获取设备的BAR REGION(寄存器,中断等信息),并完成寄存器的mmap映射,让用户态程序能够直接访问PCI设备的寄存器。
这个函数首先设置中断,将第一个中断添加到系统的中断轮训链表去。
然后设置开启设备,并对设备复位。f-stack 是基于 DPDK (Data Plante Development Kit)实现的一个用户态的协议栈;DPDK主要是 *** 作系统内核数据平面,重载网卡驱动,减少内核中断,内存拷贝和上下文切换;在此之前Linux数据平面和控制平面混在一起,不适合处理大规模的网络数据包;f-stack是基于DPDK从FressBsd协议栈移植而来的用户态协议栈;详细解释何以看相关blog,论坛和官网文档;
1), 在配置之前要保存网卡的相关信息,包括ip地址,netmask子网掩码,gateway网关,broadcast广播地址
2), 配置大页内存 set hugepage
3), 安装DPDK驱动并绑定网卡
如有疑问欢迎批评指正,谢谢!
下图展示了内部数据结构的细节。
在不同的CPU核上处理同一个发送端口的入队/出队 *** 作可能会对调度器的性能造成重大影响,因此不建议这样做。
端口入队/出队 *** 作需要共享对以下数据结构的访问:
以下 *** 作会造成预期的性能下降:
因此,调度程序的入队和出队 *** 作必须在同一个线程中运行,从而允许队列和Bitmap *** 作可以是非线程安全的,并保持调度程序的数据结构在同一个CPU核内部。 尽量将数据结构局部化,避免跨线程共享。
增加NIC端口的数量仅仅需要按比例增加用于流量调度的CPU核的数量。
包处理步骤如下:
需要注意的是,这些步骤之间有很强的依赖关系,在步骤1和步骤2的结果可用之前,步骤2和步骤3不能启动,因此没法利用处理器乱序执行引擎提供的任何性能优化。
当有大量的数据包和队列需要处理时,很有可能处理当前包所需要的数据结构不在L1和L2缓存中,因此上述3个步骤对内存的访问结果会造成L1/L2 cache miss。考虑到性能因素,每个数据包都造成3次L1/L2 cache miss是不可接受的。
解决方案是提前预取需要的数据结构。预取 *** 作有执行延迟,在此期间处理器不应该尝试访问当前正在预取的数据结构,因此处理器应该执行其他工作。唯一可做的其他工作是对其他输入包执行入队 *** 作流程的不同步骤,从而以流水线的方式处理入队 *** 作。
图2演示了入队 *** 作的流水线实现,其中有4个流水线阶段,每个阶段处理2个不同的输入包。在给定的时间内,任何输入包都不能成为多个流水线阶段的一部分。
上面描述了非常基本的入队流水线拥塞管理方案:将数据包塞入队列,当队列被填满时,所有到达同一队列的数据包将被丢弃,直到有数据包通过出队 *** 作被消费掉,才可以在队列中塞入新的数据包。如果用RED/WRED改进入队流水线,就可以通过查看队列占用率和包的优先级,为特定的包做出入队/丢弃决策(而不是不加选择地将所有包入队或者丢弃)。
在当前管道调度处理下一个包的步骤为:
以上 *** 作涉及到的数据结构(pipe, queue, queue array, mbufs)可以在被访问之前预取进缓存,从而避免cache miss。在对当前管道(管道A)的数据发出预取 *** 作后,可以立即切换到另一个管道(管道B)进行处理,等到管道A的预取 *** 作完成,再切换回去,从而利用预取本身的时延同步处理多个管道的数据。
出队状态机利用处理器缓存机制,尽可能同时处理多个活跃管道,从而发送更多的数据包。
输出端口就是一个被调度器不断的用待传输数据填充的队列。对于10 GbE,每秒有125亿个字节需要被端口调度器填充。如果调度器的速度不够快,不能填满队列(假设有足够的待传输数据包和credit),那么必然有一些带宽会被闲置,从而产生浪费。
原则上,分层调度器出队 *** 作应该由网卡发送器触发。通常情况下,一旦网卡发送队列的输入流量低于一个预定义的阈值,端口调度程序将被唤醒(基于中断或轮询的方式,通过持续监控队列占用决定是否被唤醒),填充更多的数据包进入队列。
调度器需要跟踪随着时间推移而变化的credit信息,这需要基于时间进行credit的更新(例如,子端口和管道流量整形、流量组上限的强制执行,等等)。
每次调度器决定向网卡发送器传递数据包时,调度器将相应地调整其内部时间信息。因为在物理接口上每发送一个字节所需的时间是固定的,所以可以以字节为单位作为内部时间的参考值,这样处理起来也会比较方便。当一个包准备传输时,时间以(n + h)的形式递增,其中n是以字节为单位的包长度,h是每个包的帧数据字节数(包括帧头、CRC等)。
调度器需要将其内部时间参考值与端口速率对齐,从而确保调度器不会填充超过网卡发送速率的数据,从而防止由于网卡发生队列已满而发生的丢包。
调度器需要读取每次出队调用的当前时间。CPU时间戳可以通过读取TSC寄存器(Time Stamp Counter)或HPET寄存器(High Precision Event Timer)来获得。当前CPU时间戳需要通过以下公式将CPU时钟转换为字节数:time_bytes = time_cycles / cycles_per_byte, cycles_per_byte是传输一个字节所消耗的CPU周期(例如,10GbE的端口以2GHz的CPU频率传输数据,cycles_per_byte = 2G/(10G/8) = 16)。
调度器会维护针对网卡时间的内部时间引用,网卡时间将随着被调度的数据包的长度(包含帧开销)的增加而增加。在每次出队调用时,调度程序会根据当前时间检查其对网卡时间的内部引用:
调度器往返延迟(SRTD,Scheduler Round Trip Delay)是调度器连续两次检查同一管道之间的时间(以CPU周期为单位)。
为了跟上发送端口的速率(即避免网卡带宽的浪费),调度器应该能够比网卡发送器实际传输速度更快地调度数据包。
假设没有发生端口带宽超卖,调度器需要根据配置的令牌桶,跟踪每个管道的速率。这意味着管道的令牌桶的大小应该设置得足够高,以防止它因较大的SRTD而溢出,从而导致管道的可用带宽损失。
当以下所有条件都满足时,调度器可以为(子端口S、管道P、流量组TC、队列Q)做出发送下一个包的合理调度决策:
如果以上条件都满足,则可以选择一个数据包进行传输,并从子端口S、子端口S的流量组TC、管道P、管道P的流量组TC中减去必要的credit。
由于所有数据包长度的最大公约数是一个字节,因此以字节作为credit的计量单位。传输n字节的数据包所需的credit数等于(n+h),其中h等于每个数据包的帧开销(以字节为单位)。
子端口和管道的流量整形是通过令牌桶来实现的,每个令牌桶使用一个饱和计数器(saturated counter)来实现,通过这个计数器跟踪credit的数值。
令牌桶泛型参数和 *** 作如下表所示:
为了实现上面描述的令牌桶通用 *** 作,当前设计使用下表中给出的持久数据结构:
可以用以下公式计算桶速率(单位为字节/秒):
式中,r = 端口线速(单位为字节/秒)。
令牌桶 *** 作的实现如下表所示:
同一管道内TC的优先级是严格定义的,其调度是由管道出队状态机实现的,该状态机按照优先级升序排列选择队列。因此,队列0(与TC 0相关,优先级最高的TC)在队列1(优先级低于TC 0的TC 1)之前被处理,队列1在队列2(优先级低于TC 1的TC 2)之前被处理,并继续下去,直到除最低优先级TC以外的所有TC队列都被处理。最后,队列12-15(尽力而为TC, 最低优先级TC)被处理。
管道和子端口级别的TC不支持流量整形(traffic shaping),所以在这两个级别的上下文里没有维护令牌桶。管道和子端口级别的TC上限被强制限定为周期性回填的credit,每次在管道/子端口级别处理包的时候,credit就会被消费掉,如下两表所示。
子端口/管道TC强制上限的持久化数据结构:
最低优先级TC(尽力而为TC,best effort TC)的WRR设计方案从简单到复杂的演变如下表所示。
子端口流量组X的超售是一个配置问题,当子端口的成员管道为流量组X分配的带宽大于在上层子端口级别为同一流量组分配的带宽时发生。
特定子端口和流量组的超售完全是管道和子端口级配置的结果,而不是由于运行时流量负载的动态演化(如拥塞)造成的。
如果当前子端口对流量组X的总体需求较低,超售的存在并不代表问题,因为所有成员管道对流量组X的需求已经完全可以满足。然而,当所有子端口的成员管道的流量组X的需求合在一起超过了在子端口级别配置的限制时,这将不再能够实现。
下表总结了处理这个问题的一些可能的方案,当前实现是基于第三种方案。
通常,子端口TC超售只对最低优先级的流量组启用,这通常用于尽力而为(best effort)的流量,管理平面需要防止其他(更高优先级)的TC发生这种情况。
为了便于实现,还假设子端口的最低优先级TC的上限设置为子端口速率的100%,并且对于所有子端口的成员管道,管道最低优先级TC的上限设置为管道速率的100%。
该算法会先计算一个容量(watermark),容量会根据子端口的成员管道当前的需求定期更新,其目的是限制每个管道允许发送的最低优先级(best effort)TC的流量。在每个TC上限计算周期开始时,在子端口级别上计算该容量,并在当前实施周期内在所有子端口的成员管道上使用相同的容量值。下面说明在每个周期开始时作为子端口级别计算的容量如何传播到所有子端口的成员管道的。
当前计算周期开始(和上一个计算周期结束的时间相同)时,容量值需要基于上一周期开始时分配给尽力而为TC但没有被子端口的成员管道消费掉的带宽值做出调整。
如果子端口有尽力而为TC带宽没用掉,就增加当前时段的容量值,以鼓励子端口的成员管道消耗更多带宽。否则,降低容量值,以强制子端口成员管道的尽力而为TC带宽的消耗相等。
容量值的增加或减少会以很小的增量进行,因此可能需要几个执行周期才能达到平衡状态。由于子端口成员管道对尽力而为TC的需求的变化,这种状态可以在任何时候发生改变,例如,由于需求增加(当需要降低容量值时)或需求减少(当可以增加容量值时)。
在需求较低时,为防止子端口成员管道占用太多带宽,需要设置高容量值。容量的最高值被选为子端口成员管道配置的最高速率。下表说明了容量 *** 作。
每个TC流量限制执行周期开始时,从子端口级别到成员管道的容量传递:
容量值计算:
为了调度一个报文,调度器必须在多个队列里查看报文和credit,当调度器需要查看的队列越多,其性能就越低。
调度器维护了活跃队列的Bitmap,从而可以直接跳过非活跃队列,但是为了检测特定的管道是否有足够的credit,必须使用管道发送状态机进行探测,无论最终调度结果如何(没有报文或者至少产生一个报文),总会消耗CPU时间。
这个场景强调了策略对调度器性能的重要性:如果管道没有足够的credit,其数据包应该尽快丢弃(在触发分层调度器之前),从而可以将管道队列标识为不活跃,发送端就可以跳过这个管道而不用消耗资源去查看管道credit。
端口调度器的性能针对大量队列进行了优化。但如果队列的数量很少,那么对于相同级别的活动流量,端口调度器的性能预期会比一小组消息传递队列的性能更差。
>rte_schedh文件包含port,subport和pipe的配置功能。Port调度入队API非常类似于DPDK PMD TX功能的API。
Port调度入队API非常类似于DPDK PMD RX功能的API。
内部数据结构示意图,详细内容如下。
多核缩放策略如下:
上面强调过,同一个端口的出队和入队需要由同一个线程执行。因为,在不同core上对同一个输出端口执行出队和入队 *** 作,可能会对调度程序的性能造成重大影响,因此不推荐这样做。
同一端口的入队和出队 *** 作共享以下数据结构的访问权限:
可能存在使性能下降的原因如下:
当调度程序入队和出队 *** 作必须在同一个线程运行,允许队列和位图 *** 作非线程安全,并将调度程序数据结构保持在同一个core上,可以很大程度上保证性能。
扩展NIC端口数量只需要保证用于流量调度的CPU内核数量按比例增加即可。
每个数据包的入队步骤:
应该注意到这些步骤之间具有很强的数据依赖性,因为步骤2和3在步骤1和2的结果变得可用之前无法启动,这样就无法使用处理器乱序执行引擎上提供任何显着的性能优化。
考虑这样一种情况,给定的输入报文速率很高,队列数量大,可以想象得到,入队当前数据包需要访问的数据结构不存在于当前core的L1或L2 data cache中,此时,上述3个内存访问 *** 作将会产生L1和L2 data cache miss。就性能考虑而言,每个数据包出现3次L1 / L2 data cache miss是不可接受的。
解决方法是提前预取所需的数据结构。预取 *** 作具有执行延迟,在此期间处理器不应尝试访问当前正在进行预取的数据结构,此时处理器转向执行其他工作。可用的其他工作可以是对其他输入报文执行不同阶段的入队序列,从而实现入队 *** 作的流水线实现。
下图展示出了具有4级水线的入队 *** 作实现,并且每个阶段 *** 作2个不同的输入报文。在给定的时间点上,任何报文只能在水线某个阶段进行处理。
由上图描述的入队水线实现的拥塞管理方案是非常基础的:数据包排队入队,直到指定队列变满为止;当满时,到这个队列的所有数据包将被丢弃,直到队列中有数据包出队。可以通过使用RED/WRED作为入队水线的一部分来改进,该流程查看队列占用率和报文优先级,以便产生特定数据包的入队/丢弃决定(与入队所有数据包/不加区分地丢弃所有数据包不一样)。
从当前pipe调度下一个数据包的步骤如下:
为了避免cache miss,上述数据结构(pipe,queue,queue array,mbufs)在被访问之前被预取。隐藏预取 *** 作的延迟的策略是在为当前pipe发出预取后立即从当前pipe(在grinder A中)切换到另一个pipe(在grinderB中)。这样就可以在执行切换回pipe(grinder A)之前,有足够的时间完成预取 *** 作。
出pipe状态机将数据存在处理器高速缓存中,因此它尝试从相同的pipe TC和pipe(尽可能多的数据包和信用)发送尽可能多的数据包,然后再移动到下一个活动TC pipe(如果有)或另一个活动pipe。
输出端口被建模为字节槽的传送带,需要由调度器填充用于传输的数据。对于10GbE,每秒需要由调度器填充125亿个字节的槽位。如果调度程序填充不够快,只要存在足够的报文和信用,则一些时隙将被闲置并且带宽将被浪费。
原则上,层次调度程序出队 *** 作应由NIC TX触发。通常,一旦NIC TX输入队列的占用率下降到预定义的阈值以下,端口调度器将被唤醒(基于中断或基于轮询,通过连续监视队列占用)来填充更多的数据包进入队列。
调度器需要跟踪信用逻辑的时间演化,因为信用需要基于时间更新(例如,子流量和管道流量整形,流量级上限执行等)。
每当调度程序决定将数据包发送到NIC TX进行传输时,调度器将相应地增加其内部时间参考。因此,以字节为单位保持内部时间基准是方便的,其中字节表示物理接口在传输介质上发送字节所需的持续时间。这样,当报文被调度用于传输时,时间以(n + h)递增,其中n是以字节为单位的报文长度,h是每个报文的成帧开销字节数。
调度器需要将其内部时间参考对齐到端口传送带的步速。原因是要确保调度程序不以比物理介质的线路速率更多的字节来馈送NIC TX,以防止数据包丢失。
调度程序读取每个出队调用的当前时间。可以通过读取时间戳计数器(TSC)寄存器或高精度事件定时器(HPET)寄存器来获取CPU时间戳。 当前CPU时间戳将CPU时钟数转换为字节数:time_bytes = time_cycles / cycles_per_byte,其中cycles_per_byte是等效于线上一个字节的传输时间的CPU周期数(例如CPU频率 2 GHz和10GbE端口, cycles_per_byte = 16 )。
调度程序维护NIC time的内部时间参考。 每当分组被调度时,NIC time随分组长度(包括帧开销)增加。在每次出队调用时,调度程序将检查其NIC time的内部引用与当前时间的关系:
调度器往返延迟(SRTD)是指调度器在同一个pipe的两次连续检验之间的时间(CPU周期数)。
为了跟上输出端口(即避免带宽丢失),调度程序应该能够比NIC TX发送的n个数据包更快地调度n个数据包。
假设没有端口超过流量,调度程序需要跟上管道令牌桶配置的每个管道的速率。这意味着管道令牌桶的大小应该设置得足够高,以防止它由于大的SRTD而溢出,因为这将导致管道的信用损失(带宽损失)。
当满足以下所有条件时,从(subport S,pipe P,traffic class TC,queue Q)发送下一个分组的调度决定(分组被发送):
如果满足所有上述条件,则选择分组进行传输,并从子接口S,子接口S流量类TC,管道P,管道P流量类TC中减去必要的信用。
232462 帧开销
由于所有数据包长度的最大公约数为1个字节,所以信用单位被选为1个字节。传输n个字节的报文所需的信用数量等于(n + h),其中h等于每个报文的成帧开销字节数。
Subport和pipe的流量整形使用每个subport/pipe的令牌桶来实现。令牌桶使用一个饱和计数器实现,该计数器跟踪可用信用数量。
令牌桶通用参数和 *** 作如下表所示。
为了实现上述的令牌桶通用 *** 作,当前的设计使用表238中所示的数据结构,而令牌桶 *** 作的实现在表239中描述。
桶速率(以字节为单位)可以用以下公式计算:
同一管道内流量级别的严格优先级调度由管理出队状态机实现,该队列按升序选择队列。因此,1215(TC 3,最低优先级TC),队列811(TC 2),队列47(TC 1,比TC 0的优先级低),队列03(TC 0,最高优先级TC相关联)。
Pipe和Subport级别的流量类别不是流量整形,因此在此上下文中不存在令牌桶。通过周期性地重新填充subport/pipe流量信用计数器来执行subport和pipe级别的流量类别的上限,每次为该subport/pipe调度数据包时消耗信用量,如表2310所述 和表2311。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)