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大小。分片个数不受限