Linux内核中sk_buff结构详解

一、sk_buff结构体

sk_buff是Linux网络中最核心的结构体,它用来管理和控制接收或发送数据包的信息。各层协议都依赖于sk_buff而存在。内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来操作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L4到L2,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头。这样做是为了提高CPU的工作效率。

1.1 sk_buff在内核中的结构

skb_buff结构如下所示:

/*  include/linux/skbuff.h */
struct sk_buff {
    union {
        struct {
            /* These two members must be first. 
这两个域是用来连接相关的skb的(如果有分片的话,可以通过它们将分片链接到一起),sk_buff是双链表结构。
              */
            struct sk_buff      *next;  /*链表中的下一个skb*/
            struct sk_buff      *prev; /*链表中的上一个skb*/

            union {
                ktime_t     tstamp; /*记录接受或者传输报文的时间戳*/
                struct skb_mstamp skb_mstamp;
            };
        };
        struct rb_node      rbnode; /* 红黑树,used in netem, ip4 defrag, and tcp stack */
    };

    union {
        struct sock     *sk; /*指向报文所属的套接字指针*/
        int         ip_defrag_offset;
    };

    struct net_device   *dev; /*记录接受或发送报文的网络设备*/

    /*
     * This is the control buffer. It is free to use for every
     * layer. Please put your private variables there. If you
     * want to keep them across layers you have to do a skb_clone()
     * first. This is owned by whoever has the skb queued ATM.
     */
    char            cb[48] __aligned(8); /*保存与协议相关的控制信息,每个协议可能独立使用这些信息*/

