【原创】Intel XL710网卡异常Reset问题复现

问题背景

       业务服务器使用IntelXL710网卡,上线使用过程中网卡突然断链,Link状态为down,而且不可恢复,必须

复位服务器才可以。但是过一段时间后,会再次出现同样的问题,而且在几个局点都出现了类似的问题。

  开始出现该问题时,根据以往经验,无非是光模块、光纤兼容问题,网卡硬件批次问题。但是随着出问题

的设备增多,次数增多,开始觉得该问题没这么简单。

  先看看生产环境收集到的信息:

       网卡状态信息

       # ip a | grep eth4

  5: eth4: <NO-CARRIER,BROADCAST,MULTICAST,SLAVE,UP> mtu 1500 qdisc mq master bond5 state DOWN qlen 1000

  GZ-SN-OTT18:~ #

  GZ-SN-OTT18:~ # ethtool eth4

  Settings for eth4:

  Supported ports: [ ]

  Supported link modes:   1000baseT/Full

                  10000baseT/Full

  Supports auto-negotiation: Yes

  Advertised link modes:  1000baseT/Full

                       10000baseT/Full

  Advertised pause frame use: No

  Advertised auto-negotiation: Yes

  Speed: Unknown!

  Duplex: Unknown! (255)

  Port: Other

  PHYAD: 0

  Transceiver: external

  Auto-negotiation: off

  Supports Wake-on: g

  Wake-on: g

  Current message level: 0x0000000f (15)

            drv probe link timer

  Link detected: no

  内核日志信息

Jul 20 11:28:38 JAedge5 kernel: [8395582.875067] i40e 0000:0a:00.2: TX driver issue detected, PF reset issued  ##网卡错误

Jul 20 11:28:38 JAedge5 kernel: klogd 1.4.1, ---------- state change ----------

Jul 20 11:28:38 JAedge5 kernel: [8395583.453724] kworker/u:4: page allocation failure: order:5, mode:0x80d0  ##分配order=5的连续page失败

Jul 20 11:28:38 JAedge5 kernel: [8395583.453734] Pid: 21027, comm: kworker/u:4 Tainted: G ENX 3.0.101-0.47.52-default #1

Jul 20 11:28:38 JAedge5 kernel: [8395583.453738] Call Trace:

Jul 20 11:28:38 JAedge5 kernel: [8395583.453762]  dump_trace+0x75/0x300

Jul 20 11:28:38 JAedge5 kernel: [8395583.453777]  dump_stack+0x69/0x6f

Jul 20 11:28:38 JAedge5 kernel: [8395583.453790]  warn_alloc_failed+0xc6/0x170

Jul 20 11:28:38 JAedge5 kernel: [8395583.453802]  __alloc_pages_slowpath+0x561/0x7f0

Jul 20 11:28:38 JAedge5 kernel: [8395583.453812]  __alloc_pages_nodemask+0x1e9/0x200

Jul 20 11:28:38 JAedge5 kernel: [8395583.453823]  dma_generic_alloc_coherent+0xa6/0x160

Jul 20 11:28:38 JAedge5 kernel: [8395583.453837]  x86_swiotlb_alloc_coherent+0x28/0x80

Jul 20 11:28:38 JAedge5 kernel: [8395583.453875]  i40e_setup_tx_descriptors+0xf1/0x140 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.453971]  i40e_vsi_setup_tx_resources+0x2d/0x60 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454003]  i40e_vsi_open+0x27/0x1c0 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454046]  i40e_open+0x5b/0x90 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454078]  i40e_pf_unquiesce_all_vsi+0x30/0x50 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454114]  i40e_reset_and_rebuild+0x2f2/0x600 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454156]  i40e_reset_subtask+0xf8/0x100 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454189]  i40e_service_task+0x108/0x300 [i40e]

Jul 20 11:28:38 JAedge5 kernel: [8395583.454217]  process_one_work+0x16c/0x350

Jul 20 11:28:38 JAedge5 kernel: [8395583.454229]  worker_thread+0x17a/0x410

Jul 20 11:28:38 JAedge5 kernel: [8395583.454261]  kthread+0x96/0xa0

Jul 20 11:28:38 JAedge5 kernel: [8395583.454272]  kernel_thread_helper+0x4/0x10

Jul 20 11:28:39 JAedge5 kernel: [8395583.454280] Mem-Info:

Jul 20 11:28:39 JAedge5 kernel: [8395583.454283] Node 0 DMA per-cpu:

Jul 20 11:28:39 JAedge5 kernel: [8395583.454288] CPU    0: hi:    0, btch:   1 usd:   0

