簡說 套接字快取的記憶體空間佈局

語言: CN / TW / HK

目前已經寫過以下關於Linux核心網路原始碼相關的分享:

在以上文章中,沒有分析過Linux核心網路關鍵的資料結構-套接字資料快取 struct sk_buff ,本文將第一次分享到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中關於長度的兩個欄位

  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;
}
  • 在沒有開啟分片的報文中,資料包長度在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/4794922e8900405

https://www.365seal.com/y/elnWBbxepr. html

END