Linux内核源码学习,套接字缓存sk_buff的结构体

前言

接下会有一系列阅读Linux内核网络的博客,主要是学习网络子系统,使用的Linux内核代码2.6.20, 主要的参考资料是樊东东老师写的《Linux内核源码剖析——TCP/IP实现》和网上的一些资料。

这一篇博客主要是解析sk_buff结构体,下一篇会解析有关它的操作函数

什么是sk_buff

sk_buff的意思是socket buffer,这是Linux网络子系统中的核心数据结构

skbuffs是Linux内核用来处理从网卡传来的网络包的缓冲

因此,在内核栈处理网络包的效率很重要: + 能很容易在头尾部添加和移除数据,因为这些缓存区需要在不同网络层次间进行传递 + 能很方便地处理可变长缓存,因为接收和发送的数据包长度不是固定的 + 每个协议都应该很方便访问他们的头部 + 在添加和移除数据时能尽量避免数据的复制

include/linux/skbuff.h(sk_buff结构定义和sk_buff宏)

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

	struct sock		*sk;
	struct skb_timeval	tstamp;
	struct net_device	*dev;
	struct net_device	*input_dev;

	union {
		struct tcphdr	*th;
		struct udphdr	*uh;
		struct icmphdr	*icmph;
		struct igmphdr	*igmph;
		struct iphdr	*ipiph;
		struct ipv6hdr	*ipv6h;
		unsigned char	*raw;
	} h;

	union {
		struct iphdr	*iph;
		struct ipv6hdr	*ipv6h;
		struct arphdr	*arph;
		unsigned char	*raw;
	} nh;

	union {
	  	unsigned char 	*raw;
	} mac;

	struct  dst_entry	*dst;
	struct	sec_path	*sp;

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

	unsigned int		len,
				data_len,
				mac_len;
	union {
		__wsum		csum;
		__u32		csum_offset;
	};
	__u32			priority;
	__u8			local_df:1,
				cloned:1,
				ip_summed:2,
				nohdr:1,
				nfctinfo:3;
	__u8			pkt_type:3,
				fclone:2,
				ipvs_property:1;
	__be16			protocol;

	void			(*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
	struct nf_conntrack	*nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	struct sk_buff		*nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
	struct nf_bridge_info	*nf_bridge;
#endif
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
	__u16			tc_index;	/* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
	__u16			tc_verd;	/* traffic control verdict */
#endif
#endif
#ifdef CONFIG_NET_DMA
	dma_cookie_t		dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
	__u32			secmark;
#endif

	__u32			mark;

	/* These elements must be at the end, see alloc_skb() for details.  */
	unsigned int		truesize;
	atomic_t		users;
	unsigned char		*head,
				*data,
				*tail,
				*end;
};

看了那么长的结构体,估计都看晕了,下面来看张图:

重要的成员都包含在图内了,接下来分别解析这些成员。

sk_buff组织结构

	struct sk_buff		*next;
	struct sk_buff		*prev

这两个成员很好理解,就是sk_buff是以双向链表来组织的

为了快速地从整个链表的头部找到每个sk_buff,在第一个sk_buff前面会插入一个辅助的头结点:

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链表并发操作的自旋锁

传输相关

	struct sock		*sk;
	struct skb_timeval	tstamp;
	struct net_device	*dev;
	struct net_device	*input_dev;
  • sk指向拥有该sk_buff的传输控制块,只有在网络数据报文由本地发送或本地接收才有效,否则为NULL
  • tstamp,接收或发送时间戳
  • dev指向网络设备,作用与该sk_buff是发送包还是接收包有关。在初始化网络设备驱动,分配接收缓存队列时,将该指针指向收到数据包的网络设备
  • input_dev指向接收报文的原始网络设备,如果包是本地生成的,则该值为NLL,主要用于流量控制

协议头部指针

依次是传输层、网络层、mac层的头部指针,用union表示是互斥的,只能表示其中的一种

	union {
		struct tcphdr	*th;
		struct udphdr	*uh;
		struct icmphdr	*icmph;
		struct igmphdr	*igmph;
		struct iphdr	*ipiph;
		struct ipv6hdr	*ipv6h;
		unsigned char	*raw;
	} h;

	union {
		struct iphdr	*iph;
		struct ipv6hdr	*ipv6h;
		struct arphdr	*arph;
		unsigned char	*raw;
	} nh;

	union {
	  	unsigned char 	*raw;
	} mac

