您的位置:首页 > 运维架构 > Linux

linux协议栈学习 第七节 GRO的实现

2015-04-22 16:00 363 查看

linux协议栈学习 第七节 GRO的实现

GRO
(generic receive offload)

概述:
GRO是在协议栈接收报文时进行减负的一种处理方式,该方式在设计上考虑了多种协议报文。主要原理是在接收端通过把多个相关的报文(比如TCP分段报文)组装成一个大的报文后再传送给协议栈进行处理,因为内核协议栈对报文的处理都是对报文头部进行处理,如果相关的多个报文合并后只有一个报文头,这样就减少了协议栈处理报文个数,加快协议栈对报文的处理速度。

GRO功能对到本机的报文能起到一定的加速作用,但如果linux 运行在转发设备上,一般不需要使用GRO功能,这时使用GRO功能反而会降低处理速度。

GRO功能和只是针对NAPI类型的驱动,网卡驱动支持GRO要调用内核提供的GRO函数进行收包。并且该功能和网络设备硬件无关,是纯软件实现的。

设计需求:
1、需要根据一定的规则进行报文的合并。需要支持各种协议的合并规则。linux实现中是要求支持GRO功能的协议自己实现自己的合并函数,gro根据报文类型调用相应的合并函数。

2、对等待合并的报文应该进行缓存。等合并好后再送进协议栈进行处理。linux实现中在每个NAPI实例中放有一个等待合并的skb队列gro_list。

数据结构:

1、NAPI中GRO相关的字段:

view sourceprint?

1.
struct

napi_struct

2.
{

3.
unsigned
int
gro_count;

//gro_list 上挂的skb 个数。

4.
struct

sk_buff  *gro_list;
//等待合并的skb 链表

5.
}


2、每个协议中定义自己的GRO接收合并函数和合并后处理函数。

接收合并函数定义:

struct sk_buff**(*gro_receive)(struct sk_buff **head,struct sk_buff *skb);
参数:

head:等待合并的skb链表头

skb:接收到的skb。

返回值:

如果为空,表示报文被合并后不需要现在送入协议栈。

如果不为空,表示返回的报文需要立即送入协议栈。

合并后处理函数定义:

int(*gro_complete)(struct sk_buff *skb);
该函数对合并好的报文进行进一步加工,比如更新校验和。

3、GRO功能使用skb结构体内私有空间cb[48]来存放gro所用到的一些信息。
定义结构体struct napi_gro_cb

view sourceprint?

01.
struct

napi_gro_cb

02.
{

03.
/*指向存在skb_shinfo(skb)->frag[0].page页的数据的头部,

04.
  
GRO使用过程中,如果skb是线性的,就置为空。

05.
  
如果是非线性的并且报文头部全部存在非线性区中,

06.
  
就指向页中的数据起始部分

07.
*/

08.
void

*frag0;

09.

10.
/*第一页中数据的长度,如果frag0 字段不为空,

11.
  
就设置该字段,否则为0。( Length of frag0.)

12.
*/

13.
unsigned
int
frag0_len;


14.
 

15.
/*This indicates where we are processing

16.
  
relative to skb->data.

17.
  
表明skb->data到GRO需要处理的数据区的偏移量。

18.
  
因为在GRO合并处理过程中skb->data是不能被改变的,

19.
  
所以需要使用该字段来记录一下偏移量。

20.
  
GRO处理过程中根据该记录值快速找到要处理的数据部分。

21.
  
比如进入ip层进行GRO处理,这时skb->data指向ip 头,

22.
  
而ip层的gro 正好要处理ip头,这时偏移量就为0.

23.
  
进入传输层后进行GRO处理,这时skb->data还指向ip头,

24.
  
而tcp层gro要处理tcp头,这时偏移量就是ip头部长度。

25.
*/

26.
int

data_offset;

27.
 

28.
/*This is non-zero if the packet may be of the same flow.

29.
  
标记挂在napi->gro_list上的报文是否跟现在的报文进行匹配。

30.
  
每层的gro_receive都设置该标记位。

31.
  
接收到一个报文后,使用该报文和挂在napi->gro_list 上

32.
  
的报文进行匹配。

33.
  
在链路层,使用dev 和 mac头进行匹配,如果一样表示两个报文是通一个

34.
  
设备发过来的,就标记napi->gro_list上对应的skb的same为1.

35.
  
到网络层,再进一步进行匹配时,只需跟napi->list上刚被链路层标记

36.
  
same为1的报文进行网络层的匹配即可,不需再跟每个报文进行匹配。

37.
  
如果网络层不匹配,就清除该标记。

38.
  
到传输层,也是只配置被网络层标记same为1 的报文即可。

39.
  
这样设计为的是减少没必要的匹配操作

40.
*/

41.
int

same_flow;

42.
 

43.
/*This is non-zero if the packet cannot be merged

44.
  
with the new skb.

45.
  
如果该字段不为0,表示该数据报文没必要再等待合并,

46.
  
可以直接送进协议栈进行处理了

47.
*/

48.
int

flush;

49.
  

50.
/*该报文被合并过的次数 ,Number of segments aggregated. */

51.
int

count;

52.
 

53.
/* Free the skb? ,是否该被丢弃*/

54.
int

free
;

55.
};

