在这几天的工做中老是或多或少的接触到了sk_buff结构体。后来我以为这样时不时地学点sk_buff结构还不如干脆花段时间来研究下这个重要的结构体。因此我就学习了《深刻理解linux网络技术内幕》有关sk_buff结构的介绍,这系列博文原本是我根据《深刻理解linux网络技术内幕》学习整理而来的,能够算做是笔记吧。后来在看sk_buff克隆和拷贝时,又看了下《linux内核源码剖析:TCP/IP实现》。如今这博文是通过修改的,因此也加进了在《llinux内核源码剖析:TCP/IP实现》学习到的内容。你们也能够看看原文对sk_buff结构体的一些讲解,原文分别在第二章关键数据结构:套接字缓冲区:sk_buff结构和第三章:套接字缓存。若是没有电子书的,能够私信我留下邮箱地址。node
我要先感谢下这两本书的做者以及译者,《深刻理解linux网络技术内幕》是linux网络中的一本经典之做,对于学习linux网络来讲是很是有用的,这是本全面宏观的介绍linux网络的书;《linux内核源码剖析:TCP/IP实现》我开始看的是电子书,这本书吸引个人地方是讲解的很是详细,并且是很是简洁,尤为是对各个知识点的讲解很是透彻。linux
下面开始正式的讲解sk_buff结构内容,至于sk_buff结构体的重要性以及历史背景之类的我就不过多废话了。数组
第1、内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是经过增长协议头和移动指针来操做的。若是是从L4传输到L2,则是经过往sk_buff结构体中增长该层协议头来操做;若是是从L4到L2,则是经过移动sk_buff结构体中的data指针来实现,不会删除各层协议头。这样作是为了提升CPU的工做效率。缓存
第2、sk_buff结构体中有不少条件编译,好比:cookie
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info*nf_bridge;
#endif网络
由于sk_buff结构体是linux网络代码中最重要的数据结构,是整个网络传输载体。因此sk_buff结构体里面有不少关于其余功能的成员字段,好比:防火墙,子路由系统,多播等。这些字段并非必定有的,只有在知足特色条件才有的。因此能够在须要的时候再去关心这些成员字段,如今咱们只来说解下通常的成员字段。数据结构
第3、下面就直接来看sk_buff结构体了。为了好理解结构中的一些成员字段,先把后面要讲的内容提早说下。sk_buff结构体关联多个其余结构体,第一是数据区:由sk_buff中head和end指向的数据块,用来存储sk_buff结构的数据也便是存储数据包的内容和各层协议头。第二是分片结构:用来表示IP分片的一个结构体,实则上是和sk_buff结构的数据区相连的,便是end指针的下一个字节开始就是分片结构。也正是此缘由,因此分片结构和sk_buff数据区内存分配及销毁时都是一块儿的。第三个是分片结构指向的数据区,便是IP分片内容。下面开始看sk_buff结构体:并发
struct sk_buff { /* These two members must be first. */ struct sk_buff *next; // 由于sk_buff结构体是双链表,因此有前驱后继。这是个指向后面的sk_buff结构体指针 struct sk_buff *prev; // 这是指向前一个sk_buff结构体指针 //老版本(2.6之前)应该还有个字段: sk_buff_head *list //即每一个sk_buff结构都有个指针指向头节点 struct sock *sk; // 指向拥有此缓冲的套接字sock结构体,即:宿主传输控制模块 ktime_t tstamp; // 时间戳,表示这个skb的接收到的时间,通常是在包从驱动中往二层发送的接口函数中设置 struct net_device *dev; // 表示一个网络设备,当skb为输出/输入时,dev表示要输出/输入到的设备 unsigned long _skb_dst; // 主要用于路由子系统,保存路由有关的东西 char cb[48]; // 保存每层的控制信息,每一层的私有信息 unsigned int len, // 表示数据区的长度(tail - data)与分片结构体数据区的长度之和。其实这个len中数据区长度是个有效长度, // 由于不删除协议头,因此只计算有效协议头和包内容。如:当在L3时,不会计算L2的协议头长度。 data_len; // 只表示分片结构体数据区的长度,因此len = (tail - data) + data_len; __u16 mac_len, // mac报头的长度 hdr_len; // 用于clone时,表示clone的skb的头长度 // 接下来是校验相关域,这里就不详细讲了。 __u32 priority; // 优先级,主要用于QOS kmemcheck_bitfield_begin(flags1); __u8 local_df:1, // 是否能够本地切片的标志 cloned:1, // 为1表示该结构被克隆,或者本身是个克隆的结构体;同理被克隆时,自身skb和克隆skb的cloned都要置1 ip_summed:2, nohdr:1, // nohdr标识payload是否被单独引用,不存在协议首部。 // 若是被引用,则决不能再修改协议首部,也不能经过skb->data来访问协议首部。</span></span> nfctinfo:3; __u8 pkt_type:3, // 标记帧的类型 fclone:2, // 这个成员字段是克隆时使用,表示克隆状态 ipvs_property:1, peeked:1, nf_trace:1; __be16 protocol:16; // 这是包的协议类型,标识是IP包仍是ARP包或者其余数据包。 kmemcheck_bitfield_end(flags1); void (*destructor)(struct sk_buff *skb); // 这是析构函数,后期在skb内存销毁时会用到 #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) struct nf_conntrack *nfct; struct sk_buff *nfct_reasm; #endif #ifdef CONFIG_BRIDGE_NETFILTER struct nf_bridge_info *nf_bridge; #endif int iif; // 接受设备的index #ifdef CONFIG_NET_SCHED __u16 tc_index; /* traffic control index */ #ifdef CONFIG_NET_CLS_ACT __u16 tc_verd; /* traffic control verdict */ #endif #endif kmemcheck_bitfield_begin(flags2); __u16 queue_mapping:16; #ifdef CONFIG_IPV6_NDISC_NODETYPE __u8 ndisc_nodetype:2; #endif kmemcheck_bitfield_end(flags2); /* 0/14 bit hole */ #ifdef CONFIG_NET_DMA dma_cookie_t dma_cookie; #endif #ifdef CONFIG_NETWORK_SECMARK __u32 secmark; #endif __u32 mark; __u16 vlan_tci; sk_buff_data_t transport_header; // 指向四层帧头结构体指针 sk_buff_data_t network_header; // 指向三层IP头结构体指针 sk_buff_data_t mac_header; // 指向二层mac头的头 /* 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; // 表示总长度,包括sk_buff自身长度和数据区以及分片结构体的数据区长度 atomic_t users; // skb被克隆引用的次数,在内存申请和克隆时会用到 }; //end sk_buff第4、结构体中经常使用字段解释:
char cb[48];这个字段是skb信息控制块,也就是存储每层的一些协议信息,当数据包在哪一层时,存储的就是哪一层协议信息。这个字段由数据包所在层使用和维护,若是要访问本层协议信息,能够经过用一些宏来操做这个成员字段。如:#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))app
_u8 fclone:2;这是个克隆状态标志,到sk_buff结构内存申请时会使用到。这里提早讲下:若fclone = SKB_FCLONE_UNAVAILABLE,则代表SKB未被克隆;若fclone = SKB_FCLONE_ORIG,则代表是从skbuff_fclone_cache缓存池(这个缓存池上分配内存时,每次都分配一对skb内存)中分配的父skb,能够被克隆;若fclone = SKB_FCLONE_CLONE,则代表是在skbuff_fclone_cache分配的子SKB,从父SKB克隆获得的;tcp
atomic_t users;这是个引用计数,代表了有多少实体引用了这个skb。其做用就是在销毁skb结构体时,先查看下users是否为零,若不为零,则调用函数递减下引用计数users便可;当某一次销毁时,users为零才真正释放内存空间。有两个操做函数:atomic_inc()引用计数增长1;atomic_dec()引用计数减去1;
第5、几个数据长度len的解析:
(1)sk_buff->data_len:只计算分片中数据的长度,便是分片结构体中page指向的数据区长度。这个在分片结构体中会再详细讲解下。
(2)sk_buff->len:表示当前缓冲区中数据块的大小的总长度。它包括主缓冲中(便是sk_buff结构中指针data指向)的数据区的实际长度(data-tail)和分片中的数据长度。这个长度在数据包在各层间传输时会改变,由于分片数据长度不变,从L2到L4时,则len要减去帧头大小和网络头大小;从L4到L2则相反,要加上帧头和网络头大小。因此:len = (data - tail) + data_len;
(3)sk_buff->truesize:这是缓冲区的总长度,包括sk_buff结构和数据部分。若是申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。当skb->len变化时,这个变量也会变化。因此:truesize = len + sizeof(sk_buff) = (data - tail) + data_len + sizeof(sk_buff);
第6、数据包在各层间传输时,data指针的变化:
在第一的时候就讲了,sk_buff在各层间传输时,只改变指针和添加协议头,不拷贝也不删除协议头。下面来看下处理的详细进过,首先要说下,每一层协议都有个自身的协议头指针,如:二层有mac,三层有nh(nethead网络头),四层有h(head)。如今我用的内核版本把这几个协议头指针变为了:二层为mac_header,三层为network_header,四层为transport_header。下面就简单介绍下包的传输和data指针变化状况。
假设从收包开始:
(1)开始进入第二层时,这时data指针指向帧头。mac = data,而后操做mac指针已经数据包。当二层操做完后把包往三层传送时,会调用一个函数(具体什么函数后面会详细讲)让data指针指向三层的IP头;
(2)当包进入第三层时,这时data指针已经指向了IP头,让nh = data,而后操做nh指针已经数据包,当三层操做完后把包往四层传送时,一样调用一个函数把data指向四层的TCP头;同理,四层也是同样处理的,只移动指针,不删除协议头。发包时就相反了,只是变成了为每一层添加协议头了。下面是书上的图,供参考。
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;//表明表中skb元数的个数 spinlock_t lock;//锁,防止并发访问 };其实若是要学习linux内核我建议仍是从list.h文件开始学起,由于那是内核经常使用的结构(循环双链表,哈希链表),以及里面还有些操做宏。若是学懂了list.h文件,那么看内核中其余结构将会更简单些(由于里面的一些指针和内核定义的操做宏都很是相似)。先来解释下sk_buff_head结构体成员,qlen表示当前链表中skb的个数,lock成员是自旋锁这个为了构成一个原子操做,防止多条线程同时访问结构体,后面会详细解释下。这里重点解释下next和prev,这是个前驱后继指针,这里内核特地注释规定了前两个成员必定要是:next和prev指针。由于这使得sk_buff_head和sk_buff能够放到同一个链表中,尽管他们不是同一种结构体。另外,相同的函数能够一样应用于sk_buff_head和sk_buff。不少blog都这么说,但为何sk_buff的指针(prev和next)可以指向sk_buff_head结构呢?我查了不少资料,没结果。但最后我发现原文有句话是关键:“......在表的开端额外增长一个sk_buff_head结构做为一种哑元元素。”
sk_buff_head结构体和sk_buff结构体关系:
上面的图为《深刻理解linux网络技术内幕》原文图,显示了sk_buff_head和sk_buff结构体链表。但上面sk_buff结构体是内核2.6.xx.xx比较前的版本的,我如今用的是:内核2.6.32.63版本的。sk_buff结构中没有list这个成员了。
struct skb_shared_info { atomic_t dataref;// 用于数据区的引用计数,克隆一个skb结构体时,会增长一个引用计数 unsigned short nr_frags;// 表示有多少个分片结构 unsigned short gso_size; #ifdef CONFIG_HAS_DMA dma_addr_t dma_head; #endif /* Warning: this field is not always filled in (UFO)! */ unsigned short gso_segs; unsigned short gso_type; // 分片的类型 __be32 ip6_frag_id; union skb_shared_tx tx_flags; struct sk_buff *frag_list; // 这也是一种类型的分配数据 struct skb_shared_hwtstamps hwtstamps; skb_frag_t frags[MAX_SKB_FRAGS]; // 这是个比较重要的数组,到讲分片结构数据区时会细讲 #ifdef CONFIG_HAS_DMA dma_addr_t dma_maps[MAX_SKB_FRAGS]; #endif /* Intermediate layers must ensure that destructor_arg * remains valid until skb destructor */ void * destructor_arg; };这个分片结构体和sk_buff结构的数据区是一体的,因此在各类操做时都把他们两个结构看作是一个来操做。好比:为sk_buff结构的数据区申请和释放空间时,分片结构也会跟着该数据区一块儿分配和释放。而克隆时,sk_buff的数据区和分片结构都由分片结构中的 dataref 成员字段来标识是否被引用。
typedef struct skb_frag_struct skb_frag_t; struct skb_frag_struct { struct page *page; // 指向分片数据区的指针,相似于sk_buff中的data指针 __u32 page_offset; // 偏移量,表示从page指针指向的地方,偏移page_offset __u32 size; // 数据区的长度,即:sk_buff结构中的data_len };仍是请看下面的两幅图: