簡說 套接字緩存的內存空間佈局
本文將分享到 sk_buff,但鑑於其在內核網絡中一些複雜情況,本次只簡單介紹 sk_buff 內存空間佈局情況與相關操作。
套接字數據緩存 (socket buffer) 在 Linux 內核中表示爲:struct sk_buff,是 Linux 內核中數據包管理的基本單元,主要包含兩個部分,其一:管理數據,即數據包的管理信息;其二:報文數據,保存了實際網絡中傳輸的數據,在內核協議棧起承上啓下的作用,也有很多值得關注的 sk_buff 操作。
1、sk_buff 四大指針與相關操作
分配初始化: struct sk_buff 中四個指針都指向數據區,分別是 head、data、tail、end, 剛剛分配出來的 sk_buff 會立馬進行四大指針的初始操作。
分配 sk_buff 如下所示:
struct sk_buff *buff;
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
sk_stream_alloc_skb 最終調用__alloc_skb 函數進行內存分配,分配 skb 後,進行四大指針的初始化操作:
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct sk_buff *skb;
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node)
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
}
其中 skb_reset_tail_pointer(skb):
static inline void skb_reset_tail_pointer(struct sk_buff *skb)
{
skb->tail = skb->data;
}
最終四大指針初始化爲以下圖所示:
此時 head、data、tail 三個指針指向一起,end 指向數據緩衝區的尾部。
預留協議頭空間: 在 sk_stream_alloc_skb 調用__alloc_skb 函數進行內存分配後,下一步就會預留協議頭空間,使得 head、tail、data 指針分離:
struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp,
bool force_schedule)
{
struct sk_buff *skb;
......
skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
......
skb_reserve(skb, sk->sk_prot->max_header);
......
}
skb_reserve 如下,
static inline void skb_reserve(struct sk_buff *skb, int len)
{
skb->data += len;
skb->tail += len;
}
操作後 skb_buff 的指針如下所示:
skb_reserve 作用就是預留空間,而且是盡最大的空間預留,但它並沒有把數據放到該空間,只是簡單更新指針,預留空間!
因爲很多頭都會有可選項,那麼不知道頭部可選項是多大,所以只能按照最大的分配,同時也要明白一點,預留的空間 headroom 不一定使用完,可能還有剩餘。當我們要增加協議頭信息的時候,data 指針向上移動,當增加數據的時候 tail 指針向下移動,完成數據包的封裝。此時還沒有數據,data 和 tail 指向相同。
操作 tailroom 中用戶數據塊區域:skb_put 用於修改指向數據區末尾的指針 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;
}
可以看到 tail 指針的移動是擴大數據區域,即數據區向下擴大 len 字節,並更新數據區長度 len。
增加 headroom 區域的協議頭: skb_push 函數用於移動 data 指針,增加頭部協議,與 skb_reserve() 類似,也並沒有真正向數據緩存區中添加數據,而只是移動數據緩存區的頭指針 data。數據由其他函數複製到數據緩存區中。函數如下:
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;
}
如下兩張圖分別是由傳輸層、網絡層,數據包向下傳遞時 data 指針移動,進行頭部協議的封裝。
-
TCP 層添加 TCP 首部。
-
SKB 傳遞到 IP 層,IP 層爲數據包添加 IP 首部。
-
SKB 傳遞到鏈路層,鏈路層爲數據包添加鏈路層首部。
可以看到在數據包封裝的過程中,每一層移動 data 指針進行數據報頭的封裝。
數據報文解封裝,解除協議頭: skb_pull 通過將 data 指針向下移動,進行數據報文的解封裝,函數如下所示:
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;
}
如下圖所示,在收包流程上,向上層協議,如下網絡層向傳輸層傳送的時候,調用 skb_pull 進行數據包的解封裝。
以上就是 struct sk_buff 的四大指針的相關操作, 通過分析可得:
-
head 指向緩衝區的首地址,作爲上邊界
-
end 指向緩衝區的尾地址,作爲下邊界
-
data 指針在數據包頭部封裝和解封裝的過程中移動,指向各層的協議頭,skb_push 函數將 data 的指向,向低地址移動 (向上),完成協議頭空間的佔據,skb_pull 函數將 data 的指向,向高地址移動 (向下),完成協議頭的解封裝。
-
tail 指針在增加應用層用戶緩衝數據時移動,skb_put 函數將該指針向高地址移動 (向上),完成用戶數據空間的佔據。
2、非線性區域
在 1、中,可以看到每張 sk_buff 的圖:在 end 指針緊挨着一個非線性區域;
在 struct sk_buff 中沒有指向 skb_shared_info 結構的指針,利用 end 指針,,可以用 skb_shinfo 宏來訪問:
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))
其中 skb_end_pointer 函數如下,返回 end 指針
static inline unsigned char *skb_end_pointer(const struct sk_buff *skb)
{
return skb->end;
}
具體地,struct skb_shared_info 如下:
struct skb_shared_info {
__u8 __unused;
__u8 meta_len;
//數組frags包含的元素個數
__u8 nr_frags;
__u8 tx_flags;
unsigned short gso_size;
/* Warning: this field is not always filled in (UFO)! */
unsigned short gso_segs;
struct sk_buff *frag_list;
struct skb_shared_hwtstamps hwtstamps;
unsigned int gso_type;
u32 tskey;
/*
* Warning : all fields before dataref are cleared in __alloc_skb()
*/
//結構skb_shared_info 的引用計數器
atomic_t dataref;
/* Intermediate layers must ensure that destructor_arg
* remains valid until skb destructor */
void * destructor_arg;
/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS];
};
其中 skb_frag_t 如下:
typedef struct skb_frag_struct 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
};
nr_frags,frags,frag_list 與 IP 分片存儲有關。
frag_list 的用法:
-
用於在接收分組後鏈接多個分片,組成一個完整的 IP 數據報
-
在 UDP 數據報輸出中,將待分片的 SKB 鏈接到第一個 SKB 中,然後在輸出過程中能夠快速的分片
-
用於存放 FRAGLIST 類型的聚合分散 I/O 數據包
判斷是否存在非線性緩衝區:
- 先說明 struct sk_buff 中關於長度的兩個字段
-
len 字段:無分片的報文,數據報文的大小
-
data_len 字段:存在分散報文,data_len 表示分片的部分大小
如下所示,沒有開啓分片的報文 len = x,data_len = 0:
如下所示在 Linux 內核中,使用 skb_is_nonlinear 函數判斷是否存在分片,即通過判斷 data_len 的大小是否爲 0:
static inline bool skb_is_nonlinear(const struct sk_buff *skb)
{
return skb->data_len;
}
- 在沒有開啓分片的報文中,數據包長度在 struct sk_buff 中爲 len 字段的大小,即 data 到 tail 的長度,nf_frags 爲 0,frag_list 爲 NULL。
普通聚合分散 I/O 的報文:
採用聚合分散 I/O 的報文,frag_list 爲 NULL,nf_frags 不等於 0,說明這不是一個普通的分片,而是聚合分散 I/O 的報文。
如下所示:
nr_frags 爲 2,而 frag_list 爲 NULL,說明這不是普通的分片,而是聚合分散 I/O 分片,數量爲 2,這兩個分片指向同一物理分頁,各自在分頁中的偏移和長度分別是 0/S1 和 S1/S2。
FRAGLIST 類型的分散聚合 I/O 的報文:
採用 FRAGLIST 類型的分散聚合 I/O 報文,frag_list 不爲 NULL,nf_frags 等於 0, 數據長度 len 爲 x+S1,data_len 爲 S1,
以上從 struct sk_buff 的四大指針以及操作、非線性區域對套接字緩存 (socket buffer) 進行分析,更多 sk_buff 的分析、實操等將在以後的文章中梳理。
參考:
https://www.daimajiaoliu.com/daima/4794922e8【900405
https://www.365seal.com/y/elnWBbxepr.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8mtw4h2WZQsU8xrQQCDihA