一、數據包管理
TCP/IP 是一種數據通信機制,因此,協議棧的實現本質上就是對數據包進行處理。 數據包管理應該能提供一種高效的機制,使協議棧各層能對數據包進行靈活的處理,同時減少數據在各層間傳遞時的時間與空間開銷,這是提高協議棧工作效率的關鍵點。 在 LwIP 中,也有個類似的結構,稱之為 pbuf,本章所有討論將圍繞 pbuf 而展開。
1、數據包結構pbuf
在 LwIP 中,文件 pbuf.h和 pbuf.c 實現了協議棧數據包管理相關的所有數據結構和函數。結構 pbuf 的定義如下:
————pbuf.h———————————————————— struct pbuf
{ struct pbuf *next; //構成 pbuf 鏈表時指向下一個 pbuf 結構 void * payload; //數據指針,指向該 pbuf 所記錄的數據區域 u16_t tot_len; //當前 pbuf 及其后續所有 pbuf 中包含的數據總長度 u16_t len; //當前 pbuf 的數據的長度 u8_t type; //當前 pbuf 的類型 u8_t flags; //狀態位,未用到 u16_t ref; //指向該 pbuf 的指針數,即該 pbuf 被引用的次數 };
tot_len 表示當前 pbuf 和其后所有 pbuf 的有效數據的總長度。pbuf 鏈表中第一個 pbuf 的 tot_len 字段表示整個數據包的長度,而最后一個 pbuf 的 tot_len 字段必同 len字段相等 。
2、pbuf的類型
pbuf有4類:PBUF_RAM、PBUF_ROM、PBUF_REF、PBUF_POOL系統中使用了一個專門的枚舉類型 pbuf_type 來描述它們:
————pbuf.h—————————————————— typedef enum
{ PBUF_RAM, //pbuf 描述的數據在 pbuf 結構之后的連續內存堆中 PBUF_ROM, //pbuf 描述的數據在 ROM 中 PBUF_REF, //pbuf 描述的數據在 RAM 中,但位置與 pbuf 結構所處位置無關 PBUF_POOL // pbuf 結構與其描述的數據處於同一內存池中 } pbuf_type; ————————————————————————————————
PBUF_RAM 類型的 pbuf 空間是通過內存堆分配得到的。這種類型的 pbuf 在協議棧中是使用得最多的,協議棧的待發送數據和應用程序的待發送數據一般都采用這個形式。申請 PBUF_RAM類型 pbuf 時,協議棧會在內存堆中分配相應空間。 下面來看看源代碼是怎樣申請PBUF_RAM 型的,在后續講解函數源代碼時也會詳細說到。
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length));
分配成功的 PBUF_RAM 類型 pbuf 如圖 71 所示。
從圖中可看出 pbuf 結構和相應數據在一片連續的內存區域中,注意 payload 並沒有指向整個數據區的起始處,而是間隔了一定區域。這段區域就是上面的 offset,它通常用來存儲數據包的各種首部字段,如 TCP 報文首部、IP 首部、以太網幀首部等。
PBUF_POOL 類型和 PBUF_RAM 類型的 pbuf 有很大的相似之處,但它的空間是通過內存池分配得到的。這種類型的 pbuf 可以在極短的時間內得到分配(得益於內存池的優點),在網卡接收數據包時,我們就使用了這種方式包裝數據。在申請 PBUF_POOL 類型 pbuf 時,協議棧會在內存池 MEMP_PBUF_POOL 中選擇一個或多個POOL,以滿足用戶空間大小的申請。源代碼是通過下面一條語句來完成 POOL 申請的,其中 p 是pbuf 型指針。
p = memp_malloc(MEMP_PBUF_POOL);
通常,用戶發送的數據可能很長,所以系統會多次調用上面的語句,為用戶分配多個 POOL,並把它們按照 pbuf 鏈表的形式組織在一起,以保證用戶的空間請求要求。分配成功的 PBUF_POOL 類型 pbuf 示意如圖 72所示。
PBUF_ROM 和 PBUF_REF 類型的 pbuf 基本相同,它們的申請都是在內存池中分配一個相應的 pbuf結構(即 MEMP_PBUF 類型的 POOL),而不申請數據區的空間 在發送某些靜態數據時,可以采用這兩種類型的 pbuf,這將大大節省協議棧的內存空間。下面來看看源代碼是怎樣申請 PBUF_ROM 和PBUF_REF 類型的,其中 p 是 pbuf 型指針。
3、數據包申請函數
數據包申請函數有兩個重要的參數,一是想申請的數據包 pbuf 類型,這個剛說過了,不啰嗦;另一個重要參數是該數據包是在協議棧中哪一層被申請的,分配函數會根據這個層次的不同,在 pbuf 數據區域前為相應的協議預留出首部空間,這就是前面所說的 offset 值了。總的來說,LwIP 定義了四個層次,當數據包申請時,所處的層次不同,會導致預留空間的 offset值不同。層次的定義是通過一個枚舉類型 pbuf_layer 來實現的,如下代碼所示:
————pbuf.h—————————————— #define PBUF_TRANSPORT_HLEN 20 //TCP 報文首部長度 #define PBUF_IP_HLEN 20 //IP 數據報首部長度 typedef enum
{ PBUF_TRANSPORT, //傳輸層 PBUF_IP, //網絡層 PBUF_LINK, //鏈路層 PBUF_RAW //原始層,不預留任何空間 } pbuf_layer;
PBUF_TRANSPORT_HLEN 和 PBUF_IP_HLEN,前者是典型的 TCP 報文首部長度,而后者是典型的不帶任何選項字段的 IP 首部長度
//參數 layer,指定該 pbuf 數據所處的層次,分配函數根據該值在 pbuf 數據區預留出 //首部空間;length 表示需要申請的數據區長度,type 指出需要申請的 pbuf 類型 struct pbuf *pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
p = pbuf_alloc(PBUF_RAW, packetLength, PBUF_POOL);
這個調用語句申請了一個 PBUF_POOL 類型的 pbuf,且其申請的協議層為 PBUF_RAW,所以pbuf_alloc 函數不會在數據區前預留出任何首部空間;通過使用 p>payload,就可以實現對 pbuf 中數據區的讀取或寫入操作了。
在 TCP 層要申請一個數據包時,常常調用下面的語句:
p = pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)
它告訴數據包分配函數,使用 PBUF_RAM 類型的 pbuf,且數據區前應該預留一部分的首部空間,由於這里是 PBUF_TRANSPORT 層,所以預留空間將有 54 字節,即 TCP 首部長度PBUF_TRANSPORT_HLEN(20 字節)、IP 數據包首部長度 PBUF_IP_HLEN(20 字節)以及以太網幀首部長度(14 字節)。當數據包往下層遞交,各層協議就可以直接操作這些預留空間中的數據,以實現數據包首部的填寫,這樣就避免了數據的拷貝。
4、數據包釋放函數
假如現在我們的 pbuf 鏈表由 A,B,C 三個 pbuf 結構連接起來,結構為 A>B>C,利用 pbuf_free(A)函數來刪除 pbuf 結構,下面用 ABC 的幾組不同 ref值來看看刪除結果:
(1)1>2>3 函數執行后變為 ...1>3,節點 BC 仍在;
(2)3>3>3 函數執行后變為 2>3>3,節點 ABC 仍在;
(3)1>1>2 函數執行后變為......1,節點 C 仍在;
(4)2>1>1 函數執行后變為 1>1>1,節點 ABC 仍在;
(5)1>1>1 函數執行后變為.......,節點全部被刪除。
假如在上面的第(4)種情況下,錯誤的調用數據包釋放函數,如 pbuf_free(B),這會導致嚴重的錯誤。
//函數的返回值為成功刪除的 pbuf 個數 u8_t pbuf_free(struct pbuf *p)
當可以刪除某個 pbuf 結構時,函數 pbuf_free 首先檢查這個 pbuf 是屬於四個類型中的哪種,根 據類 型的不 同, 調用不 同的 內存釋 放函 數進 行刪 除。
內存池memp_free->MEMP_PBUF_POLL->PBUF_POOL
->MEMP_PBUF->PBUF_ROM
->PBUF_REF
內存堆mem_free->PBUF_RAM
5、其他數據包操作函數
pbuf_realloc 函數在相應 pbuf(鏈表)尾部釋放一定的空間,將數據包 pbuf 中的數據長度減少為某個長度值。對於 PBUF_RAM 類型的 pbuf,函數將調用內存堆管理中介紹到的 mem_realloc 函數,釋放這些多余的空間;對於其他三種類型的 pbuf,該函數只是修改 pbuf 中的長度字段值,並不釋放對應的內存池空間。
pbuf_header 函數用於調整 pbuf 的 payload 指針(向前或向后移動一定的字節數),在前面也說到過了,在 pbuf 的數據區前可能會預留一些協議首部空間,而 pbuf 被創建時,payload 指針是指向數據區的,為了實現對這些預留空間的操作,可以調用函數 pbuf_header 使 payload 指針指向數據區前的首部字段,這就為各層對數據包首部的操作提供了方便。當然,進行這個操作的時候,len和 tot_len 字段值也會隨之更新。
pbuf_take 函數用於向 pbuf 的數據區域拷貝數據;pbuf_copy 函數用於將一個任何類型的 pbuf中的數據拷貝到一個 PBUF_RAM 類型的 pbuf 中。pbuf_chain 函數用於連接兩個 pbuf(鏈表)為一個 pbuf 鏈表;pbuf_ref 函數用於將 pbuf 中的 ref 值加 1。