linux 网卡gso,linux内核网络协议栈学习笔记:关于GRO/GSO/LRO/TSO等patch的分析和测试...

TSO,全称是TCP Segmentation Offload,我们知道通常以太网的MTU是1500,除去TCP/IP的包头,TCP的MSS (Max Segment Size)大小是1460,通常情况下协议栈会对超过1460的TCP payload进行segmentation,保证生成的IP包不超过MTU的大小,但是对于支持TSO/GSO的网卡而言,就没这个必要了,我们可以把最多64K大小的TCP payload直接往下传给协议栈,此时IP层也不会进行segmentation,一直会传给网卡驱动,支持TSO/GSO的网卡会自己生成TCP/IP包头和帧头,这样可以offload很多协议栈上的内存操作,checksum计算等原本靠CPU来做的工作都移给了网卡

GSO是TSO的增强 http://lwn.net/Articles/188489/ ,GSO不只针对TCP,而是对任意协议,尽可能把segmentation推后到交给网卡那一刻,此时会判断下网卡是否支持SG和GSO,如果不支持则在协议栈里做segmentation;如果支持则把payload直接发给网卡

ethtool -k lo

Offload parameters for lo:

rx-checksumming: on

tx-checksumming: on

scatter-gather: on

tcp segmentation offload: on

udp fragmentation offload: off

generic segmentation offload: on

generic-receive-offload: on

目前很多网卡都支持tso,但很少有支持ufo的,而gso/gro和网卡无关,只是内核的特性。gso用来delay segmentation,一直到 dev_hard_start_xmit 函数

int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,

struct netdev_queue *txq)

