Linux内核协议栈-sk_buff结构详解

为什么需要sk_buff:

​ 网络协议栈是一个层次架构的软件结构,层与层之间通过预订的接口传递报文。网络报文中包含了在协议各层使用到的各种信息。由于网络报文之间的大小不是固定的,因此采用合适的数据结构来存储这些网络报文就显得非常重要。内核层和用户层在网络方面的差别很大,在内核的网络层中 sk_buff结构占有重要的地位,几乎所有的处理均与此结构有关系。

sk_buff结构源码及注解:

struct sk_buff {
	union {
		struct {
			/* These two members must be first. */
			struct sk_buff		*next; // sk_buff链表中的下一个sock缓冲区
			struct sk_buff		*prev; // sk_buff链表中的前一个sock缓冲区
			//上述两个人变量将sk_buff链接到一个双向链表中
			union {
				struct net_device	*dev;  //接收到此网络报文的网络设备
				/* Some protocols might use this space to store information,
				 * while device pointer would be NULL.
				 * UDP receive path is one user.
				 */
				unsigned long		dev_scratch;
			};
		};
		struct rb_node		rbnode; /* used in netem, ip4 defrag, and tcp stack */
		struct list_head	list;   //内核链表结构,用于快速定位链表头 sk_buff_head
	};

	union {
		struct sock		*sk; // 网络报文所属的sock结构,此值仅在本机发出的报文中有效,从网络收到的报文此值为空。
		int			ip_defrag_offset; // 用于分片管理中
	};

	union {
		ktime_t		tstamp;	// 收到此报文的时间戳
		u64		skb_mstamp_ns; /* earliest departure time */ //最早出发时间
	};
	/*
	 * 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); //用于控制此缓冲区,每层都可以使用此指针,将私有数据放置于此。

	union {
		struct {
			unsigned long	_skb_refdst;
			void		(*destructor)(struct sk_buff *skb);
		};
		struct list_head	tcp_tsorted_anchor;
	};

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	unsigned long		 _nfct;
#endif
	unsigned int		len, 
				data_len; // len表示有效数据长度、data_len表示数据长度
	__u16			mac_len,  //连接层头部长度、对于以太网 指mac地址长度,为6
				hdr_len; //skb的可写数据长度

	/* Following fields are _not_ copied in __copy_skb_header()
	 * Note that queue_mapping is here mostly to fill a hole.
	 */
	__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)

	/* private: */
	__u8			__cloned_offset[0];
	/* public: */
	__u8			cloned:1,
				nohdr:1,
				fclone:2,
				peeked:1,
				head_frag:1,
				pfmemalloc:1;
#ifdef CONFIG_SKB_EXTENSIONS
	__u8			active_extensions;
#endif
	/* 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)

	/* private: */
	__u8			__pkt_type_offset[0];
	/* public: */
	__u8			pkt_type:3; //包类别
	__u8			ignore_df:1;
	__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;

#ifdef __BIG_ENDIAN_BITFIELD
#define PKT_VLAN_PRESENT_BIT	7
#else
#define PKT_VLAN_PRESENT_BIT	0
#endif
#define PKT_VLAN_PRESENT_OFFSET()	offsetof(struct sk_buff, __pkt_vlan_present_offset)
	/* private: */
	__u8			__pkt_vlan_present_offset[0];
	/* public: */
	__u8			vlan_present:1;
	__u8			csum_complete_sw:1;
	__u8			csum_level:2;
	__u8			csum_not_inet:1;
	__u8			dst_pending_confirm: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;
	__u8			offload_l3_fwd_mark:1;
#endif
#ifdef CONFIG_NET_CLS_ACT
	__u8			tc_skip_classify:1;
	__u8			tc_at_ingress:1;
#endif
#ifdef CONFIG_NET_REDIRECT
	__u8			redirected:1;
	__u8			from_ingress:1;
#endif
#ifdef CONFIG_TLS_DEVICE
	__u8			decrypted:1;
#endif

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

	union {
		__wsum		csum; 	//检验和
		struct {
			__u16	csum_start; //当开始计算检验和时从skb->head的偏移
			__u16	csum_offset; // 从csum_start开始的偏移
		};
	};
	__u32			priority; // 包队列的优先级
	int			skb_iif;
	__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;  // 报文缓冲区的大小
	refcount_t		users;

#ifdef CONFIG_SKB_EXTENSIONS
	/* only useable after checking ->active_extensions != 0 */
	struct skb_ext		*extensions;
#endif
};

data_len记录的是在frags和frag_list里面存储的报文的长度
len记录网络报文的总长度
truesize是head所指的存储区的大小

skb_shared_info结构和skb_shinfo函数

