走進Linux內核網絡 高屋建瓴—stack layer model

前言

作爲本系列的第一篇文章,讓我們首先聊聊協議棧模型(stack layer model)。還記得在大學課堂裏的時候學TCP/IP的時候,我其實並不能分清楚ISO/OSI分層模型和TCP/IP分層模型二者的區別. 就是下面這幅圖
在這裏插入圖片描述
可以看出,雖然層次劃分方式有區別,但大體上還是能對應上的。那麼,爲什麼要這麼分層?

爲什麼要分層

分層的好處在於,每一層可以各司其職,只關心自己這一層的功能,而不去關心上面下面需要做什麼。每一層使用下面提供的接口,同時爲上一層提供服務。每一層可能有多種實現,比如傳輸層(Transport)可以使用TCP,也可以使用UDP,它們都可以使用IP作爲下層網絡層(Network)的實現。他們就像樂高積木一樣, 你可以將不同的積木拼接起來.

報文在層間穿梭

這樣說或許還比較抽象,我們來理一理報文在協議棧中的穿梭流程

以一個標準的使用TCP/IPHTTP報文爲例
在這裏插入圖片描述
它由多個Header和一個Data部分組成,這些Header就是協議棧中各層對在報文上留下的痕跡,越上層Header越靠近Data

發送方向

發送數據時,應用層在原始數據前面貼上HTTP Header,結果作爲整體下發給傳輸層(TCP),TCP纔不管報文的組成,對它來說,現在的報文都將作爲TCPPayload。然後TCP在現在的報文前面貼上TCP Header,再作爲payload下發給下層(IP). 以此類推,報文前方又貼上了IP HeaderMAC Header,最終從實際設備發送出去了。

那麼問題來了,爲什麼在上面的過程中,報文走的是傳輸層走的是TCP而不是UDP,應用層走的是IP而不是其他網絡層實現 ,最後貼的以太頭MAC是貼的什麼?

答案是,在應用層其實就指定好了。有過一點socket編程經驗應該知道,創建套接字的的接口是

int socket(int domain, int type, int protocol)

這裏的domainprotocol就決定了網絡層用哪個,比如IP其他。
type決定了傳輸層使用什麼,比如是TCP還是UDP

由此可見, 向外發送的報文通過傳輸層和網絡層的路徑在socket創建的時候就已經決定好了.
MAC Header中的源MAC是根據實際發送報文的出接口決定的,這會涉及一些路由相關的東西,比如對於一臺有兩個網卡的電腦, 每個網卡有自己的MAC地址, 路由過程會根據目的地選擇其中一個網卡作爲報文的實際出接口,那麼MAC Header中的SMAC就是該網卡的MAC地址

layer
至此, 我們就可以描繪出發送過程報文在內核協議棧中的穿梭路徑了

接收方向

再來看接收方向, 當報文由網卡驅動程序遞交給協議棧時, 就要開始它的協議棧旅程了. 和發送過程正好相反, 發送是一層一層貼Header, 接收是一層一層脫Header了.

還是上面的例子, 假設現在設備驅動程序收到完整的HTTP 以太幀了, 將報文上送給協議棧, 協議棧最下面的鏈路層(姑且這麼稱呼) 拿到報文了, 那麼他是要將其上送給網絡層 (比如IP ) ? 還是 ARP或者 RARP ?

回想上面的報文圖, 強調一下: 由於層與層是各司其職的, 所以這裏鏈路層只能看到MAC Header和後面的Payload of Ethernet Frame, 而且, 它只能去理解前者, 對於後者, 它根本無法理解. 所以它必須從MAC Header就能分析出該把這個報文給誰.

2

顯然, Dest MACSrc MAC是用來描述設備地址的, 那麼答案很明顯了, 只有type字段。type字段描述了該以太幀的類型, 比如IP就是0x0800, ARP0x0806 也就是說,當我解析到報文type0x0800時, 就要將報文脫去MAC Header後, 扔給IP.

