skb_buff基本原理
内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来操作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L4到L2,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头,这样方式极大的提高CPU工作效率。
sk_buff结构体是linux网络代码中最重要的数据结构,是整个网络传输载体。所以sk_buff结构体里面有很多关于其他功能的成员字段,比如:防火墙,子路由系统,多播等。这些字段并不是一定有的,只有在满足特点条件才有的。所以可以在需要时候再去关心这些成员字段,现在我们只来讲解主要的成员字段。
skb_buff主要字段
为了好理解结构中的一些成员字段,先把后面要讲的内容提前说下。sk_buff结构体关联多个其他结构体,主要可以分为: 第一是数据区:由sk_buff中head和end指向的数据块,用来存储sk_buff结构的数据也即是存储数据包的内容和各层协议头。 第二是分片结构:用来表示IP分片的一个结构体,实则上是和sk_buff结构的数据区相连的,即是end指针的下一个字节开始就是分片结构。正因此,分片结构和sk_buff数据区内存分配及销毁时都是一起的。 第三个是分片结构指向的数据区,即是IP分片内容。
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来访问协议首部。
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
char cb[48];这个字段是skb信息控制块,也就是存储每层的一些协议信息,当数据包在哪一层时,存储的就是哪一层协议信息。这个字段由数据包所在层使用和维护,如果要访问本层协议信息,可以通过用一些宏来操作这个成员字段。如:#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))
_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克隆得到的;
atomic_t users;这是个引用计数,表明了有多少实体引用了这个skb。其作用就是在销毁skb结构体时,先查看下users是否为零,若不为零,则调用函数递减下引用计数users即可;当某一次销毁时,users为零才真正释放内存空间。有两个操作函数:atomic_inc()引用计数增加1;atomic_dec()引用计数减去1;
sk_buff->len:表示当前缓冲区中数据块的大小的总长度。它包括主缓冲中(即是sk_buff结构中指针data指向)的数据区的实际长度(data-tail)和分片中的数据长度。这个长度在数据包在各层间传输时会改变,因为分片数据长度不变,从L2到L4时,则len要减去帧头大小和网络头大小;从L4到L2则相反,要加上帧头和网络头大小。所以:len = (data - tail) + data_len;
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);