前言
编写内核数据转发代码之前需要对内核报文的数据结构以及函数说明有详细的了解,才能在后续的编码中能够根据自己的需求进行快速的开发。
一、skb_buff结构体
说明:sk_buff是Linux网络中最核心的结构体,它用来管理和控制接收或发送数据包的信息。各层协议都依赖于sk_buff而存在。内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来操作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L2到L4,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头。这样做是为了提高CPU的工作效率
struct sk_buff_head {
struct sk_buff *next; //链表的下一个元素
struct sk_buff *prev; //链表的前一个元素
__u32 qlen;
spinlock_t lock; //自旋锁
};
struct sk_buff {
struct sk_buff *next;
struct sk_buff *prev;
struct sock *sk;
struct ktime_t tstamp; //Time arrived, 记录接收或发送报文的时间戳
struct net_device *dev; //通过该设备接收或发送,记录网络接口的信息和完成操作
struct net_device *input_dev; //接收数据的网络设备
/*
* control buffer. 可以被任意层随意使用,可以将私有变量放在这里。大小为40字节。
* 如果要跨层保留它们,必须先执行skb_clone()。由用于skb队列ATM的所有。
*/
char cb[40] __aligned(8);
/*
* 函数指针可以初始化成一个在缓冲区释放时完成某些动作的函数。如果缓冲区不属于一个socket,
* 此函数通常不会被赋值。如果缓冲区属于一个socket,此函数指针会被赋值为sock_rfree
* 或sock_wfree(分别由skb_set_owner_r或skb_set_owner_w函数初始化)。两个sock_xxx
* 函数用于更新socket队列中的内存容量。
*/
void (*destructor)(struct sk_buff *skb);
unsigned int len, //缓冲区数据部分的长度
data_len; //data_len只计算分片中数据的长度
__u16 mac_len, //mac头的长度
hdr_len; //报头长度
__wsum csum;
__u32 priority; //描述发送或转发包的QoS类别。若本地生成,socket层会设置
u16 alloc_cpu;
/* 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;
refcount_t users; //引用计数,计算有多少实体引用了这个sk_buff缓冲区。
};
二、skb_buff结构头部和尾部
说明:
- head和end分别指向存放数据内存区域的头和尾,一旦分配就固定不变。
- data和tail分别是真正数据的起始位结束。
- head和data之间的区域成为headroom,data和tail之间的区域存放真正的数据,tail和end之间的区域成为tailroom。
- skb刚分配时,head,data和tail在同一位置,end在末尾,所以刚开始时,headroom大小为0,tailroom大小为size,后续对数据包的操作,通过移动data和tail完成,head和end固定不变。
重要的长度len的解析
这里要声明两个概念的区别,后续直接用这两个概念,注意区分:
(1)线性数据:head - end。
(2)实际线性数据:data - tail,不包含线性数据中的头空间和尾空间。
skb->data_len: skb中的分片数据(非线性数据)的长度。
skb->len: skb中的数据块的总长度,数据块包括实际线性数据和非线性数据,非线性数据为data_len,所以skb->len= (data - tail) + data_len。
skb->truesize: skb的总长度,包括sk_buff结构和数据部分,skb=sk_buff控制信息 + 线性数据(包括头空间和尾空间) + skb_shared_info控制信息 + 非线性数据,所以skb->truesize = sizeof(struct sk_buff) + (head - end) + sizeof(struct skb_shared_info) + data_len。
三 、skb_buff数据区
3.1 数据区
说明:
sk_buff数据区
sk_buff结构体中的都是sk_buff的控制信息,是网络数据包的一些配置,真正储存数据的是sk_buff结构体中几个指针指向的数据区中,线性数据区的大小 = (skb->end - skb->head),对于每个数据包来说这个大小都是固定不变的,在传输过程中skb->end和skb->head所指向的地址都是不变的,这里要注意这个地址不是本机的地址,如果是本机的地址那么数据包传到其他主机上这个地址就是无效的,所以这个地址是这个skb缓冲区的相对地址。
3.2 预留headroom
线性数据区是用来存放各层协议头部和应用层发下来的数据。各层协议头部相关信息放在线性数据区中。实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。
用一张图来表示sk_buff和数据区的关系:
说明:
开始准备存储应用层下发过来的数据,通过调用函数 skb_reserve(m) 来使 data 指针和 tail 指针同时向下移动,空出一部分空间来为后期添加协议信息。m一般为最大协议头长度,内核中定义。
static inline void skb_reserve(struct sk_buff *skb, int len)
{
skb->data += len;
skb->tail += len;
}
3.3 在尾部添加
说明:
存储数据
在尾部添加数据,通过调用函数 skb_put() 来使 tail 指针向下移动空出空间来添加数据,此时 skb->data 和 skb->tail 之间存放的都是数据信息,无协议信息。
void *skb_put(struct sk_buff *skb, unsigned int len)
{
void *tmp = skb_tail_pointer(skb);
SKB_LINEAR_ASSERT(skb);
skb->tail += len;
skb->len += len;
if (unlikely(skb->tail > skb->end))
skb_over_panic(skb, len, __builtin_return_address(0));
return tmp;
}
3.4 在头部添加
说明:
添加协议头
在skb头部添加协议头,调用函数 skb_push() 来使 data 指针向上移动,空出空间来添加各层协议信息,添加协议信息也是用skb_push()。直到最后到达二层,添加完帧头然后就开始发包了
void *skb_push(struct sk_buff *skb, unsigned int len);
static inline void *__skb_push(struct sk_buff *skb, unsigned int len)
{
skb->data -= len;
skb->len += len;
return skb->data;
}
3.5 在头部删除
说明:
从缓冲区的数据区删除len字节,将空出来的内存归还给头部空间。data指针下移,并减小skb的len值。这个操作使data指针指向下一层网络报文的头部:
void *skb_pull(struct sk_buff *skb, unsigned int len);
static inline void *__skb_pull(struct sk_buff *skb, unsigned int len)
{
skb->len -= len;
if (unlikely(skb->len < skb->data_len)) {
#if defined(CONFIG_DEBUG_NET)
skb->len += len;
pr_err("__skb_pull(len=%u)\n", len);
skb_dump(KERN_ERR, skb, false);
#endif
BUG();
}
return skb->data += len;
}