Jul 20 11:28:39 JAedge5 kernel: [8395583.454292] CPU    1: hi:    0, btch:   1 usd:   0

Jul 20 11:28:39 JAedge5 kernel: [8395583.454296] CPU    2: hi:    0, btch:   1 usd:   0

  

   

 

内核异常日志分析

 

  从网卡和内核日志分析,这台设备eth4网卡是down的,是由于网卡出现PF reset打印,引起网卡

重新创建Rx/Tx descriptor,但是由于内存不足(应该是需要的连续内存页不能满足),分配descriptor

失败,导致网卡启动失败,最终引起网卡Link down状态。

  XL710 PF reset:

i40e 0000:0a:00.2: TX driver issue detected, PF reset issued

网口PF_RESET在驱动里是探测到MDD事件,即是 Malicious Driver Detection Event

通过 I40E_GL_MDET_TX0x000E6480)获取。

  这个问题问过Intel技术支持,由于目前底层都被封装起来了,所以也不知道是什么类型的错误。

  XL710网卡内存分配失败

  page allocation failure: order:5, mode:0x80d0

  PF重启时,需要重新分配网卡descriptor资源,需要连续的内存页。当系统内存碎片严重时,会出现分配page失败,导致网口再也up不起来。

 

  网卡重建队列调用堆栈:

  i40e_reset_and_rebuild

   --> i40e_pf_unquiesce_all_vsi

        --> i40e_open

       --> i40e_vsi_open

         --> i40e_vsi_setup_tx_resources

            --> i40e_setup_tx_descriptors

             --> x86_swiotlb_alloc_coherent

               --> dma_generic_alloc_coherent

                 --> __alloc_pages_nodemask

                   --> __alloc_pages_slowpath

  void *dma_generic_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp)

   {

     void *ret, *ret_nocache;

     int order = get_order(size);

     gfp |= __GFP_ZERO;

     ret = (void *)__get_free_pages(gfp, order);

     if (!ret)

       return NULL;

     ....

   }

     在网上也搜索了相关的资料,有说是网卡自身问题,要把TCP segment offload关掉,生产环境也做了相关操作,

确实也没有再出现PF reset的问题。这也只是临时规避措施。分配连续page失败是附带的问题,根本还是要找到网卡

产生PF reset的原因。

根本原因

    这里要感谢Intel技术工程师,出现这个问题后,反复和他们进行了沟通和交流,收集了很多信息,也联系了美国

团队技术专家。最终,我们在Intel一篇技术文档中找到了问题的可疑原因:

   Intel specification update文档

Intel ® Ethernet Controller X710/XXV710/XL710
Specification Update
Ethernet Networking Division (ND)

 Revision 3.3

  这是一个安全漏洞,是在2017年1月10号加入这个文档的。

怎么理解和复现问题?

   从文档描述中,可疑肯定一点,这个问题与TSO相关,在生产环境关闭TSO后,也确实没有

再次出现问题。但是,怎么理解文档描述的问题?又怎么复现这个问题,得到和生产环境一样的

错误信息呢?

  先试着翻译一下:

  当一个TSO报文的第一个Tx descriptor包含报文头和数据载荷时,在MAX_BUFFS的异常检测中

被计算两次。因此,如果第一个segment分布在8个descriptor中,就会报告一个MDD时间。然而,如果

一个segement中超过8个descriptor时,它应该只产生一个MDD事件。

  这会导致虚假的MDD(Malicious Driver Detection)事件。

  软件驱动必须限制一个TSO报文的第一个段(segment)包含7个descriptor,而不是8个descriptor。

这个现实已经在Intel 21.3驱动版本中实现。

bug说明:

      从文档描述看,与发送TSO数据报文有关,而LInux协议栈发送TSO数据报文是通过skb数据结构传递到

网卡驱动的,我们可以从skb入手,看看什么样的skb数据会导致整个问题。

  文档描述中的7个descriptor又是什么意思?什么样的skb数据才能算是7个descriptor?这要从I40E驱动

里面寻找答案。

I40E驱动如何计算descriptor个数

  PS:以I40E 1.4.25驱动源码为基础进行说明。

  I40E驱动发包函数为i40e_xmit_frame_ring,进入后调用i40e_xmit_descriptor_count计算descriptor是否充足,

否则返回NETDEV_TX_BUSY。

  计算descriptor函数如下:

 1 #ifndef DIV_ROUND_UP
 2 #define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))
 3 #endif
 4 
 5 #define I40E_MAX_DATA_PER_TXD    8192
 6 
 7 /* Tx Descriptors needed, worst case */
 8 #define TXD_USE_COUNT(S) DIV_ROUND_UP((S), I40E_MAX_DATA_PER_TXD)
 9 