{

const struct net_device_ops *ops = dev->netdev_ops;

int rc;

unsigned int skb_len;

if (likely(!skb->next)) {

if (!list_empty(&ptype_all))

dev_queue_xmit_nit(skb, dev);

if (netif_needs_gso(dev, skb)) {

if (unlikely(dev_gso_segment(skb)))

goto out_kfree_skb;

if (skb->next)

goto gso;

}

......

gso:

do {

struct sk_buff *nskb = skb->next;

skb->next = nskb->next;

nskb->next = NULL;

skb_len = nskb->len;

rc = ops->ndo_start_xmit(nskb, dev);

trace_net_dev_xmit(nskb, rc, dev, skb_len);

if (unlikely(rc != NETDEV_TX_OK)) {

nskb->next = skb->next;

skb->next = nskb;

return rc;

}

txq_trans_update(txq);

if (unlikely(netif_tx_queue_stopped(txq) && skb->next))

return NETDEV_TX_BUSY;

} while (skb->next);

skb->destructor = DEV_GSO_CB(skb)->destructor;

out_kfree_skb:

kfree_skb(skb);

return NETDEV_TX_OK;

}

dev_hard_start_xmit 里判断 netif_needs_gso 判断网卡是否支持gso,如果不支持则调用 dev_gso_segment 里面又调用 skb_gso_segment 把报文分片,对于ipv4而言,实际调用了 tcp_tso_segment,最后返回多个sk_buff 组成的链表,头指针存在 skb->next 里;如果网卡本身支持的话,直接把大块的skb交给网卡:调用netdev_ops->ndo_start_xmit 发送出去

可以看到,在判断netif_need_gso时,是要检查网卡的netdev->features值的,我们可以在include/linux/netdevice.h中看到这些值:

#define NETIF_F_SG      1   /* Scatter/gather IO. */

#define NETIF_F_IP_CSUM     2   /* Can checksum TCP/UDP over IPv4. */

#define NETIF_F_NO_CSUM     4   /* Does not require checksum. F.e. loopack. */

#define NETIF_F_HW_CSUM     8   /* Can checksum all the packets. */

#define NETIF_F_FRAGLIST    64  /* Scatter/gather IO. */

#define NETIF_F_GSO     2048    /* Enable software GSO. */

#define NETIF_F_GSO_SHIFT   16

#define NETIF_F_GSO_MASK    0x00ff0000

#define NETIF_F_TSO     (SKB_GSO_TCPV4 << NETIF_F_GSO_SHIFT)

#define NETIF_F_UFO     (SKB_GSO_UDP << NETIF_F_GSO_SHIFT)

对于要支持TSO的网卡而言,需要有 NETIF_F_SG | NETIF_F_TSO | NETIF_F_IP_CSUM,相应如果要支持UFO,应该就需要 NETIF_F_SG | NETIF_F_UFO | NETIF_F_IP_CSUM

下面做个测试来考量下tso, gso对性能的影响,本人手头的测试机不支持ufo,所以只好拿tcp来测试了

scatter-gather: on

tcp segmentation offload: on

udp fragmentation offload: off

generic segmentation offload: on

generic-receive-offload: on

Recv   Send    Send

Socket Socket  Message  Elapsed

Size   Size    Size     Time     Throughput

bytes  bytes   bytes    secs.    10^6bits/sec

87380  65536  65536    10.00    26864.51

关闭了tso, gso之后

Recv   Send    Send

Socket Socket  Message  Elapsed

Size   Size    Size     Time     Throughput

bytes  bytes   bytes    secs.    10^6bits/sec

87380  65536  65536    10.00    18626.44

对于如果只是关闭gso而言,throughput不太稳定,但平均下来还是比gso打开有点降低的

顺便说下,tso, gso效果随着MTU增大越来越不明显,

#ifconfig lo mtu 65535

之后 netperf -t TCP_STREAM 测下来,tso开或者关已经差别不大了,10%左右吧

主要增加了 dev_gso_segment,skb_gso_segment 函数,修改了dev_hard_start_xmit ,dev_queue_xmit 函数,这些之前已经提过了

------------------------------ 华丽的分割线 ------------------------------------

LRO(Large Receive Offload)是针对TCP的机制,GRO(Generic Receive Offload) 是LRO的增强版,对skb merge的限制更多,同时不限于tcp/ip,本文主要讲GRO,因为LRO对于ip forward以及bridge的场景会有问题,已经用得很少了

如果驱动打开了gro特性,会调用napi_gro_receive来收包,而不是通常的netif_receive_skb或者netif_rx,可以看到gro是和napi_struct紧密绑在一起的,我们这里回到之前研究过很多遍的napi_struct结构上来

struct napi_struct {

struct list_head    poll_list;

unsigned long       state;

int         weight;

int         (*poll)(struct napi_struct *, int);

#ifdef CONFIG_NETPOLL

spinlock_t      poll_lock;

int         poll_owner;

#endif

unsigned int        gro_count;

struct net_device   *dev;

struct list_head    dev_list;

struct sk_buff      *gro_list;

struct sk_buff      *skb;

};

napi_struct 包含了 gro_list 一个skb的链表,链表中的每一个skb都代表了一个flow,gro_count代表了flow的个数

napi_gro_receive 会调用 __napi_gro_receive_gr,之后又会调用 __napi_gro_receive, __napi_gro_receive 会遍历 napi_struct->gro_list,通过比较skb->dev,和skb的mac_header来确定是否属于同一个flow,并存在napi_gro_cb->flow中。这里要提下 struct napi_gro_cb 结构,对于通过gro处理的每一个skb,都在skb->cb保存一个私有数据结构的指针,就是这个napi_gro_cb。

注意这里skb的私有数据结构只是个void*,和skb_shared_info不要搞混了,后者是在sk_buff后面的一块线性内存

struct napi_gro_cb {

/* Virtual address of skb_shinfo(skb)->frags[0].page + offset. */

void *frag0;

/* Length of frag0. */

unsigned int frag0_len;

/* This indicates where we are processing relative to skb->data. */

int data_offset;

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

int same_flow;

/* This is non-zero if the packet cannot be merged with the new skb. */

int flush;

/* Number of segments aggregated. */

int count;

/* Free the skb? */

int free;

};

然后 __napi_gro_receive 会调用 dev_gro_receive ,dev_gro_receive会先调用ptype->gso_receive,一般而言就是ip协议对应的inet_gso_receive

inet_gro_receive 主要做如下事情:

首先拿到ip包头,然后对包头做check,如果check passed则开始遍历napi_struct->gro_list,根据ip saddr, daddr, tos, protocol等来和那些之前在二层有可能是同一flow的skb进行判断,如果不一致就把same_flow置0,当然光是slow_flow并不能就此开始merge,还要进行flush的判断,任何flush判断不过都会放弃merge而调用直接调用skb_gro_flush函数交到协议栈上层去处理

ip层结束之后,会继续tcp层的gro_receive,调用tcp_gro_receive ,其核心也是遍历napi_struct->gro_list,基于source addr判断是否是same_flow,对是否需要flush做计算,这里提一下关于ack一致的要求,ack一致说明是同一个tcp payload被tso/gso分段之后的结果,所以是必需条件

如果 tcp 也认为不需要flush,那么会进到 skb_gro_receive 中,这个函数就是用来合并的,第一个参数是gro_list里的那个skb,第二个是新来的skb,这里不多说了,我推荐的博客文章里讲的很清楚了。其实就分两种情况,如果是scatter-gather的skb包,则把新skb里的frags的数据放到gro_list的skb对应的frags数据后面;否则skb数据都在skb的线性地址中,这样直接alloc一个新的skb,把新skb挂到frag_list里面,最后放到原来gro_list的位置上;如果gro_list的skb已经有了frag_list,那么就直接挂进去好了

现在返回到dev_gro_receive中了,这时如果需要flush或者same_flow为0,说明需要传给上层协议栈了,此时调用napi_gro_complete

走到最后一种情况即这个skb是个新的flow,那么就加到gro_list的链表中

最后提下,所谓flush是指把现有gro_list中的skb flush到上层协议,千万别搞反了

实际测试下来,TSO在对性能的提升上非常明显,但是GRO并不是太明显,不知道在极限的性能测试下会是神马情况

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值