    unsigned long       _skb_refdst; /*主要用于路由子系统,保存路由相关的东西*/
    void            (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_XFRM
    struct  sec_path    *sp;
#endif
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
    struct nf_conntrack *nfct;
#endif
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
    struct nf_bridge_info   *nf_bridge;
#endif
    unsigned int        len,   
/*整个数据区域的长度,
这里的len = length(实际线性数据,不包括头空间和尾空间) + length(非线性数据)
len = (tail - data) + data_len
这个len中数据区长度是个有效长度,因为不删除协议头,
所以只计算有效协议头和包内容。如:当在L3时,不会计算L2的协议头长度。*/
                data_len; /*非线性数据,length(实际线性数据 = skb->len - skb->data_len)*/
    __u16           mac_len, /*mac层报头的长度*/
                hdr_len; /*用于clone时,表示clone的skb的头长度*/

    /* Following fields are _not_ copied in __copy_skb_header()
     * Note that queue_mapping is here mostly to fill a hole.
     */
    kmemcheck_bitfield_begin(flags1);
    __u16           queue_mapping;

/* if you move cloned around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define CLONED_MASK (1 << 7)
#else
#define CLONED_MASK 1
#endif
#define CLONED_OFFSET()     offsetof(struct sk_buff, __cloned_offset)

    __u8            __cloned_offset[0];
    __u8            cloned:1,
                nohdr:1,
                fclone:2,
                peeked:1,
                head_frag:1,
                xmit_more:1,
                pfmemalloc:1;
    kmemcheck_bitfield_end(flags1);

    /* fields enclosed in headers_start/headers_end are copied
     * using a single memcpy() in __copy_skb_header()
     */
    /* private: */
    __u32           headers_start[0];
    /* public: */

/* if you move pkt_type around you also must adapt those constants */
#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_TYPE_MAX    (7 << 5)
#else
#define PKT_TYPE_MAX    7
#endif
#define PKT_TYPE_OFFSET()   offsetof(struct sk_buff, __pkt_type_offset)

    __u8            __pkt_type_offset[0];
    __u8            pkt_type:3; /*标记帧的类型*/
    __u8            ignore_df:1;
    __u8            nfctinfo:3;
    __u8            nf_trace:1;

    __u8            ip_summed:2;
    __u8            ooo_okay:1;
    __u8            l4_hash:1;
    __u8            sw_hash:1;
    __u8            wifi_acked_valid:1;
    __u8            wifi_acked:1;
    __u8            no_fcs:1;

    /* Indicates the inner headers are valid in the skbuff. */
    __u8            encapsulation:1;
    __u8            encap_hdr_csum:1;
    __u8            csum_valid:1;
    __u8            csum_complete_sw:1;
    __u8            csum_level:2;
    __u8            csum_bad:1;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
    __u8            ndisc_nodetype:2;
#endif
    __u8            ipvs_property:1;

    __u8            inner_protocol_type:1;
    __u8            remcsum_offload:1;
#ifdef CONFIG_NET_SWITCHDEV
    __u8            offload_fwd_mark:1;
#endif
    /* 2, 4 or 5 bit hole */

#ifdef CONFIG_NET_SCHED
    __u16           tc_index;   /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
    __u16           tc_verd;    /* traffic control verdict */
#endif
#endif

    union {
        __wsum      csum;
        struct {
            __u16   csum_start;
            __u16   csum_offset;
        };
    };
    __u32           priority; /*优先级,主要用于QOS*/
    int         skb_iif; /*接收设备的index*/
    __u32           hash;
    __be16          vlan_proto;
    __u16           vlan_tci;
#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
    union {
        unsigned int    napi_id;
        unsigned int    sender_cpu;
    };
#endif
#ifdef CONFIG_NETWORK_SECMARK
    __u32       secmark;
#endif

    union {
        __u32       mark;
        __u32       reserved_tailroom;
    };

    union {
        __be16      inner_protocol;
        __u8        inner_ipproto;
    };

    __u16           inner_transport_header; 
    __u16           inner_network_header; 
    __u16           inner_mac_header; 

    __be16          protocol; /*协议类型*/
    __u16           transport_header;
    __u16           network_header;
    __u16           mac_header;

    /* private: */
    __u32           headers_end[0];
    /* public: */

    /* These elements must be at the end, see alloc_skb() for details.  */
    sk_buff_data_t      tail; /*指向数据区中实际数据结束的位置*/
    sk_buff_data_t      end; /*指向数据区中结束的位置(非实际数据区域结束位置)*/
     
    unsigned char       *head, /* 指向数据区中开始的位置(非实际数据区域开始位置)*/
                *data; /*指向数据区中实际数据开始的位置*/
    unsigned int        truesize; 
    atomic_t        users; /*缓冲区总长度*/
};

1.2 重要的长度len的解析

这里要声明两个概念的区别,后续直接用这两个概念,注意区分:
(1)线性数据:head - end。
(2)实际线性数据:data - tail,不包含线性数据中的头空间和尾空间。
skb->data_len: skb中的分片数据(非线性数据)的长度。
skb->len: skb中的数据块的总长度,数据块包括实际线性数据和非线性数据,非线性数据为data_len,所以skb->len= (data - tail) + data_len。
skb->truesize: skb的总长度,包括sk_buff结构和数据部分,skb=sk_buff控制信息 + 线性数据(包括头空间和尾空间) + skb_shared_info控制信息 + 非线性数据,所以skb->truesize = sizeof(struct sk_buff) + (head - end) + sizeof(struct skb_shared_info) + data_len。

二、sk_buff数据区

sk_buff结构体中的都是sk_buff的控制信息,是网络数据包的一些配置,真正储存数据的是sk_buff结构体中几个指针指向的数据区中,线性数据区的大小 = (skb->end - skb->head),对于每个数据包来说这个大小都是固定不变的,在传输过程中skb->end和skb->head所指向的地址都是不变的,这里要注意这个地址不是本机的地址,如果是本机的地址那么数据包传到其他主机上这个地址就是无效的,所以这个地址是这个skb缓冲区的相对地址。

线性数据区是用来存放各层协议头部和应用层发下来的数据。各层协议头部相关信息放在线性数据区中。实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。
用一张图来表示sk_buff和数据区的关系:


sk_buff与数据区的关系

2.1 线性数据区

这一节介绍先行数据区在sk_buff创建过程中的变化,图中暂时省略了非线性数据区:

  1. sk_buff结构数据区刚被申请好,此时 head 指针、data 指针、tail 指针都是指向同一个地方。记住前面讲过的:head 指针和 end 指针指向的位置一直都不变,而对于数据的变化和协议信息的添加都是通过 data 指针和 tail 指针的改变来表现的。


    初始化sk_buff
  2. 开始准备存储应用层下发过来的数据,通过调用函数 skb_reserve(m) 来使 data 指针和 tail 指针同时向下移动,空出一部分空间来为后期添加协议信息。m一般为最大协议头长度,内核中定义。


    初始定位skb_reserve(m)
  3. 开始存储数据了,通过调用函数 skb_put() 来使 tail 指针向下移动空出空间来添加数据,此时 skb->data 和 skb->tail 之间存放的都是数据信息,无协议信息。


    储存应用层数据skb_put()
  4. 这时就开始调用函数 skb_push() 来使 data 指针向上移动,空出空间来添加各层协议信息,添加协议信息也是用skb_put()。直到最后到达二层,添加完帧头然后就开始发包了。


    添加协议头

2.2 非线性数据区

2.1中所讲的都是线性数据区中的相关的配置,当线性数据区不够用的时候就会启用非线性数据区作为数据区域的扩展,skb中用skb_shared_info分片结构体来配置非线性数据。

skb_shared_info结构体是和skb中的线性数据区一体的,所以在skb的各种操作时都会把这两个结构看作是一个结构来操作。如:

  1. 当sk_buff结构的线性数据区申请和释放空间时,分片结构会跟着数据区一起分配和释放。
  2. 克隆skb时,sk_buff的线性数据区和分片结构都由分片结构中的dataref成员字段来标识是否被引用。


    sk_buff与数据区的关系

    从上图中可以看出来非线性数据区接到skb->end的位置后,skb->end的下一个字节就作为非线性数据区的开始。end指针的下个字节可以作为分片结构的开始,获取end指针的位置要强行转成分片结构,内核中有定义好的宏:

#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))

skb_shared_info结构:

/*  include/linux/skbuff.h */
struct skb_shared_info {
    unsigned char   nr_frags; /*表示有多少分片结构*/
    __u8        tx_flags;
    unsigned short  gso_size;
    /* Warning: this field is not always filled in (UFO)! */
    unsigned short  gso_segs;
    unsigned short  gso_type;
    struct sk_buff  *frag_list; /*一种类型的分配数据*/
    struct skb_shared_hwtstamps hwtstamps;
    u32     tskey;
    __be32          ip6_frag_id;

    /*
     * Warning : all fields before dataref are cleared in __alloc_skb()
     */
    atomic_t    dataref; /*用于引用计数,克隆一个skb结构体时会增加一个引用计数*/

    /* 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];  /*保存分页数据,skb->data_len = 所有数组数据长度之和*/
};

非线性数据区有两种不同的构成数据的方式
(1)用数组存储的分片数据区,采用是是结构体中的frags[MAX_SKB_FRAGS]
对于frags[]一般用在当数据比较多,在线性数据区装不下的时候,skb_frag_t中是一页一页的数据,skb_frag_struct结构体如下:

/*  include/linux/skbuff.h */
typedef struct skb_frag_struct skb_frag_t;

struct skb_frag_struct {
    struct {
        struct page *p; /*指向分片数据区的指针,类似于sk_buff中的data指针*/
    } page;
#if (BITS_PER_LONG > 32) || (PAGE_SIZE >= 65536)
    __u32 page_offset;
    __u32 size;
#else
    __u16 page_offset; /*偏移量,表示相对开始位置的页偏移量*/
    __u16 size; /*page中的数据长度*/
#endif
};

下图显示了frags是怎么分配分片数据的:


数组存储分片指针类型

(2)frag_list指针来指向的分片数据:


frag_list指针来指向的分片数据类型

参考:

  1. linux 内核网络 sk_buff 之数据结构剖析Ⅰ_老王不让用的博客-CSDN博客
  2. sk_buff 结构体 以及 完全解释_gsls181711的专栏-CSDN博客

你可能感兴趣的:(Linux内核中sk_buff结构详解)