10 
11 #ifdef I40E_FCOE
12 inline int i40e_xmit_descriptor_count(struct sk_buff *skb,
13                       struct i40e_ring *tx_ring)
14 #else
15 static inline int i40e_xmit_descriptor_count(struct sk_buff *skb,
16                          struct i40e_ring *tx_ring)
17 #endif
18 {
19     unsigned int f;
20     int count = 0;
21 
22     /* need: 1 descriptor per page * PAGE_SIZE/I40E_MAX_DATA_PER_TXD,
23      *       + 1 desc for skb_head_len/I40E_MAX_DATA_PER_TXD,
24      *       + 4 desc gap to avoid the cache line where head is,
25      *       + 1 desc for context descriptor,
26      * otherwise try next time
27      */
28     for (f = 0; f < skb_shinfo(skb)->nr_frags; f++)
29         count += TXD_USE_COUNT(skb_shinfo(skb)->frags[f].size);
30 
31     count += TXD_USE_COUNT(skb_headlen(skb));
32     if (i40e_maybe_stop_tx(tx_ring, count + 4 + 1)) {
33         tx_ring->tx_stats.tx_busy++;
34         return 0;
35     }
36     return count;
37 }

  

  一个skb需要的TXD(Transmit Descriptor)计算分为两部分,一个是非线性区每个page需要的TXD计算,

另一个是线性区数据需要的TXD计算。同时,要预留5个TXD,其中一个是context TXD,另外4个TXD是为避免

head的cache line而保留的TXD间隔(gap)。

  每个TXD最大的数据长度是8192字节(I40E_MAX_DATA_PER_TXD)。

  注意:I40E_MAX_DATA_PER_TXD这个值在后来的高版本驱动中是12K。

  综上,bug说明中所说的一个segment分布在8个TXD中,即skb线性区数据占用一个TXD,skb frag占用7个TXD。

这样,可以很容易构造使得I40E网卡产生MDD异常的SKB数据。

复现代码 & skb结构

构造特殊skb数据

 #define WIT_TCP_SEG_NUM   7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN  150
#define WIT_SKB_LINE_LEN 260

#define WIT_MSS  1460
#define WIT_SKB_CACHE_LENGTH 512

/* 构造能够产生MDD事件的tcp skb */
static int mdd_tcp_skb(struct sk_buff **skb)
{
    /* 核心代码 */
    int skb_page_offset = 0;
    int pgrefcnt = 0;
    int page_index = 0;
    int rc = 0;
    struct page *linear_page=NULL;
    unsigned int headroom;
    unsigned int iphdr_length;
    unsigned int ethhdr_length;
    unsigned int tcphdr_length;
    unsigned int payload_length = 0;
    
    unsigned char * pdata=NULL;
    
    /* 分配页面,存放skb非线性区数据 */
    linear_page = alloc_pages(GFP_ATOMIC|__GFP_COMP, get_order(WIT_TCP_DATA_LEN));
    if (!linear_page)
    {
        rc = -ENOMEM;
        goto alloc_page_err;
    }
    /* 转化为线性虚拟地址,便于访问页面 */    
    linear_vaddr = page_address(linear_page);
    
    struct sk_buff *nskb=NULL;

    /*分配skb,并设置。*/
    /*
    skb->head = data;
    skb->data = data;
    skb_reset_tail_pointer(skb);
    skb->end = skb->tail + size;
    */
    nskb = __alloc_skb(WIT_SKB_CACHE_LENGTH, GFP_ATOMIC, 0, nid/* numa id */);
    
    /*需要重新分配skb,没有可复用的。*/
    if (unlikely(nskb==NULL))
    {
        BUG_ON(nskb==NULL);
        rc = -ENOMEM;
        goto alloc_skb_err;
    }
    
    /* nskb线性区保留空间大小,即将头部指针往后移动,然后从高层协议开始填充。*/
    /* skb_reserve operation result:
    skb->data += len;
    skb->tail += len;
    */
    headroom = 2;
    iphdr_length = sizeof(struct iphdr);
    ethhdr_length = sizeof(struct ethhdr);
    tcphdr_length = sizeof(struct tcphdr);
    skb_reserve(nskb, headroom + ethhdr_length + iphdr_length + tcphdr_length);
    
    /* 存放线性区数据 */
    /*
    unsigned char *tmp = skb_tail_pointer(skb);
    skb->tail += len;
    skb->len  += len;
    返回 tmp,即移动之前的skb->tail
    */
    pdata = __skb_put(nskb, WIT_SKB_LINE_LEN);
    memset(pdata, 0xAA, WIT_SKB_LINE_LEN);

    /* 指定一个page frag包含的数据长度 */
    payload_length = WIT_TCP_SEG_LEN;
    
    /*数据页面直接改用增加页面引用计数。*/
    for(page_index = 0; page_index < 8; page_index++)
    {
        skb_fill_page_desc(nskb, page_index, linear_page, skb_page_offset, payload_length);
        /* liangjs modi 2015-7-16, if rtphdr_length, second reference page, or it's first ref page, unnecssary */
        if(pgrefcnt > 0)
        {
            get_page(linear_page);
        }
        pgrefcnt++;
        skb_page_offset += payload_length;
        nskb->len += payload_length;
        nskb->data_len += payload_length;
        nskb->truesize += payload_length;
    }
    
    //填写包头 ether,ip and tcp header
    /* skb是从原始sk_buff,里面存放有 二层到四层 报文头 信息 */
    fill_mdd_skb_tcphdr(nskb);
    
    /* 计算 nskb中 tcp seg个数,tcp最大段长度(MSS), 以及GSO类型 */
    struct skb_shared_info *shinfo = skb_shinfo(nskb);
    
    shinfo->gso_segs = DIV_ROUND_UP(nskb->len, WIT_MSS);
    shinfo->gso_size = WIT_MSS;
    shinfo->gso_type = SKB_GSO_TCPV4; //sk->sk_gso_type;
    
    return rc;
    
alloc_skb_err:
    if (linear_page)
    {
        /* liangjs note 2014-08-22: need not put_page   */
        printk("%s:%d: linear_page = %p ", __FUNCTION__, __LINE__, linear_page);
        __free_pages(linear_page, get_order(WIT_TCP_DATA_LEN));
    }
    
alloc_page_err:
    *skb = NULL;
    return rc;
}
    
