【Linux】网络专题(二)——核心数据结构sk_buff

本篇介绍网络栈中使用的核心数据结构sk_buff,从而帮助我们更快更透彻地理解网络栈的实现细节。

论对于网络栈的重要性,sk_buff说第二,就没有谁敢说第一。

从传输层到链路层,它是存放数据的通用结构,为了保持高效率,数据在传递过程中尽量不发生额外的拷贝。因此,从高层到低层的时候,会不断地在数据前加头,因此每一层的协议都会调用skb_reserve,为自己的报头预留空间。至于从低层到高层,去掉低层报头的方式就是移动一下指针,指向高层头,非常简单。

sk_buff的成员和函数大致可分为以下几类:

  • 组织布局(Layout)
  • 通用数据成员(General)
  • 用于实现特定功能的数据成员(Feature-specific)
  • 管理sk_buff结构体的函数(Management functions)

我们来看以上的1、2、4点。

1. Layout成员

sk_buff是怎样组织起来的?便于追加、删除,双链表是再合适不过了。然而除此之外,sk_buff还有必要能在O(1)时间内获得双链表的头节点,所以sk_buff_headlist_head是有一点不同之处的。我们看看sk_buff的布局:

神书就是神书,图例放在这里,自己的blog立马蓬荜生辉。
struct sk_buff_head {
	/* These two members must be first. */
	struct sk_buff	*next;
	struct sk_buff	*prev;

	__u32		qlen;
	spinlock_t	lock;
};

qlen是链表中元素的数量,lock用于在可能的并发访问时保护双链表结构体。

sk_buff中的nextprev指向相邻的sk_buff。不过在有些情况下sk_buff不是用双链表而是用红黑树组织的,那么有效的域是rbnode

值得一提的是,5.10中,list域是一个list_head结构体,而非sk_buff_head,后面我们会回答这一问题。

	union {
		struct {
			/* These two members must be first. */
			struct sk_buff		*next;
			struct sk_buff		*prev;

			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;
	};

还有一些关键的数据结构:

	union {
		struct sock		*sk;
		int			ip_defrag_offset;
	};

sk指向拥有该sk_buff的套接字。ip_defrag_offset会用来处理IPv4报文分片,老古董了属于是。

再例如,

	/* 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;

我们看看插图就知道怎么一回事了。

清晰明了。

在headroom和tailroom可以插入一些数据,例如协议头等等。实际上tailend在5.10中是unsigned类型,表示相对head的偏移量。

	unsigned int		truesize;
	refcount_t		users;

truesize表示skb使用的大小,包括skb结构体以及它所指向的数据;users是一个引用计数器。

	unsigned int		len,
				data_len;
	__u16			mac_len,
				hdr_len;

len这个东西把所有数据的长度都算上了,包括分片的数据以及协议头。data_len只算了分片的数据。mac_len是链路层帧头长度。hdr_len是被拷贝的skb中可写的头部长度,skb是否是被拷贝出来的,取决于1比特的cloned字段。

2. General成员

这一部分包括了skb的通用成员,它们与协议类型或内核特性无关:

	union {
		ktime_t		tstamp;
		u64		skb_mstamp_ns; /* earliest departure time */
	};

这是时间戳标志。一般来说tstamp只有在收包的时候比较有意义。skb_mstamp_ns可以用于标记最早的发包时间,即重传计时器的起点。

			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;
			};

dev是一个很重要的数据结构,后面会讲,它用来表示从哪个设备收到报文,或将把报文发到哪个设备。有时有些协议用不着dev,其实应该把它置空,但实际上却利用这8字节存放了一些别的信息,所以就有了dev_scratch

	/*
	 * 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);

这是skb能被各层共用的精髓。它的大小是48,这其实是TCP的控制块的大小。

struct tcp_skb_cb {
	__u32		seq;		/* Starting sequence number	*/
	__u32		end_seq;	/* SEQ + FIN + SYN + datalen	*/

	/* ... */
	__u8		tcp_flags;	/* TCP header flags. (tcp[13])	*/

	/* ... */
};

要获得这一字段,使用:

#define TCP_SKB_CB(__skb)	((struct tcp_skb_cb *)&((__skb)->cb[0]))

还有一个字段表示数据包类型:

	__u8			pkt_type:3;

它的类型是由目标MAC地址决定的,比如:

#define PACKET_HOST		0		/* To us		*/
#define PACKET_BROADCAST	1		/* To all		*/
#define PACKET_MULTICAST	2		/* To group		*/
#define PACKET_OTHERHOST	3		/* To someone else 	*/
#define PACKET_OUTGOING		4		/* Outgoing of any type */
#define PACKET_LOOPBACK		5		/* MC/BRD frame looped back */
#define PACKET_USER		6		/* To user space	*/
#define PACKET_KERNEL		7		/* To kernel space	*/
	__be16			protocol;

该字段标识了L2上层的协议类型,种类可多了,这里只列一些家喻户晓的:

/*
 *	These are the defined Ethernet Protocol ID's.
 */


#define ETH_P_IP	0x0800		/* Internet Protocol packet	*/
#define ETH_P_X25	0x0805		/* CCITT X.25			*/
#define ETH_P_ARP	0x0806		/* Address Resolution packet	*/

#define ETH_P_IPV6	0x86DD		/* IPv6 over bluebook		*/

有了它,就能在收包的时候选择合适的处理函数。

	__u16			transport_header;
	__u16			network_header;
	__u16			mac_header;

这些字段标识了各层头部相对于head的偏移量。

3. 管理函数

大多数管理函数都成对出现:包括do_sth__do_sth。一般前者只是个wrapper,用于进行安全检查或函数特化等,后者执行真正的功能。

3.1. 分配和释放内存

static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)

用它就能获得一个sk_buff,附赠一片size大小(经过对齐)的数据缓冲区。核心代码如下。

	skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
	/* ... */
	size = SKB_DATA_ALIGN(size);
	size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
	data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
	/* kmalloc(size) might give us more room than requested.
	 * Put skb_shared_info exactly at the end of allocated zone,
	 * to allow max possible filling before reallocation.
	 */
	size = SKB_WITH_OVERHEAD(ksize(data));
	/* ... */
	skb->head = data;
	skb->data = data;
	skb_reset_tail_pointer(skb);
	skb->end = skb->tail + size;

其实skb->end指向的是一个skb_shared_info结构体。headdatatail初始化的时候都是重合的,但end可能会偏后一点点,留出padding(其实也是一片tailroom),从而使读取为主的skb_shared_info与前面的数据不在一个缓存行。说到这里还是非常抽象,不要紧,要是会让读者看不懂那就不叫神书了:

dev_alloc_skb只接收一个参数length,本质上还是__alloc_skb的wrapper。它在设备驱动中工作(即中断上下文),所以只能以GFP_ATOMIC标志申请内存。此外,出于一些优化的目的,它会偷偷多申请16字节空间并用skb_reserve预留出来以供他用。

3.2. 数据缓冲区指针操作

四大天王:skb_reserveskb_putskb_pushskb_pull。一张图搞懂:

形象来说,put就是在后面追加,push是从数据内部往外推,pull是往里拉。reserve就是给非数据留出空间来,skb创建出来之后就调用,可以预留用作协议头。

skb_reserve还有一种应用场景,例如以太网帧头有14字节,那么skb创建出来的时候会先把data往后挪2字节,好让IP头进行16字节对齐,如此暖心真是令人泪目(bushi

skb_reserve(skb, 2);	/* Align IP on 16 byte boundaries */

再以发送数据包为例,skb缓冲区是这样被填满的:

先预留足够的空间放头,TCP载荷可以put到剩下的空间里去。之后,不断地push,就可以将缓冲范围扩大,从而可以显式地用其它什么函数把不同的协议头写入数据缓冲。

3.3. 链表和数据操作

skb的操作有很多,我们会在后面遇到的时候再讲解。

参考资料

Understanding Linux Network Internals

发表评论

您的电子邮箱地址不会被公开。