Linux内核网络源码解析1——sk_buff结构
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的传输控制块,只有在网络数据报文由本地发送或本地接收才有效,否则为NULLtstamp
,接收或发送时间戳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_frags
、frag_list
、frags
与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网络子系统中最基础的数据结构了,后面基本都会用到这个结构,因此掌握好它十分重要, 由于篇幅太长,操作函数就留到下一篇了