static inline int fill_mdd_skb_tcphdr(struct sk_buff *nskb)
{
    struct ethhdr * pethhdr=NULL;
    struct iphdr * piphdr=NULL;
    struct tcphdr *ptcphdr=NULL;    
 
 #if 0
    struct tcphdr {
    __be16    source;
    __be16    dest;
    __be32    seq;
    __be32    ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u16    res1:4,
        doff:4,
        fin:1,
        syn:1,
        rst:1,
        psh:1,
        ack:1,
        urg:1,
        ece:1,
        cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u16    doff:4,
        res1:4,
        cwr:1,
        ece:1,
        urg:1,
        ack:1,
        psh:1,
        rst:1,
        syn:1,
        fin:1;
#else
#error    "Adjust your <asm/byteorder.h> defines"
#endif    
    __be16    window;
    __sum16    check;
    __be16    urg_ptr;
};
 #endif

    /*调整UDP指针*/
    skb->protocol = __constant_htons(ETH_P_IP)
    ptcphdr = (struct tcphdr *)__skb_push(nskb, sizeof(struct tcphdr));
    skb_set_transport_header(nskb, 0);
    
    memset(ptcphdr, 0, sizeof(struct tcphdr));
    ptcphdr->source = htons(61369);
    ptcphdr->dest = htons(9527);
    ptcphdr->seq = 0x3b5f862d;
    ptcphdr->ack_seq = 0xe8f76aee;
    ptcphdr->doff = 5;
    ptcphdr->fin = 0;
    ptcphdr->syn = 0;
    ptcphdr->rst = 0;
    ptcphdr->psh = 0;
    ptcphdr->ack=1;
    #if 0
    ptcphdr->urg:1,
    ptcphdr->ece:1,
    ptcphdr->cwr:1;
    #endif
    ptcphdr->window = 0x0073;
    ptcphdr->check = 0;
    ptcphdr->urg_ptr = 0;

    /*ip头部*/
    piphdr = (struct iphdr * )__skb_push(nskb, sizeof(struct iphdr));
    //skb_copy_from_linear_data_offset(skb, skb_network_offset(skb),
    //    (unsigned char *)piphdr, sizeof(struct iphdr));
    piphdr->version = 4;    
    piphdr->protocol = 0x06;  /* TCP protocol */
    piphdr->saddr = 0x01010101;
    piphdr->daddr = 0x02020202;
    skb_set_network_header(nskb, 0);

    piphdr->ihl = sizeof(struct iphdr)/4;
    piphdr->tot_len = htons(nskb->len);
    piphdr->check = 0;
    piphdr->frag_off = htons(0x4000);
    piphdr->check = ip_fast_csum((unsigned char *)piphdr, piphdr->ihl);

    nskb->csum = 0;
    /* 这个赋值很重要,因为在I40E驱动的i40e_tso函数中,非CHECKSUM_PARTIAL,直接返回了 */
    nskb->ip_summed = CHECKSUM_PARTIAL;//CHECKSUM_UNNECESSARY;

    /*eth头部*/        
    pethhdr = (struct ethhdr * )__skb_push(nskb, sizeof(struct ethhdr));
    skb_set_mac_header(nskb, 0);
    //skb_copy_from_linear_data_offset(skb, 0,
    //    (unsigned char *)pethhdr, sizeof(struct ethhdr) );
    unsigned char dest[ETH_ALEN] = {0x98,0xf5,0x00,0x01,0x02,0x03};
    unsigned char src[ETH_ALEN] = {0x98,0xf5,0x00,0x01,0x02,0x04};
    memcpy(pethhdr->h_dest, dest, ETH_ALEN);
    memcp(pethhdr->h_source, src, ETH_ALEN);
    pethhdr->h_proto = 0x0800;
    
    nskb->mac_len = sizeof(struct ethhdr);
    nskb->dev = skb->dev;
    nskb->queue_mapping = skb->queue_mapping;
    nskb->protocol = skb->protocol;
    nskb->vlan_tci = skb->vlan_tci;
    nskb->pkt_type = skb->pkt_type;
    nskb->mark = 0;

    return 1;
}

 测试复现

