局域网论文.docx

上传人:b****2 文档编号:1973944 上传时间:2023-05-02 格式:DOCX 页数:16 大小:26.92KB
下载 相关 举报
局域网论文.docx_第1页
第1页 / 共16页
局域网论文.docx_第2页
第2页 / 共16页
局域网论文.docx_第3页
第3页 / 共16页
局域网论文.docx_第4页
第4页 / 共16页
局域网论文.docx_第5页
第5页 / 共16页
局域网论文.docx_第6页
第6页 / 共16页
局域网论文.docx_第7页
第7页 / 共16页
局域网论文.docx_第8页
第8页 / 共16页
局域网论文.docx_第9页
第9页 / 共16页
局域网论文.docx_第10页
第10页 / 共16页
局域网论文.docx_第11页
第11页 / 共16页
局域网论文.docx_第12页
第12页 / 共16页
局域网论文.docx_第13页
第13页 / 共16页
局域网论文.docx_第14页
第14页 / 共16页
局域网论文.docx_第15页
第15页 / 共16页
局域网论文.docx_第16页
第16页 / 共16页
亲,该文档总共16页,全部预览完了,如果喜欢就下载吧!
下载资源
资源描述

局域网论文.docx

《局域网论文.docx》由会员分享,可在线阅读,更多相关《局域网论文.docx(16页珍藏版)》请在冰点文库上搜索。

局域网论文.docx

局域网论文

概述

对于网络的行为,可以简单划分为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的实现中存在一些瑕疵:

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

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

∙在虚拟化条件下,

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 求职职场 > 简历

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2