前言
今天來聊下sk_buff
。如果把內核網絡協議棧比作一個人,那麼sk_buff
就是流淌在他體內血管裏紅血球,它運輸養分(數據)走遍全身(協議棧每一層)。一個sk_buff
就是一個報文。
報文的表示方式
在協議棧層次模型中,我們提到過,報文在協議棧各層之間穿梭,發送方向不斷加上Header
,接收方向不斷去掉Header
。自然而然,我們要設計數據結構去表示在各個層上形態各異的報文
我們可以這麼設計IP
報文:
struct ip_pdu{
struct ip_hdr ip_hdr;
char* payload;
}
那TCP
報文呢, 就像下面這樣
struct tcp_pdu{
struct tcp_hdr tcp_hdr;
char* payload;
}
然後層與層之間進行報文數據結構的拷貝和轉換。
等等!網絡協議那麼多,難道要每種協議設計一個新的數據結構?!另外,像這樣每個報文都要拷貝多次的話效率也太低了吧?!
所以,linux採用的報文層間傳遞的方式就是一個結構,傳遞指針,這個結構就是sk_buff
, 也就是說, 無論是哪個層次的報文, 在內核中始終都以sk_buff
表示(sk_buff
是 socket buffer
的簡稱)
下面是sk_buff
結構的構成(精簡後)
<skbuff.h>
typedef unsigned char *sk_buffer_data_t;
struct sk_buff{
struct sock *sk;
struct net_device *dev;
char cb[40];
sk_buff_data_t transport_header;
sk_buff_data_t network_header;
sk_buff_data_t mac_header;
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head;
*data;
}
重點關注最後四個字段,準確的說是四個指針,
如上圖所示,這四個指針的外面兩個限定了一個大的緩衝區,而中間兩個則是包裹了有效數據,所謂有效數據便是報文。這麼設計的原因就是避免拷貝! 以發送方向爲例,我們知道發送過程是一層一層的在報文前方貼Header
,因此,既然我們知道隨着報文向下傳遞時,長度會增加。那麼我們不如就在最初報文(應用層)的前面預留一定的空間,就可以避免向下傳遞時還要重新申請內存再拷貝了。
上圖描述了在接收方向報文上送時的指針移動方向,之前說的去掉Header
就是指這個。
控制字段cb
是一個很有意思的字段,它是一個雜貨間,沒有標準的格式,因此可以存放各種數據,最多40個字節。當報文在協議棧中流動時,流到哪一層,哪一層就將自己的私有數據放到這個區域。比如TCP
用這個區域儲存收到的TCP
報文頭中的一些字段。
struct tcp_skb_cb{
......
u32 seq; /* Starting sequence number */
u32 end_seq; /* SEQ + FIN + SYN + dalalen */
u32 when; /* used to compute rtt */
u32 flags /* TCP header flags */
}
而struct sock *sk
和struct net_device *dev
則分別代表了sk_buff
的起點和終點。對於發送方向,報文由應用層產生,自然是通過一個套接字到內核,最終從一個設備發送出去,這時sk
是起點,dev
是終點。而對於接收方向,報文由一個設備接收,最終被應用層通過一個套接字拿到,這時,這時dev
是起點,sk
是終點.
總結
- 協議棧使用
sk_buff
表示所有報文,sk_buff
在協議棧中傳遞時以指針傳遞並且不會拷貝 cb
是一個雜貨間,每一層都可以使用sk
和dev
是報文的起點和終點,對發送方向和接收方向來說正好相反。