56.
#define NAPI_GRO_CB(skb) ((struct napi_gro_cb *)(skb)->cb)


NAPI_GRO_CB(skb) 的初始化:
skb_reset_offset() 来重置gro
的 cb区域。如果是skb非线性的,并且本身不包含数据(包括头也没有),而所有的数据都保存在skb_shared_info中(支持S/G的网卡有可能会这么做)。
因为合并报文时需要报文头的信息,这时报文头是存在skb_shared_info的frags[0]中的,我们使用指针指向正确的报文头部。

view sourceprint?

01.
void

skb_gro_reset_offset(
struct

sk_buff *skb)

02.
{

03.
NAPI_GRO_CB(skb)->data_offset = 0;

04.
NAPI_GRO_CB(skb)->frag0 = NULL;

05.
NAPI_GRO_CB(skb)->frag0_len = 0;

06.

07.
/*如果skb 不包括数据并且skb_shinfo(skb)->frags[0].page 不在

08.
  
高端内存中,表示报文头存在skb_shinfo(skb)->frags[0].page中

09.
*/

10.
if

(skb->mac_header == skb->tail &&

11.
!PageHighMem(skb_shinfo(skb)->frags[0].page))

12.
{

13.
NAPI_GRO_CB(skb)->frag0 =

14.
page_address(skb_shinfo(skb)->frags[0].page) +

15.
skb_shinfo(skb)->frags[0].page_offset;

16.
NAPI_GRO_CB(skb)->frag0_len =

17.
skb_shinfo(skb)->frags[0].size;

18.
}

19.
}


GRO在如下地方将报文送进协议栈进行处理:
1、当napi的循环执行完毕时,也就是执行napi_complete的时候,调用napi_gro_flush来把能送协议栈的报文送给协议栈。一般调用napi_complete时,是NAPI一次轮询就处理完了全部的报文,这时短期内网卡可能不会进行报文的接收,所有要把napi->gro_list上的报文都送到协议栈,不用再等待合并后再送了。

view sourceprint?

01.
void

napi_complete(
struct

napi_struct *n)

02.
{

03.
......

04.
/*把napi->gro_list上的所有报文调用napi_gro_complete都

05.
  
送给协议栈,并清空grp_list

06.
*/

07.
napi_gro_flush(n);

08.
......

09.
}

10.
void

napi_gro_flush(
struct

napi_struct *napi)

11.
{

12.
struct

sk_buff *skb, *next;

13.
for

(skb = napi->gro_list; skb; skb = next)

14.
{

15.
next = skb->next;

16.
skb->next = NULL;

17.
napi_gro_complete(skb);

18.
}

19.
napi->gro_count = 0;

20.
napi->gro_list = NULL;

21.
}


2、在napi_skb_finish里,他会通过判断__napi_gro_receive的返回值,来决定是需要将数据包立即送进进协议栈还是保存起来。

view sourceprint?

01.
int

napi_skb_finish(
int
ret,
struct
sk_buff *skb)

02.
{

03.
int

err = NET_RX_SUCCESS;

04.
switch

(ret)

05.
{

06.
/*如果返回NORMAL,就把报文送给协议栈进行处理*/

07.
case

GRO_NORMAL:

08.
return

netif_receive_skb(skb);

09.
/*如果报文经过检查被丢弃了,释放内存并直接返回*/

10.
case

GRO_DROP:

11.
err = NET_RX_DROP;

12.
   

13.
   
/*如果报文被合并了,这时报文已经被copy走了,

14.
 
释放该报文占用的内存

15.
*/

16.
case

GRO_MERGED_FREE:

17.
kfree_skb(skb);

18.
break
;

19.
}

20.
return

err;

21.
}


GRO
的收包函数:

支持GRO功能的网卡驱动必须支持NAPI接口并调用GRO的专用接收函数napi_gro_receive()来把报文送给协议栈进行处理。

view sourceprint?

1.
int

napi_gro_receive(
struct

napi_struct *napi,

2.
 
struct

sk_buff *skb)

3.
{

4.
skb_gro_reset_offset(skb);

5.
return

napi_skb_finish(__napi_gro_receive(napi, skb), skb);


6.
}


napi_skb_finish根据__napi_gro_receive(napi,
skb)函数返回的结果,来处理报文。如果合并完成或不需要gro处理,返回GRO_NORMAL。

__napi_gro_receive()算是链路层上实现的gro_receive函数,详解见下文。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