局域网论文.docx
- 文档编号:20278273
- 上传时间:2023-04-25
- 格式:DOCX
- 页数:16
- 大小:26.92KB
局域网论文.docx
《局域网论文.docx》由会员分享,可在线阅读,更多相关《局域网论文.docx(16页珍藏版)》请在冰豆网上搜索。
局域网论文
概述
对于网络的行为,可以简单划分为3条路径:
1)发送路径,2)转发路径,3)接收路径,而网络性能的优化则可基于这3条路径来考虑。
由于数据包的转发一般是具备路由功能的设备所关注,在本文中没有叙述,读者如果有兴趣,可以自行学习(在Linux内核中,分别使用了基于哈希的路由查找和基于动态Trie的路由查找算法)。
本文集中于发送路径和接收路径上的优化方法分析,其中的NAPI本质上是接收路径上的优化,但因为它在Linux的内核出现时间较早,而它也是后续出现的各种优化方法的基础,所以将其单独分析。
最为基本的NAPI
NAPI
NAPI的核心在于:
在一个繁忙网络,每次有网络数据包到达时,不需要都引发中断,因为高频率的中断可能会影响系统的整体效率,假象一个场景,我们此时使用标准的100M网卡,可能实际达到的接收速率为80MBits/s,而此时数据包平均长度为1500Bytes,则每秒产生的中断数目为:
80Mbits/s/(8Bits/Byte*1500Byte)=6667个中断/s
每秒6667个中断,对于系统是个很大的压力,此时其实可以转为使用轮询(polling)来处理,而不是中断;但轮询在网络流量较小的时没有效率,因此低流量时,基于中断的方式则比较合适,这就是NAPI出现的原因,在低流量时候使用中断接收数据包,而在高流量时候则使用基于轮询的方式接收。
现在内核中NIC基本上已经全部支持NAPI功能,由前面的叙述可知,NAPI适合处理高速率数据包的处理,而带来的好处则是:
∙中断缓和(Interruptmitigation),由上面的例子可以看到,在高流量下,网卡产生的中断可能达到每秒几千次,而如果每次中断都需要系统来处理,是一个很大的压力,而NAPI使用轮询时是禁止了网卡的接收中断的,这样会减小系统处理中断的压力
∙数据包节流(Packetthrottling),NAPI之前的LinuxNIC驱动总在接收到数据包之后产生一个IRQ,接着在中断服务例程里将这个skb加入本地的softnet,然后触发本地NET_RX_SOFTIRQ软中断后续处理。
如果包速过高,因为IRQ的优先级高于SoftIRQ,导致系统的大部分资源都在响应中断,但softnet的队列大小有限,接收到的超额数据包也只能丢掉,所以这时这个模型是在用宝贵的系统资源做无用功。
而NAPI则在这样的情况下,直接把包丢掉,不会继续将需要丢掉的数据包扔给内核去处理,这样,网卡将需要丢掉的数据包尽可能的早丢弃掉,内核将不可见需要丢掉的数据包,这样也减少了内核的压力
对NAPI的使用,一般包括以下的几个步骤:
1.在中断处理函数中,先禁止接收中断,且告诉网络子系统,将以轮询方式快速收包,其中禁止接收中断完全由硬件功能决定,而告诉内核将以轮询方式处理包则是使用函数netif_rx_schedule(),也可以使用下面的方式,其中的netif_rx_schedule_prep是为了判定现在是否已经进入了轮询模式:
:
清单1.将网卡预定为轮询模式
voidnetif_rx_schedule(structnet_device*dev);
或者
if(netif_rx_schedule_prep(dev))
__netif_rx_schedule(dev);
2.在驱动中创建轮询函数,它的工作是从网卡获取数据包并将其送入到网络子系统,其原型是:
清单2.NAPI的轮询方法
int(*poll)(structnet_device*dev,int*budget);
这里的轮询函数用于在将网卡切换为轮询模式之后,用poll()方法处理接收队列中的数据包,如队列为空,则重新切换为中断模式。
切换回中断模式需要先关闭轮询模式,使用的是函数netif_rx_complete(),接着开启网卡接收中断.。
清单3.退出轮询模式
voidnetif_rx_complete(structnet_device*dev);
3.在驱动中创建轮询函数,需要和实际的网络设备structnet_device关联起来,这一般在网卡的初始化时候完成,示例代码如下:
清单4.设置网卡支持轮询模式
dev->poll=my_poll;
dev->weight=64;
里面另外一个字段为权重(weight),该值并没有一个非常严格的要求,实际上是个经验数据,一般10Mb的网卡,我们设置为16,而更快的网卡,我们则设置为64。
NAPI的一些相关Interface
下面是NAPI功能的一些接口,在前面都基本有涉及,我们简单看看:
netif_rx_schedule(dev)
在网卡的中断处理函数中调用,用于将网卡的接收模式切换为轮询
netif_rx_schedule_prep(dev)
在网卡是Up且运行状态时,将该网卡设置为准备将其加入到轮询列表的状态,可以将该函数看做是netif_rx_schedule(dev)的前半部分
__netif_rx_schedule(dev)
将设备加入轮询列表,前提是需要netif_schedule_prep(dev)函数已经返回了1
__netif_rx_schedule_prep(dev)
与netif_rx_schedule_prep(dev)相似,但是没有判断网卡设备是否Up及运行,不建议使用
netif_rx_complete(dev)
用于将网卡接口从轮询列表中移除,一般在轮询函数完成之后调用该函数。
__netif_rx_complete(dev)
NewernewerNAPI
其实之前的NAPI(NewAPI)这样的命名已经有点让人忍俊不禁了,可见Linux的内核极客们对名字的掌控,比对代码的掌控差太多,于是乎,连续的两次对NAPI的重构,被戏称为NewernewerNAPI了。
与netif_rx_complete(dev)类似,但是需要确保本地中断被禁止
NewernewerNAPI
在最初实现的NAPI中,有2个字段在结构体 net_device中,分别为轮询函数poll()和权重weight,而所谓的NewernewerNAPI,是在2.6.24版内核之后,对原有的NAPI实现的几次重构,其核心是将NAPI相关功能和net_device分离,这样减少了耦合,代码更加的灵活,因为NAPI的相关信息已经从特定的网络设备剥离了,不再是以前的一对一的关系了。
例如有些网络适配器,可能提供了多个port,但所有的port却是共用同一个接受数据包的中断,这时候,分离的NAPI信息只用存一份,同时被所有的port来共享,这样,代码框架上更好地适应了真实的硬件能力。
NewernewerNAPI的中心结构体是napi_struct:
清单5.NAPI结构体
/*
*StructureforNAPIschedulingsimilartotaskletbutwithweighting
*/
structnapi_struct{
/*Thepoll_listmustonlybemanagedbytheentitywhich
*changesthestateoftheNAPI_STATE_SCHEDbit.Thismeans
*whoeveratomicallysetsthatbitcanaddthisnapi_struct
*totheper-cpupoll_list,andwhoeverclearsthatbit
*canremovefromthelistrightbeforeclearingthebit.
*/
structlist_headpoll_list;
unsignedlongstate;
intweight;
int(*poll)(structnapi_struct*,int);
#ifdefCONFIG_NETPOLL
spinlock_tpoll_lock;
intpoll_owner;
#endif
unsignedintgro_count;
structnet_device*dev;
structlist_headdev_list;
structsk_buff*gro_list;
structsk_buff*skb;
};
熟悉老的NAPI接口实现的话,里面的字段poll_list、state、weight、poll、dev、没什么好说的,gro_count和gro_list会在后面讲述GRO时候会讲述。
需要注意的是,与之前的NAPI实现的最大的区别是该结构体不再是net_device的一部分,事实上,现在希望网卡驱动自己单独分配与管理napi实例,通常将其放在了网卡驱动的私有信息,这样最主要的好处在于,如果驱动愿意,可以创建多个napi_struct,因为现在越来越多的硬件已经开始支持多接收队列(multiplereceivequeues),这样,多个napi_struct的实现使得多队列的使用也更加的有效。
与最初的NAPI相比较,轮询函数的注册有些变化,现在使用的新接口是:
voidnetif_napi_add(structnet_device*dev,structnapi_struct*napi,
int(*poll)(structnapi_struct*,int),intweight)
熟悉老的NAPI接口的话,这个函数也没什么好说的。
值得注意的是,前面的轮询poll()方法原型也开始需要一些小小的改变:
int(*poll)(structnapi_struct*napi,intbudget);
大部分NAPI相关的函数也需要改变之前的原型,下面是打开轮询功能的API:
voidnetif_rx_schedule(structnet_device*dev,
structnapi_struct*napi);
/*...or...*/
intnetif_rx_schedule_prep(structnet_device*dev,
structnapi_struct*napi);
void__netif_rx_schedule(structnet_device*dev,
structnapi_struct*napi);
轮询功能的关闭则需要使用:
voidnetif_rx_complete(structnet_device*dev,
structnapi_struct*napi);
因为可能存在多个napi_struct的实例,要求每个实例能够独立的使能或者禁止,因此,需要驱动作者保证在网卡接口关闭时,禁止所有的napi_struct的实例。
函数netif_poll_enable()和netif_poll_disable()不再需要,因为轮询管理不再和net_device直接管理,取而代之的是下面的两个函数:
voidnapi_enable(structnapi*napi);
voidnapi_disable(structnapi*napi);
发送路径上的优化
TSO(TCPSegmentationOffload)
TSO(TCPSegmentationOffload)是一种利用网卡分割大数据包,减小CPU负荷的一种技术,也被叫做LSO(Largesegmentoffload),如果数据包的类型只能是TCP,则被称之为TSO,如果硬件支持TSO功能的话,也需要同时支持硬件的TCP校验计算和分散-聚集(ScatterGather)功能。
可以看到TSO的实现,需要一些基本条件,而这些其实是由软件和硬件结合起来完成的,对于硬件,具体说来,硬件能够对大的数据包进行分片,分片之后,还要能够对每个分片附着相关的头部。
TSO的支持主要有需要以下几步:
∙如果网路适配器支持TSO功能,需要声明网卡的能力支持TSO,这是通过以NETIF_F_TSO标志设置net_devicestructure的features字段来表明,例如,在benet(drivers/net/benet/be_main.c)网卡的驱动程序中,设置NETIF_F_TSO的代码如下:
清单6.benet网卡驱动声明支持TSO功能
staticvoidbe_netdev_init(structnet_device*netdev)
{
structbe_adapter*adapter=netdev_priv(netdev);
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;
netdev->vlan_features|=NETIF_F_SG|NETIF_F_TSO|NETIF_F_HW_CSUM;
netdev->flags|=IFF_MULTICAST;
adapter->rx_csum=true;
/*DefaultsettingsforRxandTxflowcontrol*/
adapter->rx_fc=true;
adapter->tx_fc=true;
netif_set_gso_max_size(netdev,65535);
BE_SET_NETDEV_OPS(netdev,&be_netdev_ops);
SET_ETHTOOL_OPS(netdev,&be_ethtool_ops);
netif_napi_add(netdev,&adapter->rx_eq.napi,be_poll_rx,
BE_NAPI_WEIGHT);
netif_napi_add(netdev,&adapter->tx_eq.napi,be_poll_tx_mcc,
BE_NAPI_WEIGHT);
netif_carrier_off(netdev);
netif_stop_queue(netdev);
}
在代码中,同时也用netif_set_gso_max_size函数设置了net_device的gso_max_size字段。
该字段表明网络接口一次能处理的最大buffer大小,一般该值为64Kb,这意味着只要TCP的数据大小不超过64Kb,就不用在内核中分片,而只需一次性的推送到网络接口,由网络接口去执行分片功能。
∙当一个TCP的socket被创建,其中一个职责是设置该连接的能力,在网络层的socket的表示是strucksock,其中有一个字段sk_route_caps标示该连接的能力,在TCP的三路握手完成之后,将基于网络接口的能力和连接来设置该字段。
清单7.网路层对TSO功能支持的设定
/*Thiswillinitiateanoutgoingconnection.*/
inttcp_v4_connect(structsock*sk,structsockaddr*uaddr,intaddr_len)
{
……
/*OK,nowcommitdestinationtosocket.*/
sk->sk_gso_type=SKB_GSO_TCPV4;
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”,这样,从网络层带驱动的路径就被打通了。
GSO(GenericSegmentationOffload)
TSO是使得网络协议栈能够将大块buffer推送至网卡,然后网卡执行分片工作,这样减轻了CPU的负荷,但TSO需要硬件来实现分片功能;而性能上的提高,主要是因为延缓分片而减轻了CPU的负载,因此,可以考虑将TSO技术一般化,因为其本质实际是延缓分片,这种技术,在Linux中被叫做GSO(GenericSegmentationOffload),它比TSO更通用,原因在于它不需要硬件的支持分片就可使用,对于支持TSO功能的硬件,则先经过GSO功能,然后使用网卡的硬件分片能力执行分片;而对于不支持TSO功能的网卡,将分片的执行,放在了将数据推送的网卡的前一刻,也就是在调用驱动的xmit函数前。
我们再来看看内核中数据包的分片都有可能在哪些时刻:
1.在传输协议中,当构造skb用于排队的时候
2.在传输协议中,但是使用了NETIF_F_GSO功能,当即将传递个网卡驱动的时候
3.在驱动程序里,此时驱动支持TSO功能(设置了NETIF_F_TSO标志)
对于支持GSO的情况,主要使用了情况2或者是情况2.、3,其中情况二是在硬件不支持TSO的情况下,而情况2、3则是在硬件支持TSO的情况下。
代码中是在dev_hard_start_xmit函数里调用dev_gso_segment执行分片,这样尽量推迟分片的时间以提高性能:
清单8.GSO中的分片
intdev_hard_start_xmit(structsk_buff*skb,structnet_device*dev,
structnetdev_queue*txq)
{
……
if(netif_needs_gso(dev,skb)){
if(unlikely(dev_gso_segment(skb)))
gotoout_kfree_skb;
if(skb->next)
gotogso;
}else{
……
}
……
}
接收路径上的优化
LRO(LargeReceiveOffload)
Linux在2.6.24中加入了支持IPv4TCP协议的LRO(LargeReceiveOffload),它通过将多个TCP数据聚合在一个skb结构,在稍后的某个时刻作为一个大数据包交付给上层的网络协议栈,以减少上层协议栈处理skb的开销,提高系统接收TCP数据包的能力。
当然,这一切都需要网卡驱动程序支持。
理解LRO的工作原理,需要理解sk_buff结构体对于负载的存储方式,在内核中,sk_buff可以有三种方式保存真实的负载:
1.数据被保存在skb->data指向的由kmalloc申请的内存缓冲区中,这个数据区通常被称为线性数据区,数据区长度由函数skb_headlen给出
2.数据被保存在紧随skb线性数据区尾部的共享结构体skb_shared_info中的成员frags所表示的内存页面中,skb_frag_t的数目由nr_frags给出,skb_frags_t中有数据在内存页面中的偏移量和数据区的大小
3.数据被保存于skb_shared_info中的成员frag_list所表示的skb分片队列中
合并了多个skb的超级skb,能够一次性通过网络协议栈,而不是多次,这对CPU负荷的减轻是显然的。
清单10.LRO收包函数
voidlro_receive_skb(structnet_lro_mgr*lro_mgr,
structsk_buff*skb,
void*priv);
voidlro_receive_frags(structnet_lro_mgr*lro_mgr,
structskb_frag_struct*frags,
intlen,inttrue_size,
void*priv,__wsumsum);
因为LRO需要聚集到max_aggr数目的数据包,但有些情况下可能导致延迟比较大,这种情况下,可以在聚集了部分包之后,直接传递给网络协议栈处理,这时可以使用下面的函数,也可以在收到某个特殊的包之后,不经过LRO,直接传递个网络协议栈:
清单11.LROflush函数
voidlro_flush_all(structnet_lro_mgr*lro_mgr);
voidlro_flush_pkt(structnet_lro_mgr*lro_mgr,
structiphdr*iph,
structtcphdr*tcph);
GRO(GenericReceiveOffload)
前面的LRO的核心在于:
在接收路径上,将多个数据包聚合成一个大的数据包,然后传递给网络协议栈处理,但LRO的实现中存在一些瑕疵:
∙数据包合并可能会破坏一些状态
∙数据包合并条件过于宽泛,导致某些情况下本来需要区分的数据包也被合并了,这对于路由器是不可接收的
∙在虚拟化条件下,
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 局域网 论文