网卡驱动收发包sk_buff相关分析

sk_buff相关字段

 
struct sk_buff {
    //报文在每一层或每一模块处理时,可以存储一些私有临时数据
    char            cb[48] __aligned(8);

    unsigned int    len,  //sk_buff的data存储的数据长度 + frags里面存储的数据的长度
          //如果skb使用frag_list串起来了,那么第一个skb的len要加上后面所有skb的len
                    data_len; //仅仅指frags里面存储的数据的长度
          //如果skb使用frag_list串起来了,那么第一个skb的data_len要加上后面所有skb的len
    __u16           hdr_len;  //clone skb后要使用的字段,具体未分析

    sk_buff_data_t      tail;
    sk_buff_data_t      end;
    unsigned char       *head,
                        *data;
    unsigned int        truesize; //申请的内存数据的总大小,
         //包括sk_buff结构长度 + 申请的data总长度 + frags里面每个分片的长度。
         //如果skb使用frag_list串起来了,那么第一个skb的truesize要加上后面所有skb的truesize

};

frag_list和frags[]

struct skb_shared_info {
    __u8        flags;
    __u8        meta_len;
    __u8        nr_frags;
    __u8        tx_flags;
    unsigned short  gso_size;
    /* Warning: this field is not always filled in (UFO)! */
    unsigned short  gso_segs;
    struct sk_buff  *frag_list; //将多个skb串起来,这个是链头字段
    struct skb_shared_hwtstamps hwtstamps;
    unsigned int    gso_type;
    u32     tskey;

    /*
     * Warning : all fields before dataref are cleared in __alloc_skb()
     */
    atomic_t    dataref;
    unsigned int    xdp_frags_size;

    /* Intermediate layers must ensure that destructor_arg
     * remains valid until skb destructor */
    void *      destructor_arg;

    /* must be last field, see pskb_expand_head() */
    skb_frag_t  frags[MAX_SKB_FRAGS];
};

struct bio_vec {
    struct page *bv_page;
    unsigned int    bv_len;
    unsigned int    bv_offset;
};

typedef struct bio_vec skb_frag_t;

#ifndef CONFIG_MAX_SKB_FRAGS
# define CONFIG_MAX_SKB_FRAGS 17
#endif

#define MAX_SKB_FRAGS CONFIG_MAX_SKB_FRAGS

        在发送方向,内核给网卡发送的skb,不需要考虑frag_list的的情况。内核可能会在网络协议栈内部使用frag_list,但是将报文发送给网卡驱动的时候,不会使用frag_list。

        发送方向,网卡支持SG(NETIF_F_SG)的话,可能会将一个报文分成多个frags发送,此时会使用frags[MAX_SKB_FRAGS],nr_frags为实际的frags的个数。此时skb->data中还是会存储报文的一部分信息,通常为网络head信息,frags[]会存储报文的后续部分,通常情况下为L4的payload。

        接收方向,如果存在SG的情况,frag_list和frags[]都可能使用,但只会使用一种,各个网卡厂商的实现不一样。例如华为的hinic使用frag_list,但是使用frag_list的厂家相对来说比较少,大部分还是使用frags[],例如intel和mlx的网卡。

        使用frags[]相对于使用frag_list来说,省略了多个skb的申请,相对来说效率更高。skb支持frags[]和frag_list共同存在的场景。

