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博客
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 在拥有复杂功能的现代操作中,对数据包的存储要求是非常严格的。因为其中涉及数据包在不同网络层之间传递,需要灵活增减包...
    少阁主_enfj阅读 3,348评论 0 2
  • 1、关键数据结构 1.1 嵌套字缓冲区 一个数据封包就存储在这里,所有网络分层都会使用这个结构来存储其报头、有关的...
    shaarawy18阅读 3,652评论 0 5
  • 彩排完,天已黑
    刘凯书法阅读 4,199评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,467评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,041评论 0 4