路由相关

	struct  dst_entry	*dst;
	struct	sec_path	*sp
  • dst_entry指向需要转发包的函数,这个指针在sk_buff在IP层传输前必须是合法的
  • sec_path是一个可选的有关网络安全的成员

信息控制块

	char	cb[48]

每层协议私有的信息存储空间,由每一层自己维护和使用,并只在本层有效

长度相关

	unsigned int	len,
			data_len,
			mac_len;
	union {
		__wsum	csum;
		__u32	csum_offset;
	};
	__u32		priority;
	__u8		local_df:1,
			cloned:1,
			ip_summed:2,
			nohdr:1,
			nfctinfo:3;
	__u8		pkt_type:3,
			fclone:2,
			ipvs_property:1;
	__be16		protocol;

	void		(*destructor)(struct sk_buff *skb);
  • 前面几个长度在后面几个区域的操作函数会解释
  • 在变量名后面加一个冒号?我也是第一次见到这种语法,这叫位域,具体可以看这个解释: http://c.biancheng.net/view/2037.html

引用计数

	atomic_t		users;
  • 记录有多少引用指向这个sk_buff,该计数器只保护sk_buff描述符,要区别于skb_shared_info结构的dataref成员
  • 通常使用skb_get()和kfree_skb()操作引用计数

数据区域指针

	unsigned char	*head,
			*data,
			*tail,
			*end;

看个图更清楚:

之后介绍的几个函数主要就是移动这几个指针,改变这几个空间的大小

skb_shared_info

在sk_buff的数据缓冲区的末尾,即end指针所指向的地址起紧跟着有一个skb_shared_info结构,保存了数据块的附加信息

/* This data is invariant across clones and lives at
 * the end of the header data, ie. at skb->end.
 */
struct skb_shared_info {
	atomic_t	dataref;
	unsigned short	nr_frags;
	unsigned short	gso_size;
	/* Warning: this field is not always filled in (UFO)! */
	unsigned short	gso_segs;
	unsigned short  gso_type;
	__be32          ip6_frag_id;
	struct sk_buff	*frag_list;
	skb_frag_t	frags[MAX_SKB_FRAGS];
};
  • dataref:当一个数据缓存区被多个skb_buff的描述符引用时,就会设置相应的计数,比如克隆一个sk_buff 之前skb_shared_info结构体有几个成员和聚合分散IO有关:
  • nr_fragsfrag_listfrags与IP分片的存储有关
    • 一般来说,数据都存储到线性区域中,即sk_buff几个区域指针指向的区域,但当为了支持聚合分散IO(后面会解释),数据需要存储在支持聚合分散IO的区域中

零拷贝技术

Linux的sendfile系统调用就用到了零拷贝技术,零拷贝可以减少数据复制和减少上下文切换的次数

比较read+write和open+sendfile的数据复制: + read+write 1. 从硬件的DMA缓存复制到内核缓存 2. 从内核缓存复制到用户缓存 3. 从用户缓存复制到内核缓存 4. 从内核缓存复制到DMA缓存 + open+sendfile 1. 数据通过DMA被复制到内核缓存区 2. 由于数据并未被复制到套接口关联的缓存区内,而只是记录数据位置和长度的数据缓存区被加入到sk_buff中,因此DMA模块直接将数据从内核缓存区传递给协议模块

对聚合分散IO数据的支持

什么是聚合分散IO? + 网络中创建一个发送报文的过程包括组合多个片,报文数据必须从用户空间复制到内核空间,同时加上网络协议栈各层的首部,这需要大量的数据拷贝 + 如果发送报文的网络接口支持聚合分散IO,报文就无需组装成一个单块,可避免大量拷贝 + 局和分散IO从用户空间启动零拷贝网络发送,首先要检查网络设备是否设置了NETIF_F_SG,没设置就只能线性化处理,设置了接下来就检查nr_frags的值,该字段确定了分段数,这些分散的片段以关联的方式存储在frags数组中

/* To allow 64K frame to be packed as single skb without frag_list */
#define MAX_SKB_FRAGS (65536/PAGE_SIZE + 2)

typedef struct skb_frag_struct skb_frag_t;

struct skb_frag_struct {
	struct page *page;
	__u16 page_offset;
	__u16 size;
};
  • page:指向文件系统缓存页的指针
  • page_offset:数据起始地址在文件系统缓存页中的偏移
  • size:数据在文件系统缓存页中使用的长度

总结

sk_buff是Linux网络子系统中最基础的数据结构了,后面基本都会用到这个结构,因此掌握好它十分重要, 由于篇幅太长,操作函数就留到下一篇了