主机TX报文

        TX报文不会使用skb的frag_list,只可能使用frags[MAX_SKB_FRAGS]。目前内核默认支持的最大分片数为17,加上skb自己的data,网卡要支持SG的话,应该要支持17+1=18个分片。

        网卡支持SG必须给netdev->features打上NETIF_F_SG标记。内核在构造skb的时候,会查找出口网卡上的特性是否有NETIF_F_SG标记,如果没有这个标记,则不会使用frags[MAX_SKB_FRAGS],而是申请一个大的skb data存储数据,这在某些情况下可能效率会比较低。

        内核发送一个报文时,确定了一个出接口后,还确定一个出队列。这个是通过设置skb->queue_mapping来确定队列。该字段的设置,驱动可以参与,即注册net_device_ops的ndo_select_queue回调函数来根据skb信息,返回一个queue index。驱动也可以不注册ndo_select_queue回调函数,这样内核会用默认的函数来根据skb信息,决策出一个queue index。

        virtio-net和华为hinic驱动都没有注册ndo_select_queue,intel、mlx、broadcom、marvell的网卡都注册了。

        其中broadcom和intel 82599都是在以太网光纤通道FCOE场景下才用到。

        Intel E810是考虑到DCB场景会自己决策queue。即网卡自己配置了dscp的mapping表,此时要根据配置的dscp mapping表,选择一个队列。

        marvell是考虑到自己虚报了netdev->real_num_tx_queues个数,需要自己修正queue mapping。

        mlx4是考虑到目前可能存在部分队列down的情况,此时也要自己修正queue mapping。

        mlx5比较复杂,涉及到dscp、vlan优先级选择队列,还涉及到channel相关的选择。具体没仔细分析。

        内核自己决策skb发送队列的流程,在如下的链接中有介绍:

          https://blog.csdn.net/qq_34258344/article/details/109447837

        总结下有以下几种选择方式:

  • 根据skb的socket确定发送队列

      例如tcp的一个流的第一个报文确定一个发送队列后,会将发送队列index写到socket中,发送后续的skb,会使用socket中记录的队列index作为发送队列。

  • 根据XPS(Transmit Packet Steering)来选择队列

      发送数据包控制(XPS)是一项功能,允许系统管理员配置哪些 CPU 可以处理网卡的哪些发送 队列。XPS 的主要目的是避免处理发送请求时的锁竞争。使用 XPS 还可以减少缓存驱逐, 避免NUMA机器上的远程内存访问等。

  • 根据报文信息的hash、优先级和接口配置的TC mapping表来选择队列

      TC mapping表对于同一个优先级,可能会有一组队列,利用报文的hash值,在这一组队列中hash选择一个队列。接口的TC mapping是驱动配置的。

      如果没有TC,则将整个可用队列作为一组,在该组中hash选择一个队列。

      hash值通常是报文的N元组计算的hash值,但是如果报文是从一个网卡队列收上来的,然后经过IP协议栈后再转出去,那么此时会用收包的队列作为hash值来选择一个队列。

硬件队列满的情况下,发送不出去,各个网卡的实现不一样:

  • Virtio-net、mlx4:

        drop skb,且返回NETDEV_TX_OK

  • mlx5:

        似乎不会出现满的情况,有fifo缓存skb(fifo不会满?)

  • broadcom、ixgbe、E810、marvell octeontx2、hinic:

      调用netif_tx_stop_queue,stop queue,返回NETDEV_TX_BUSY

返回NETDEV_TX_BUSY,内核会将skb缓存在qdisc(Queueing Discipline)的缓存队列中?

网卡RX收包的实现

每个rxq都有一个napi,需要初始化,并将收到的报文通过napi接口上送协议栈处理,有些实现还通过napi来申请skb。不同厂家的网卡收包的实现方式不一样,这里选取几种介绍下。

华为hinic实现

  • 填充rxq的描述符

    • 预先申请好skb,每个skb大小2K。dma map每个skb->data为dma addr,将dma addr写入rx描述符。同时驱动要记录rx描述符和skb的映射关系,以便收到一个描述符时,找到对应的skb。具体实现是记录一个影子队列,在对应的index位置上,其存储的skb和描述符队列中对应位置的描述符对应,这样可以通过收到的描述符,在tx queue中对应的位置找到skb。

      //申请skb的接口为: 
      static inline struct sk_buff *netdev_alloc_skb_ip_align(struct net_device *dev, unsigned int length)
  • 接收收包

      收到一个大包,可能需要多个skb存储,此时会通过frag_list的方式将这些skb串起来。

  • 将报文送给内核

        调用napi_gro_receive,将报文交给协议栈处理

gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)

mlx4(ConnectX-3)实现

  • 填充rxq的描述符

      描述符中填充的不是skb的data,而是单独找linux内核申请的物理页。调用alloc_page申请一个page,大小为内核页大小。然后调用dma_map_page mapping成dma地址

  • 接收收包

      收到一个报文,会申请一个skb,这个skb的线性部分(skb本身的data)不会存储数据,会把数据存储在frags[MAX_SKB_FRAGS]。如果一个打包分多个page存储的话,会有多个frags,nr_frags>1。即使只有一个page,也会使用frags,nr_frags=1。
    //调用napi_get_frags申请skb,此时申请的skb的大小不大,为GRO_MAX_HEAD定义的大小,200多字节。
    //申请好的skb会在napi->skb临时存储
    struct sk_buff *napi_get_frags(struct napi_struct *napi)
    
    #if defined(CONFIG_HYPERV_NET)
    # define LL_MAX_HEADER 128
    #elif defined(CONFIG_WLAN) || IS_ENABLED(CONFIG_AX25)
    # if defined(CONFIG_MAC80211_MESH)
    #  define LL_MAX_HEADER 128
    # else
    #  define LL_MAX_HEADER 96
    # endif
    #else
    # define LL_MAX_HEADER 32
    #endif
    
    #if !IS_ENABLED(CONFIG_NET_IPIP) && !IS_ENABLED(CONFIG_NET_IPGRE) && \
        !IS_ENABLED(CONFIG_IPV6_SIT) && !IS_ENABLED(CONFIG_IPV6_TUNNEL)
    #define MAX_HEADER LL_MAX_HEADER
    #else
    #define MAX_HEADER (LL_MAX_HEADER + 48)
    #endif
    
    #define GRO_MAX_HEAD (MAX_HEADER + 128)

