TCP实现之:sk_buff结构浅析

TCP实现之:sk_buff结构浅析

一、前言

sk_buff是内核中用于存储报文缓存信息的结构体,可以算得上是内核协议栈中最重要的一个数据结构了。一方面,为了保持高效的网络报文处理效率,这要求sk_buff的结构也必须是高效的;另一方面,sk_buff被内核协议栈中的各个协议所共同使用,并在各个协议之间传递,这要求其能够兼容所有的网络协议。种种因素导致了这个结构体异常的复杂,广定义就花了三页的代码,这里我们简单对其关键属性进行分析。

二、报文存储

首先我们要理解网络报文存储的几个概念:线性缓存区、IP分片区和分散聚合I/O区。这几个概念还挺抽象,咱们一个个分析哈。

2.1 线性区

skb的初始化

首先来分析一下线性缓存区。线性缓存区就是sk_buff常规的存储报文数据(包括报文头和报文数据)的地方。sk_buff的创建是通过__alloc_skb函数来进行的,该函数的定义如下:

struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
			    int flags, int node)

在进行skb的内存分配时,要分配两部分的内存:skb结构体本身所占据的内存和存放报文数据的线性缓冲区buff内存。__alloc_skb第一个参数size代表的是要分配的线性缓存区buff的大小。在分配buff的时候,内核会在size的基础上额外分配一段内存用于存储skb_shared_info,该结构体用于存储一些额外的信息,如IP分片的frag_list等。

sk_buff采用四个字段来管理和维护buff缓冲区:headdatatailend,其与线性缓存区的对应关系如下图所示:

在这里插入图片描述

  • head:指向缓存区头部的地址。
  • end:指向缓存区尾部的地址。
  • data:指向报文数据的地址。注意,这里的报文数据是一个相对的过程。对于IP协议,它不关注甚至不知晓链路层协议,因此它关注的报文数据是从IP头部开始的,这里的data就会指向IP报头的地址;而对于TCP协议,data就会指向TCP报头地址。skb在各层协议之间传递时,data会被修改。datahead之间的这段空间用于报头的填充,被称为headroom
  • tail:指向报文数据的尾部。endtail之间的这段空间被称为tailroom,有些协议可能会在报文的尾部附加信息,这段空间为这种情况做准备的。

我们注意到,tailend的类型是sk_buff_data_t,翻开源码可以看到该数据类型根据内核配置的不同,意义也不同:采用地址还是采用相对于head的偏移量。

#ifdef NET_SKBUFF_DATA_USES_OFFSET
typedef unsigned int sk_buff_data_t;
#else
typedef unsigned char *sk_buff_data_t;
#endif

sk_buff刚创建的时候,headdatatail均指向缓存区开始的地方,end指向缓冲区的尾部,如下图所示:

image-20200512202001728
协议相关变量

除了上面基本的四个变量外,sk_buff还定义了一些成员变量用于对标准协议的支持。首先我们来看一下,如何从skb中提取二层、三层和四层报文数据。skb使用以下三个成员变量来记录各个协议层的报文在线性缓冲区中的位置,这三个变量是相对于head的偏移量。

	__u16			transport_header;	//传输层报文(tcp、udp等)头部偏移量
	__u16			network_header;		//网络层报文(ip等)头部偏移量
	__u16			mac_header;			//以太网报文头部偏移量

内核为每个协议都封装好了函数,用于从skb中获取报文数据地址:

static inline unsigned char *skb_transport_header(const struct sk_buff *skb)
{
	return skb->head + skb->transport_header;
}
static inline unsigned char *skb_network_header(const struct sk_buff *skb)
{
	return skb->head + skb->network_header;
}

static inline unsigned char *skb_mac_header(const struct sk_buff *skb)
{
	return skb->head + skb->mac_header;
}

各个协议的报文获取函数基本上都是上面三个函数的直接调用,例如iphdr()直接调用的skb_network_header()

除此之外,sk_buff中还定义了与之对应的inner类型的成员变量,用于在网卡处进行报文分段:

	__u16			inner_transport_header;
	__u16			inner_network_header;
	__u16			inner_mac_header;
统计相关变量
	unsigned int		len,
				data_len;
	__u16			mac_len,
				hdr_len;
	unsigned int		truesize;
	refcount_t		users;
  • mac_len:二层协议头部的长度,在以太网中为以太网报头的长度。
  • hdr_len:对于克隆的skb,可读写的报文头部的长度。
  • len:当前报文数据的长度,包括当前skb线性缓冲区和分片区中报文数据长度的总和。
  • data_lenIP分片区数据的长度。
  • truesize:整个套接字缓冲区的长度,包括skb、线性缓冲区以及分散聚合I/O区的数据长度。
  • users:当前skb的引用计数,引用计数归0的时候skb才能被释放。

2.2 IP分片区

IP分片区,顾名思义,就是存储用于IP分片时的数据。想要理解这个概念,首先我们看一下skb线性缓存区尾部的那个skb_shared_info结构体:

struct skb_shared_info {
	struct sk_buff	*frag_list;			-> IP分片数据区
	skb_frag_t	frags[MAX_SKB_FRAGS];	-> 分散聚合I/O数据区
	......
};

该结构体中的frag_list是指向struct sk_buff结构体的指针,准确的说,指向sk_buff链表的首地址,该链表中的sk_buff通过struct sk_buffnext字段形成一个链表,具体的构造过程可参考我的《TCP实现之: IP 分片内核实现》中的__ip_make_skb函数。

需要说明的是,frag_list链表中的skb都没有分配IP报头,且当前skblen字段的值是自身以及所有frag_list中的skb的和,truesize也是这样的。由于IP报文长度的上限是0xffff,所以len的长度不会超过这个值。

想要理解内核IP分片过程的实现,请参考我的另一篇文章:

2.3 分散聚合I/O区

什么是分散聚合?简单来说,就是分散—聚合!把分散的数据聚合起来就叫分散聚合。什么意思呢?举个例子,我有两块缓冲数据:buf1buf2,现在想将其整合成一个报文发送出去。常规的流程可能是,先申请一块大的内存buf3,然后将buf1buf2拷贝到buf3中,最后将buf3交给网卡来进行数据的发送。这个过程中有个问题,就是产生了两次内存拷贝,浪费了资源。分散聚合I/O就是不分配buf3,而是分配一个数组用来存储buf1buf2的地址,并将其传递给网卡。网卡拿到数组后,依次取出里面的指针,并将指针指向的数据发送出去,从而避免了无用的内存拷贝。

struct skb_shared_info {
	skb_frag_t	frags[MAX_SKB_FRAGS];	-> 分散聚合I/O数据区
	......
};

MAX_SKB_FRAGS为16,意味着分散数据区不能超过16个。skb_frag_t是用来存储分散缓存区信息的,其定义如下:

struct skb_frag_struct {
	struct {
		struct page *p;
	} page;
#if (BITS_PER_LONG > 32) || (PAGE_SIZE >= 65536)
	__u32 page_offset;
	__u32 size;
#else
	__u16 page_offset;
	__u16 size;
#endif
};

从上面可以看出,其存储了缓存区所在的页和页偏移,以及缓存区的大小。

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值