sk_buff是Linux网络中最核心的结构体,它用来管理和控制接收或发送数据包的信息。各层协议都依赖于sk_buff而存在。内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来操作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L2到L4,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头。这样做是为了提高CPU的工作效率。

1.skb_buff关键结构成员

struct sk_buff {

__u16 transport_header; //传输头相对于skb->head的偏移

__u16 network_header;//网络头相对于skb->head的偏移

__u16 mac_header;//以太网头相对于skb->head的偏移

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

}

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。

2.sk_buff数据区

sk_buff结构体中的都是sk_buff的控制信息,是网络数据包的一些配置,真正储存数据的是sk_buff结构体中几个指针指向的数据区中,线性数据区的大小 = (skb->end - skb->head),对于每个数据包来说这个大小都是固定不变的,在传输过程中skb->end和skb->head所指向的地址都是不变的,这里要注意这个地址不是本机的地址,如果是本机的地址那么数据包传到其他主机上这个地址就是无效的,所以这个地址是这个skb缓冲区的相对地址。

线性数据区是用来存放各层协议头部和应用层发下来的数据。各层协议头部相关信息放在线性数据区中。实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。
用一张图来表示sk_buff和数据区的关系:

2.1 存储数据前的准备

开始准备存储应用层下发过来的数据,通过调用函数 skb_reserve(m) 来使 data 指针和 tail 指针同时向下移动,空出一部分空间来为后期添加协议信息。m一般为最大协议头长度,内核中定义。

static inline void skb_reserve(struct sk_buff *skb, int len)
{
	skb->data += len;
	skb->tail += len;
}

 2.2 存储数据

在尾部添加数据,通过调用函数 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;
}

2.3 添加协议头 

在skb头部添加协议头,调用函数 skb_push() 来使 data 指针向上移动,空出空间来添加各层协议信息,添加协议信息也是用skb_put()。直到最后到达二层,添加完帧头然后就开始发包了

void *skb_push(struct sk_buff *skb, unsigned int len)
{
	skb->data -= len;
	skb->len  += len;
	if (unlikely(skb->data < skb->head))
		skb_under_panic(skb, len, __builtin_return_address(0));
	return skb->data;
}

3 内核中skb的分配和常用的函数

struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
 cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;
//从 cache里取出一个skb结构体。为了提高分配skb效率,会在初始化时,分配一个skb
//放在cache中。
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
//分配数据区域和skb_shared_info,它俩是在一块连续内存中
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
/* kmalloc(size) might give us more room than requested.
* Put skb_shared_info exactly at the end of allocated zone,
* to allow max possible filling before reallocation.
*/
分配完内存后,将size减去skb_shared_info 的大小,此时size只表示存放数据的大小
size = SKB_WITH_OVERHEAD(ksize(data));
#define SKB_WITH_OVERHEAD(X) \
((X) - SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
prefetchw(data + size);
/*
* Only clear those fields we need to clear, not those that we will
* actually initialise below. Hence, don't put any more fields after
* the tail pointer in struct sk_buff!
*/
//将skb tail前面的成员全部清零。tail后面的不用清零,因为随后就会赋值
memset(skb, 0, offsetof(struct sk_buff, tail));
/* Account for allocated memory : skb + skb->head */
skb->truesize = SKB_TRUESIZE(size);
skb->pfmemalloc = pfmemalloc;
//引用计数设置为1
atomic_set(&skb->users, 1);
//在后续报文处理过程中,head和end分别表示数据内存的起始和结尾,是固定不变的
//通过偏移data和tail来指向不同的数据位置
//初始化时head和data指针都指向data
skb->head = data;
skb->data = data;
//在64位下,tail和end都是整数,表示相对于head的偏移
//初始时tailf为0,即指向head
//end为tail+size,即指向内存中存放数据的末尾,skb_shared_info的起始
skb_reset_tail_pointer(skb);
    skb->tail = skb->data - skb->head
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;

/* make sure we initialize shinfo sequentially */
//返回 skb->head + skb->end,即为skb_shared_info的首地址
shinfo = skb_shinfo(skb);
  #define skb_shinfo(SKB) ((struct skb_shared_info *)  
    (skb_end_pointer(SKB)))skb->head + skb->end
memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
//设置shinfo引用计数为1
atomic_set(&shinfo->dataref, 1);
kmemcheck_annotate_variable(shinfo->destructor_arg);
}
  1. 在skb头部添加 len 字节
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
  skb->data -= len;
  skb->len += len;

2.在skb头部删除 len 字节

unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
  skb->len -= len;
  skb->data += len;

3.在skb尾部添加 len 字节

unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
skb->tail += len;
skb->len += len;

4.预留headroom

static inline void skb_reserve(struct sk_buff *skb, int len)
  skb->data += len;
  skb->tail += len;

4 数据从驱动上送协议栈过程skb分析

数据包从驱动接收到上送协议栈的处理中,skb的操作,mac,网络和传输头的变化

 4.1 mac头处理

mac处理是通过eth_type_trans函数

eth_type_trans
  //reset mac头位置,mac_header 是相对于skb->skb的偏移
  skb_reset_mac_header(skb);
    skb->mac_header = skb->data - skb->head; //mac_header存储的是基于header的一个偏移量
  //将data指针向后偏移14字节,指向下一个协议头,即三层头或者vlan头
  //skb->len表示数据包的总长度,偏移14字节后,len也要减去14
  #define ETH_HLEN 14 /* Total octets in header. */
  skb_pull_inline(skb, ETH_HLEN);
    skb->len -= len;
    skb->data += len;
  //通过 skb->mac_header 仍然可以获取mac头
  eth = eth_hdr(skb);
    (struct ethhdr *)skb_mac_header(skb);
    skb->head + skb->mac_header;
  //根据数据包的目的mac决定此数据包的pkt_type
  //如果目的mac中从左往右第二个字节为1为组播,并且如果mac为     全1,则为广播,否则为组播
  //如果目的mac中从左往右第二个字节不为1为单播,并且和接收设备的mac不同则设置为PACKET_OTHERHOST
  //如果和接收设备的mac相同,则不用设置,默认为0,即   PACKET_HOST,表示 to us的数据包
  if (unlikely(is_multicast_ether_addr(eth->h_dest))) {
    if (ether_addr_equal_64bits(eth->h_dest, dev->broadcast))
      skb->pkt_type = PACKET_BROADCAST;
    else
      skb->pkt_type = PACKET_MULTICAST;
  }
  else if (unlikely(!ether_addr_equal_64bits(eth->h_dest,dev->dev_addr)))
    skb->pkt_type = PACKET_OTHERHOST;
  //如果以太网协议大于ETH_P_802_3_MIN,则返回以太网协议即可
  if (likely(ntohs(eth->h_proto) >= ETH_P_802_3_MIN))
    return eth->h_proto;

 eth_type_trans处理完成后
skb->mac_header = skb->data - skb->head;
skb->data 指向网络

4.2 网络协议头处理

经过软中断处理后,会调用__netif_receive_skb_core上送到协议栈

__netif_receive_skb_core
 //设置网络头偏移量
 skb_reset_network_header(skb);
   skb->network_header = skb->data - skb->head;
 //重置mac头长度
 skb_reset_mac_len(skb);
   skb->mac_len = skb->network_header - skb->mac_header;

 skb->mac_header = skb->data - skb->head;
skb->network_header = skb->data - skb->head;
skb->data 仍然指向网络

4.3 对IP头的处理

static struct sk_buff *ip_rcv_core(struct sk_buff *skb, struct net *net)
{
	const struct iphdr *iph;
	u32 len;

	/* When the interface is in promisc. mode, drop all the crap
	 * that it receives, do not try to analyse it.
	 */
	if (skb->pkt_type == PACKET_OTHERHOST)
		goto drop;


	__IP_UPD_PO_STATS(net, IPSTATS_MIB_IN, skb->len);

	skb = skb_share_check(skb, GFP_ATOMIC);
	if (!skb) {
		__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);
		goto out;
	}

	if (!pskb_may_pull(skb, sizeof(struct iphdr)))
		goto inhdr_error;

	iph = ip_hdr(skb);
。。。。。。。。。。。。。。。。。。
}

涉及到对skb操作的函数有pskb_may_pull,首先对len合法性进行判断。 

if (!pskb_may_pull(skb, sizeof(struct iphdr)))
  /* skb_headlen定义为skb->len - skb->data_len。即skb->head指向  
  的线性缓冲区里当前
  * 有效数据的长度。*/
    if (likely(len <= skb_headlen(skb)))
      return 1;
    if (unlikely(len > skb->len))
      return 0;
    return __pskb_pull_tail(skb, len - skb_headlen(skb)) != NULL;
  iph = ip_hdr(skb);
    (struct iphdr *)skb_network_header(skb);
      skb->head + skb->network_header;
  skb->transport_header = skb->network_header + iph->ihl*4;

skb->network_header = skb->data - skb->head;
skb->transport_header = skb->network_header + iph->ihl*4;
skb->data 仍然指向网络

4.4 对传输层进行处理

ip_local_deliver_finish
  //获取网络头长度
  static inline u32 skb_network_header_len(const struct sk_buff *skb)
    return skb->transport_header - skb->network_header;
  //偏移data到传输头
  __skb_pull(skb, skb_network_header_len(skb));
    skb->len -= len;
    skb->data += len;
  int protocol = ip_hdr(skb)->protocol;
  ipprot = rcu_dereference(inet_protos[protocol]);
  ipprot->handler(skb);

skb->transport_header = skb->network_header + iph->ihl*4;
skb->data 指向传输层

4.5 对应用进行处理

icmp_rcv
  pskb_pull(skb, sizeof(*icmph))
    skb->len -= len;
    skb->data += len;
  icmph = icmp_hdr(skb);
    (struct icmphdr *)skb_transport_header(skb);
      skb->head + skb->transport_header;

skb->transport_header = skb->network_header + iph->ihl*4;
skb->data 指向icmp头后面的数据内容

head+transport_header后指向icmp头


 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