文章目录
- 1. 前言
- 2. skb 数据管理
- 2.1 初始化
- 2.2 数据的插入
- 2.2.1 在头部插入数据
- 2.2.2 在尾部插入数据
- 2.2 数据的移除
- 3. 小结
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. skb 数据管理
数据结构:
/* 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
时的初始化:
struct sk_buff *skb;
int frame_len; /* 网卡接收的数据帧长度 */
frame_len = ...;
skb = netdev_alloc_skb_ip_align(priv->dev, frame_len);
/* 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;
}
/* 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
这几个成员。从图中可见,在初始时:
head: 指向数据的开始位置。
data: 指向可用数据的开始位置,跳过了头部保留空间。
tail: 当前已填充数据的结尾位置偏移。
end: 数据可用空间结尾位置偏移。
2.2 数据的插入
数据插入位置可以是 头部
和 尾部
。
2.2.1 在头部插入数据
在头部
插入数据,首先通过 *skb_push()
系列 API 按插入数据长度移动数据位置指针,然后执行数据拷贝。
如进行 IP 数据分片
时:
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); /* 在 头部 插入 网络层 数据 */
...
}
/* 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;
}
从这里的示例代码可以看到,在头部
插入数据,分为两步:
1. 通过 __skb_push() 移动数据指针
2. 然后再将数据拷贝到指定位置
可见,*skb_push()
系列 API 只会移动数据指针,并不会做数据拷贝操作。
2.2.2 在尾部插入数据
在尾部
插入数据,首先执行数据拷贝,然后通过 *skb_put()
系列 API 按拷贝的数据长度移动 skb
数据位置指针。
如网卡接收数据时:
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); /* 移动数据指针 */
...
}
/* 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 */
/* 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 执行数据的移除:
/* net/core/skbuff.c */
void *skb_pull(struct sk_buff *skb, unsigned int len)
{
return skb_pull_inline(skb, len);
}
EXPORT_SYMBOL(skb_pull);
/* 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
的管理等等,以后有机会再和大家一起探讨。