数据缓冲区尾端有个名为skb_shared_info的数据结构,用以保持此数据区块的附加信息。此数据结构紧接在标记数据尾端的end指针之后。

skb_shared_info结构
struct skb_shared_info {
    __u8        __unused;
    __u8        meta_len;
    __u8        nr_frags;                             // 用于处理IP片段
    __u8        tx_flags;                             // 用于处理IP片段
    unsigned short  gso_size;
    unsigned short  gso_segs;
    struct sk_buff  *frag_list;                      // 用于处理IP片段
    struct skb_shared_hwtstamps hwtstamps;
    unsigned int    gso_type;
    u32     tskey;

    atomic_t    dataref;                            // 数据块的“用户”数目

    void *      destructor_arg;
    skb_frag_t  frags[MAX_SKB_FRAGS];
};

sk_buff结构中没有指向skb_shared_info数据结构的字段。为了访问该结构体,函数必须使用返回end指针的skb_shinfo宏:

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

通过这个指针去访问里面的成员

u32 nr_frags = skb_shinfo(skb)->nr_frags + 1

sk_buff结构框图

sk_buff包括如下几个部分:
(1) struct sk_buff–用于维护socket buffer状态和描述信息
(2) 网络报文缓冲区-独立于sk_buff结构体的数据缓冲区,用来存放报文分组,使各层协议的header存储在连续的空间中,以方便协议栈对其操作
(3) skb_shared_info 作为网络报文缓冲区的补充,用于存储ip分片,其中sk_buff *frag_list是一系列子skbuff链表,而frag[]是由一组单独的page组成的数据缓冲区
skb buff结构图如下:
在这里插入图片描述

网络报文存储空间是在应用层发送网络数据或者网络设备收到网络数据时动态分配的,分配成功之后,将接收或者发送的网络数据填充到这个存储空间中去。将网络数据填充到存储空间时,在存储空间的头部预留了一定数量的空隙,然后从此偏移量开始将网络报文复制到存储空间中。skb_shared_info数据结构可以包含一个sk_buff结构列表(链接到一个frag_list字段)。

sk_buff环状链

结构sk_buff 以 sk_buff_head构成一个环状的链,如下图所示, next变量指向下一个sk_buff结构, prev变量指向前一个sk_buff结构。内核程序通过访问其中的各个单元来遍历整个协议栈中的网络数据。

在这里插入图片描述

sk_buff_head结构
struct sk_buff_head {
	/* These two members must be first. */
	struct sk_buff	*next;
	struct sk_buff	*prev;
	__u32		qlen;
	spinlock_t	lock;
};

sk_buff操作API

API说明
dev_alloc_skb在中断模式下的缓冲区分配,即原子操作的alloc_skb
kfree_skb释放缓冲区
dev_kfree_skb释放缓冲区,调用kfree_skb,当skb->users减为0才释放内存
skb_reserve在缓冲区的头部预留一些空间,允许插入一个报头。此函数只是移动了data和tail指针
skb_put将 tail 指针向数据区的末尾移动,增加了 len 字段的长度
skb_push将 data 指针向数据区的前端移动,增加 了len 字段的长度。
skb_pull将 data 指针向数据区的末尾移动,减少了len 字段的长度。
skb_trim将网络报文的长度缩减到len,丢弃了网络报文尾部的填充值
skb_clone克隆sk_buff数据结构,双方的clone位置1,user置1,数据缓冲区内的dataref递增
skb_share_check检查引用计数skb->users,并且当users字段说明该缓冲区是共享时可以克隆该缓冲区
skb_copy拷贝sk_buff以及数据缓冲区
pskb_copy只拷贝sk_buff
alloc_skb用于分配sk_buff数据结构以及数据缓冲区
skb_queue_head_init用一个元素为空的队列初始化sk_buff_head
skb_queue_head把一个缓冲区分别添加到队列的头
skb_queue_tail把一个缓冲区分别添加到队列的尾
skb_dequeue_head把一个元素分别从队列的头去掉
skb_dequeue_tail把一个元素分别从队列的尾去掉
skb_queue_purge把队列变为空队列
skb_queue_walk依次循环运行队列里的每个元素
skb_clone、 pskb_copy和skb_copy区别

当同一个缓冲区需要不同的程序修改sk_buff描述符的内容,为了提高效率,内核不需要完全拷贝sk_buff结构和相关联的数据缓冲区,内核仅克隆原始值,也就是只拷贝sk_buff结构体,然后使用引用计数,以免过早释放共享的数据块。缓冲区的克隆由skb_clone函数实现,如下图:

在这里插入图片描述

struct sk_buff *skb_clone(struct sk_buff *skb, int gfp_mask)