IP收到後怎麼辦呢, 在進行自己的處理之後, 它需要把報文上送給上層(傳輸層) . 給傳輸層誰呢, 是TCP還是UDP還是其他 ? 一樣的道理, 這個信息就在IP Header裏, 下面是IPv4版本的Header (IPv6一樣的道理)

在這裏插入圖片描述
看! 關鍵就是protocol字段, 它指明瞭該使用的傳輸層協議. 比如TCP6 , UDP17. 在我們上面的例子中, 收到報文中這個字段一定是 6, 所以IP將報文脫掉IP Header後就會上送給TCP.

TCP收到後怎麼辦呢, 首先是自己的處理(這部分內容很多, 但我不打算在本文中談), 最後, TCP會將報文上送給應用層. 一臺主機上,使用TCP的應用可能有很多,那麼TCP如何正確分發呢。 還是一樣,祕密就在TCP Header中。
在這裏插入圖片描述

在一臺主機上,每個應用程序使用的端口號是不同的,正是通過端口號的唯一性,TCP就能知道該送給哪個應用層。從這裏可以推斷出, 內核一定是有一張類似表的結構,記錄了使用的端口號和對應應用程序的信息。其實這個信息正是保存在內核的socket結構裏的。在發送過程中,我們沒有關注TCP Header中的src portdst port,但其實 這兩個端口號也是保存在內核socket結構中,TCP在填寫TCP Header時, 也正是根據它.

所以, TCP根據Header中的dst port, 會去看哪個socket結構上的src port與它一致, 一致就說明應該這個報文脫掉TCP Header後丟給這個socket,而應用層程序就可以通過這個socket讀取到數據了

鬆耦合

內核協議棧層與層之間是鬆耦合的. 比如對於網絡層IP來說, 它的報文上送代碼一定不是這樣:

ip_local_deliver(skb)
{
    protocol = get_protocol(skb)
    switch (protocol):
    case TCP: 
        tcp_rcv(skb);
        break;
    case UDP:
        udp_rcv(skb);
        break;
    ......
}

這樣並不是不能工作, 只是如果這樣, 當新添加一種傳輸層協議時, 還要在IP的代碼裏新加一個case, 這就不合理了. 所以, 內核採用的辦法是, 各種傳輸層實現一組形式一樣的接口, 註冊到內核中, IP根據Header中的protocol, 一個一個看這個protocol是否已經註冊了, 找到了就調用註冊的接收函數. 像這樣:

static struct net_protocol tcp_protocol {
   .handler = tcp_v4_rcv,
}
static struct net_protocol udp_protocol {
   .handler = udp_v4_rcv,
}
{
    inet_add_protocol(&tcp_protocol, 6)
    inet_add_protocol(&tcp_protocol, 17)
}

ip_local_deliver(skb)
{
    protocol = get_protocol(skb) // TCP報文會返回 6  UDP報文會返回 17
    transport = get(protocol);  //  TCP報文會返回 tcp_protocol UDP報文會返回 udp_protocol
    transport->handler(skb); 
}

這裏的struct net_protocol就是每種傳輸層實現需要實現的接口. 這種統一的形式(比如上面都實現了handler函數)使得IP可以不用管系統中究竟有實現了哪些傳輸層.後續即使添加新的傳輸層實現, IP的邏輯也不用變. 這就是鬆耦合帶來的好處.

上面只是網絡層<->傳輸層的例子, 實際上, 協議棧的層與層之間都是以這種形式工作的. 這也是內核網絡的設計美學.

總結

  1. 協議棧劃分了不同的層次,每個層次都有多種實現. 無論是發送還是接收,報文都按順序經過每一層的一種實現
  2. 發送過程穿梭路徑在套接字創建的時候就已經確定了, 層層貼頭
  3. 接收過程層層去頭, 頭中的信息指明瞭報文該送到上層哪個實現
  4. 協議棧層與層之間的鬆耦合體現了協議棧的設計美學
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章