文章目录
- 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
的管理等等,以后有机会再和大家一起探讨。