因为skb本身不会存储报文数据,因此

  • skb->head_len = 0。head_len指skb本身的data存储的长度 = skb->tail - skb->head。

  • skb->len = frags len。len为总的报文长度。

  • skb->data_len = frags len。data_len为frags中存储的数据长度,不包括skb的data存储的数据长度。

  • 将报文送给内核

调用napi_gro_frags,将报文交给协议栈处理

//skb就在napi中临时存储:napi->skb
gro_result_t napi_gro_frags(struct napi_struct *napi)

ixgbe(82599)实现

  • 填充rxq的描述符

      描述符中填充的不是skb的data,而是单独找linux内核申请的物理页。调用dev_alloc_pages申请一个page,正常情况下为一个内核页大小,然后调用dma_map_page_attrs和dma_sync_single_range_for_device,mapping成dma地址

  • 接收收包

      收到一个报文,会申请一个skb。这里调用了napi_build_skb函数。该函数只申请skb数据结构,不需要申请data缓存,skb的data使用调用者传递的buff,这里传递了前面申请的page,然后组装成一个skb。这样避免了像mlx4那样,浪费第一个skb的data不用。

     //* @data: data buffer provided by caller
     //* @frag_size: size of data
    struct sk_buff *napi_build_skb(void *data, unsigned int frag_size)

      如果一个大包分多个描述符上送,则将第二个及后面的报文放在frags[MAX_SKB_FRAGS]中。第一个报文直接放在skb的data中。

注:ixgbe有个legacy的驱动,使用的是和mlx4一样的方式,即浪费第一个skb的data不用。

  • 将报文送给内核

      调用napi_gro_receive,将报文交给协议栈处理

mlx5(ConnectX-4/5/6)实现

  • 填充rxq的描述符

使用page来填充描述符,为了加速page的申请,使用了page_pool,每个rq初始化一个page_pool,page大小为一个内核页,需要page时,从page_pool中申请page。 (mlx5的这部分代码很复杂)

  • 接收收包

调用了napi_build_skb函数,只申请skb,skb->data使用自己的buff,不浪费第一个skb的data。使用frags[MAX_SKB_FRAGS]存放一个报文的多个分片。

  • 将报文送给内核

调用napi_gro_receive,将报文交给协议栈处理

ice(E810)实现

  • 填充rxq的描述符

      和ixgbe的实现一样,描述符中填充的不是skb的data,而是单独找linux内核申请的物理页。调用dev_alloc_pages申请一个page,正常情况下为一个内核页大小,然后调用dma_map_page_attrs和dma_sync_single_range_for_device,mapping成dma地址。

  • 接收收包

和ixgbe实现一样,调用了napi_build_skb函数,只申请skb,skb->data使用自己的buff,不浪费第一个skb的data。使用frags[MAX_SKB_FRAGS]存放一个报文的多个分片。

  • 将报文送给内核

和ixgbe实现一样,调用napi_gro_receive,将报文交给协议栈处理。

virtio-net实现

  • 填充rxq的描述符

      申请page填充描述符,但是会将一个page切割成多个buff,写多个描述符。默认情况下,一次申请32K的页大小的page,以供切割buff。使用alloc_pages来申请page。

      会动态统计最近收到的报文的平均大小,并按照报文平均大小来在page上切割buff。但是,buff的最小长度为硬件默认的MTU大小,buff最大的大小不超过一个内核页大小。

  • 接收收包

      当报文的第一个分片的长度小于或等于128时,申请一个新的skb+data(napi_alloc_skb,data长度128),并将报文内容拷贝到skb的data中。

      如果报文长度大于128时,只申请skb,并将之前的buff做为skb的data。但是也可能申请一个新的skb+data,此时,skb的data中只拷贝14个字节的eth head,buff的剩余部分,作为skb的第一个frags存放在frags[MAX_SKB_FRAGS]中。

      一个报文的其余的分片,会继续存放在skb的frags中。如果分片个数超过了单个skb的frags的最大值,则再申请一个新的skb,新的skb的data为0,不存放数据,数据全部存放再frags。然后将新的skb串在第一个skb的frag_list中。

  • 将报文送给内核

调用napi_gro_receive,将报文交给协议栈处理

总结:virtio-net的实现最高效和全面,有几个特点:

  • 报文buff在大的page(默认32K)上切割,并根据收到的报文的长度统计值动态调节切割大小,但是最小不能少于1500字节。省内存

  • 收包支持skb的frag_list和frags[MAX_SKB_FRAGS]的组合使用。这样上送报文的分片个数,不用限制在MAX_SKB_FRAGS大小。分片个数不受限

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值