簡說 套接字緩存的內存空間佈局

本文將分享到 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 指針移動,進行頭部協議的封裝。

可以看到在數據包封裝的過程中,每一層移動 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 的四大指針的相關操作, 通過分析可得:

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 的用法:

判斷是否存在非線性緩衝區:

  1. len 字段:無分片的報文,數據報文的大小

  2. 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;
}

普通聚合分散 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