Linux 网络:skb 数据管理

文章目录

  • [1. 前言](#1. 前言)
  • [2. skb 数据管理](#2. skb 数据管理)
    • [2.1 初始化](#2.1 初始化)
    • [2.2 数据的插入](#2.2 数据的插入)
      • [2.2.1 在头部插入数据](#2.2.1 在头部插入数据)
      • [2.2.2 在尾部插入数据](#2.2.2 在尾部插入数据)
    • [2.2 数据的移除](#2.2 数据的移除)
  • [3. 小结](#3. 小结)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. skb 数据管理

数据结构:

c 复制代码
/* include/linux/skbuff.h */

struct sk_buff {
	...
	/*
	 * @len: sk_buff 包含的数据长度(sk_buff::tail - sk_buff::data). 
	 *       当在协议层间移动时, @len 会改变: 添加(往下层)或移除(往上层)
	 *		 操作接口 skb_reserve(), skb_put(), skb_push(), skb_pull()
	 */
	unsigned int		len,
				data_len/* 仅用于分片场景下分片(fragment)数据的长度 */;
	__u16			mac_len/* MAC 头部长度 */,
				hdr_len;
	...
	__be16			protocol; /* ETH_P_IP, ... */
	__u16			transport_header; /* 传输层数据偏移 */
	__u16			network_header; /* 网络层数据偏移 */
	__u16			mac_header; /* MAC/Linker 层数据偏移 */
	...
	/* These elements must be at the end, see alloc_skb() for details.  */
	/*
	 * head --> -------------------
	 *         | reserved headroom |
	 * data -->|-------------------|
	 *         |///|
	 *         |///|
	 * tail -->|-------------------|
	 *         |      tailroom     |
	 * end  -->|-------------------|
	 *         |  skb_shared_info  |
	 *         |-------------------|
	 *         |       PAD         |
	 *          -------------------
	 */
	sk_buff_data_t		tail; /* 偏移位置: 指向当前数据的尾部,随数据的添加、移除而改变 */
	sk_buff_data_t		end; /* 偏移位置: 可用数据空间的尾部。end 后还跟有 skb_shared_info */
	unsigned char		*head, /* 数据指针: 指向可用数据空间数据头部 */
						*data; /* 数据指针: 指向当前数据开始位置 */
	unsigned int		truesize; /* sizeof(sk_buff) + size(数据空间大小) + sizeof(skb_shared_info), 由 alloc_skb() 初始化 */
	refcount_t			users; /* sk_buff 引用计数. skb_get(), kfree_skb() 接口影响 */
};

2.1 初始化

创建 skb 时的初始化:

c 复制代码
struct sk_buff *skb;
int frame_len; /* 网卡接收的数据帧长度 */

frame_len = ...;
skb = netdev_alloc_skb_ip_align(priv->dev, frame_len);
c 复制代码
/* include/linux/skbuff.h */

static inline struct sk_buff *netdev_alloc_skb_ip_align(struct net_device *dev,
		unsigned int length)
{
	return __netdev_alloc_skb_ip_align(dev, length, GFP_ATOMIC);
}

static inline struct sk_buff *__netdev_alloc_skb_ip_align(struct net_device *dev,
		unsigned int length, gfp_t gfp)
{
	struct sk_buff *skb = __netdev_alloc_skb(dev, length + NET_IP_ALIGN, gfp);

	if (NET_IP_ALIGN && skb)
		skb_reserve(skb, NET_IP_ALIGN); /* 在数据头部保留部分空间 */
	return skb;
}
c 复制代码
/* net/core/skbuff.c */

struct sk_buff *__netdev_alloc_skb(struct net_device *dev, unsigned int len,
				   gfp_t gfp_mask)
{
	struct page_frag_cache *nc;
	unsigned long flags;
	struct sk_buff *skb;
	bool pfmemalloc;
	void *data;

	/*
	 * 数据长度对齐:
	 * 没有特别定义 NET_SKB_PAD 时,是对齐到 cache 行。
	 */
	len += NET_SKB_PAD;

	...

	/* skb_shared_info 包含在 skb 的数据(长度)内 */
	len += SKB_DATA_ALIGN(sizeof(struct skb_shared_info)); 
	len = SKB_DATA_ALIGN(len);

	...
	data = page_frag_alloc(nc, len, gfp_mask); /* 分配 @len 数据空间 */
	...

	skb = __build_skb(data, len); /* 用 @len 长度数据 @data 构建 skb */
	...

skb_success:
	skb_reserve(skb, NET_SKB_PAD); /* 在数据头部保留部分空间 */
	skb->dev = dev; /* 设定 skb 的 所属的 网卡设备对象 */

skb_fail:
	return skb;
}

struct sk_buff *__build_skb(void *data, unsigned int frag_size)
{
	struct skb_shared_info *shinfo;
	struct sk_buff *skb;
	unsigned int size = frag_size ? : ksize(data);

	/* 创建 skb 对象 */
	skb = kmem_cache_alloc(skbuff_head_cache, GFP_ATOMIC);
	...

	size -= SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

	memset(skb, 0, offsetof(struct sk_buff, tail)); /* skb->tail 之前的所有成员清 0 */
	skb->truesize = SKB_TRUESIZE(size);
	refcount_set(&skb->users, 1);
	skb->head = data;
	skb->data = data;
	skb_reset_tail_pointer(skb); /* 初始化 tail: 指向数据开始的偏移位置,即 skb->data 所在的偏移位置 */
	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 */
	shinfo = skb_shinfo(skb);
	memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
	atomic_set(&shinfo->dataref, 1);

	return skb;
}

我们用一张图来描述 skb 的初始状态:

skb 数据管理,主要依靠 tail, end, head, data 这几个成员。从图中可见,在初始时:

bash 复制代码
head: 指向数据的开始位置。
data: 指向可用数据的开始位置,跳过了头部保留空间。
tail: 当前已填充数据的结尾位置偏移。
end: 数据可用空间结尾位置偏移。

2.2 数据的插入

数据插入位置可以是 头部尾部

2.2.1 在头部插入数据

头部插入数据,首先通过 *skb_push() 系列 API 按插入数据长度移动数据位置指针,然后执行数据拷贝。

如进行 IP 数据分片时:

c 复制代码
int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
		   int (*output)(struct net *, struct sock *, struct sk_buff *))
{
	...
	skb_reset_transport_header(frag); /* 设置 传输层 数据偏移位置 */
	__skb_push(frag, hlen); /* 往后移动数据指针,在头部为 网络层协议头(L3) 划分一部分空间 */
	skb_reset_network_header(frag); /* 设置 网络层 数据偏移位置 */
	memcpy(skb_network_header(frag), iph, hlen); /* 在 头部 插入 网络层 数据 */
	...
}
c 复制代码
/* include/linux/skbuff.h */

/* 设置 传输层(L4) TCP/UDP 头部数据偏移位置 */
static inline void skb_reset_transport_header(struct sk_buff *skb)
{
	skb->transport_header = skb->data - skb->head;
}

/*
 * 从头部增加 @len 长度数据后调用: 
 * . skb->data 后退 @len 个位置 (skb->data -= len)
 * . skb->len 增大 @len
 */
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;
}

/* 设置 网络层(L3) IP 头部数据偏移位置 */
static inline void skb_reset_network_header(struct sk_buff *skb)
{
	skb->network_header = skb->data - skb->head;
}

/* 返回 网络层 (L3) IP 头部位置 (struct ip_header *) */
static inline unsigned char *skb_network_header(const struct sk_buff *skb)
{
	return skb->head + skb->network_header;
}

从这里的示例代码可以看到,在头部插入数据,分为两步:

bash 复制代码
1. 通过 __skb_push() 移动数据指针
2. 然后再将数据拷贝到指定位置

可见,*skb_push() 系列 API 只会移动数据指针,并不会做数据拷贝操作。

2.2.2 在尾部插入数据

尾部插入数据,首先执行数据拷贝,然后通过 *skb_put() 系列 API 按拷贝的数据长度移动 skb 数据位置指针。

如网卡接收数据时:

c 复制代码
static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
{
	...
	/* 分配 skb 缓冲(细节见前面分析) */     
	skb = netdev_alloc_skb_ip_align(priv->dev, frame_len);
	/* 拷贝接收的数据(RING BUFFE 中的数据)到 skb 缓冲 */
	skb_copy_to_linear_data(skb,
							rx_q->rx_skbuff[entry]->data,
							frame_len);
	skb_put(skb, frame_len); /* 移动数据指针 */
	...
}
c 复制代码
/* include/linux/skbuff.h */

static inline void skb_copy_to_linear_data(struct sk_buff *skb,
					   const void *from,
					   const unsigned int len)
{
	memcpy(skb->data, from, len);
}

#ifdef NET_SKBUFF_DATA_USES_OFFSET
static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb)
{
	return skb->head + skb->tail;
}

static inline void skb_reset_tail_pointer(struct sk_buff *skb)
{
	skb->tail = skb->data - skb->head;
}

static inline void skb_set_tail_pointer(struct sk_buff *skb, const int offset)
{
	skb_reset_tail_pointer(skb);
	skb->tail += offset;
}

#else /* NET_SKBUFF_DATA_USES_OFFSET */
static inline unsigned char *skb_tail_pointer(const struct sk_buff *skb)
{
	return skb->tail;
}

static inline void skb_reset_tail_pointer(struct sk_buff *skb)
{
	skb->tail = skb->data;
}

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

#endif /* NET_SKBUFF_DATA_USES_OFFSET */
c 复制代码
/* net/core/skbuff.c */

/*
 * 增加数据长度,在【尾部】增加数据时使用: 
 * . 前进数据【尾部偏移】 skb->tail += len
 * . 增大数据长度 skb->len += len
 * 返回: 数据旧的尾部位置数据指针。
 */
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;
}
EXPORT_SYMBOL(skb_put);

2.2 数据的移除

处于效率的考虑,skb 数据的移除,并不是真的移除,而是仅仅移动数据指针位置。通过 *skb_pull() 系列 API 执行数据的移除:

c 复制代码
/* net/core/skbuff.c */

void *skb_pull(struct sk_buff *skb, unsigned int len)
{
	return skb_pull_inline(skb, len);
}
EXPORT_SYMBOL(skb_pull);
c 复制代码
/* include/linux/skbuff.h */

/*
 * 从头部拉取 @len 长度数据后调用: 
 * . skb->data 前进 @len 个位置 (skb->data += len)
 * . skb->len 减小 @len
 */
static inline void *skb_pull_inline(struct sk_buff *skb, unsigned int len)
{
	return unlikely(len > skb->len) ? NULL : __skb_pull(skb, len);
}

/*
 * 从头部拉取 @len 长度数据后调用: 
 * . skb->data 前进 @len 个位置 (skb->data += len)
 * . skb->len 减小 @len
 */
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;
	BUG_ON(skb->len < skb->data_len);
	return skb->data += len;
}

3. 小结

本文简答的对 skb 数据管理的最基本情形 - 线性数据的插入删除 - 做了描述,事实上,skb 的数据的管理,远比这个要更复杂,譬如 IP 分片的非线性数据管理skb_shared_info 的管理等等,以后有机会再和大家一起探讨。

相关推荐
火车叼位15 分钟前
命令`ls **/*.exe`遗漏本目录下文件?Bash的globstar配置了解一下
linux·shell
违章的王17 分钟前
环路广播风暴演示图
网络·智能路由器
嵌入式-老费1 小时前
Linux上位机开发实战(x86和arm自由切换)
linux·运维·arm开发
猪猪侠|ZZXia1 小时前
# linux有哪些显示服务器协议、显示服务器、显示管理器、窗口管理器?有哪些用于开发图形用户界面的工具包?有哪些桌面环境?
linux·服务器
网络安全(king)1 小时前
基于java社交网络安全的知识图谱的构建与实现
开发语言·网络·深度学习·安全·web安全·php
艾希逐月1 小时前
【动手实验】TCP 连接的建立与关闭抓包分析
网络·tcp/ip
人间凡尔赛1 小时前
VSCode-Server 在 Linux 容器中的手动安装指南
linux·运维·服务器·docker
Chenyu_3101 小时前
05.基于 TCP 的远程计算器:从协议设计到高并发实现
linux·网络·c++·vscode·网络协议·tcp/ip·算法
板鸭〈小号〉1 小时前
Linux开发工具----vim
linux·运维·vim
洛神灬殇2 小时前
【技术白皮书】内功心法 | 第二部分 | Telnet远程登录的工作原理
运维·服务器·网络