从控制结构skb中clone出一个新的控制结构,它们都指向同一个网络报文。clone成功之后,将新的控制结构和原来的控制结构的is_clone,cloned两个标记都置为1。同时还增加网络报文的引用计数(这个引用计数存放在存储空间的结束地址的内存中,由函数atomic_t *skb_datarefp(struct sk_buff *skb)访问,引用计数记录了这个存储空间有多少个控制结构)。由于存在多个控制结构指向同一个存储空间的情况,所以在修改存储空间里面的内容时,先要确定这个存储空间的引用计数为1,或者用下面的拷贝函数复制一个新的存储空间,然后才可以修改它里面的内容。

当一个缓冲区被克隆时,数据区块的内容不能修改。当函数不仅需要修改sk_buff结构的内容,而且也需要修改数据时,就必须连数据区块一起克隆。这样有两种选择:

1.修改skb—>head和skb->end之间的数据内容,可以使用pskb_copy函数
2.除上述1之外,片段数据区块(skb_shared_info)的内容也要修改时,必须使用skb_copy函数

pskb_copy实现如下图

在这里插入图片描述

skb_copy实现如下图:

struct sk_buff *skb_copy(struct sk_buff *skb, int gfp_mask)

复制控制结构skb和它所指的存储空间的内容。复制成功之后,新的控制结构和存储空间与原来的控制结构和存储空间相对独立。所以新的控制结构里的is_clone,cloned两个标记都是0,而且新的存储空间的引用计数是1。
在这里插入图片描述

分配skb函数alloc_skb和dev_alloc_skb

alloc_skb函数原型

struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)

分配大小为size的存储空间存放网络报文,同时分配它的控制结构。size的值是16字节对齐的,gfp_mask是内存分配的优先级。常见的内存分配优先级有GFP_ATOMIC,代表分配过程不能被中断,一般用于中断上下文中分配内存;GFP_KERNEL,代表分配过程可以被中断,相应的分配请求被放到等待队列中。分配成功之后,因为还没有存放具体的网络报文,所以sk_buff的data,tail指针都指向存储空间的起始地址,len的大小为0,而且is_clone和cloned两个标记的值都是0。

定义在net/core/skbuff.c中的alloc_skb是分配缓冲区的主要函数。数据缓冲区和报头(sk_buff数据结构)是两种不同的实例
建立一个缓冲区有两次的内存分配(一个是分配缓冲区,另一个是分配sk_buff结构)。

dev_alloc_skb是由设备驱动程序使用的缓冲区分配函数,这个函数是在中断中执行。这个函数是alloc_skb函数的包装,为了做优化,在申请的大小之上再加需要的字节。由于中断中处理函数调用,会要求原子操作

__alloc_skb            
    -> skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);   // 从一个缓存中取得一个sk_buff数据结构
    -> size = SKB_DATA_ALIGN(size);                                       // 强制对齐     
    -> size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
    -> data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);

在这里插入图片描述

释放skb:kfree_skb和dev_kfree_skb

两个函数会释放一个缓冲区,使其返回缓冲池(缓存)。kfree_skb是直接由dev_kfree_skb调用并启动的。
只有当skb->users计数器为1时(该缓冲区已无任何用户时),这个基本函数才会释放一个缓冲区。否则,只是递减该计数器。
在sk_buff底端的skb_shared_info数据结构可以持有一些指向其他内存片段的指针。kfree_skb也会释放这些片段所持有的内存。

void kfree_skb(struct sk_buff *skb)

释放控制结构skb和它所指的存储空间。由于一个存储空间可以有多个控制结构,所以只有在存储空间的引用计数为1的情况下才释放存储空间,一般情况下,只释放控制结构skb。

skb_reserve
void skb_reserve(struct sk_buff *skb, unsigned int len)

将data指针和tail指针同时下移。这个操作在存储空间的头部预留len长度的空隙。
在这里插入图片描述

skb_put
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)

将tail指针下移,并增加skb的len值。data和tail之间的空间就是可以存放网络报文的空间。这个操作增加了可以存储网络报文的空间,但是增加不能使tail的值大于end的值,skb的len值大于truesize的值。
在这里插入图片描述

skb_push
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)

将data指针上移,并增加skb的len值。这个操作在存储空间的头部增加了一段可以存储网络报文的空间
在这里插入图片描述

skb_pull
unsigned char * skb_pull(struct sk_buff *skb, unsigned int len)

将data指针下移,并减小skb的len值。这个操作使data指针指向下一层网络报文的头部。
在这里插入图片描述

skb_trim
void skb_trim(struct sk_buff *skb, unsigned int len)

将网络报文的长度缩减到len,丢弃了网络报文尾部的填充值。

参考文献:Linux网络编程
《Understanding Linux Network Internals》

  • 6
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值