测试用例1:
#define WIT_TCP_SEG_NUM   7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN  150
#define WIT_SKB_LINE_LEN 400

#define WIT_MSS  1450

内核日志:
Nov 15 20:32:18 linux kernel: [18363.480323] i40e 0000:0a:00.3: TX driver issue detected, PF reset issued


测试用例2  :
#define WIT_TCP_SEG_NUM   7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN  150
#define WIT_SKB_LINE_LEN 410

#define WIT_MSS  1460

内核日志:
[70466.672761] i40e 0000:0a:00.3: TX driver issue detected, PF reset issued
[70467.512354] bonding: bond0: link status up again after 0 ms for interface eth0.

测试用例3 :
#define WIT_TCP_SEG_NUM   8
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN  150
#define WIT_SKB_LINE_LEN 260

#define WIT_MSS  1460

测试结果:
no PF reset


测试用例4  :
#define WIT_TCP_SEG_NUM   7
#define WIT_TCP_DATA_LEN 4000
#define WIT_TCP_SEG_LEN  150
#define WIT_SKB_LINE_LEN 260

#define WIT_MSS  1460

# dmesg -c
[73345.238145] i40e 0000:0a:00.3: TX driver issue detected, PF reset issued
[73345.847481] bonding: bond0: link status up again after 0 ms for interface eth0.


更换驱动和固件:驱动:i40e-2.0.30  固件 5.05
重复以上测试用例,没有再产生PF Reset错误。
 

skb结构复现总结

1. skb->data 线性区 有报文头 和 要发送的数据;

2. 总共有7page frag

3. 线性区数据 + page data_len <= mss_now(gso_size)

后记

  软件也是人写的,是人都会犯错误,所以需要测试的介入,尽可能保证故障不外泄。

针对Intel X710网卡的故障,个人愚见,如果进行分支覆盖测试和边界测试,可能也就

不会出现这么严重的BUG了;或者驱动和固件进行整合测试,可能也会发现两边不一致的

情况。软件的任何改动都需要标记和记录,都是测试的重点。

  出现该问题的时候,着实困惑了很久,排查过程中,甚至怀疑bond驱动和Linux系统BUG,

或者交换机BUG,为此还专门咨询了在友商交换机部门工作的同学Σ( ° △ °|||)︴。虽然不是这几个

问题,但也学习了相关的模块和技术,也算是一种收获吧。

  向Intel技术支持咨询时,总是建议我们升级驱动和固件,后来也确实是通过升级解决问题;

从网上也搜到了相关的问题,关闭TSO也能规避问题。但是作为一位程序员总想搞清楚为什么

会这样(其实有个领导也要求排查出问题),这样才能安心升级驱动呀,否则向局方说升级驱动

能解决问题,如果又出相同问题,就是打自己的脸呀!

  还好,有Intel的相关勘误表,我们终于知道了原因,并且也可以复现出来。总算圆满解决。

  其实Intel有很多技术文档,包括勘误表,都在它的官方网站上,关键是怎么去查找。而且,

文档中的描述和现象不是一致,而是另外一种说法,所以说要理解技术后面的意思非常重要。

  

PS:您的支持是对博主最大的鼓励,感谢您的认真阅读。
        本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,

      且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/smith9527/p/10522501.html