1. 程式人生 > >Linux 網路協議棧開發(一)—— 網路協議棧核心分析

Linux 網路協議棧開發(一)—— 網路協議棧核心分析

1

1.1 傳送端

1.1.1 應用層

(1) Socket

應用層的各種網路應用程式基本上都是通過 Linux Socket 程式設計介面來和核心空間的網路協議棧通訊的。Linux Socket 是從 BSD Socket 發展而來的,它是 Linux 作業系統的重要組成部分之一,它是網路應用程式的基礎。從層次上來說,它位於應用層,是作業系統為應用程式設計師提供的 API,通過它,應用程式可以訪問傳輸層協議。

  • socket 位於傳輸層協議之上,遮蔽了不同網路協議之間的差異
  • socket 是網路程式設計的入口,它提供了大量的系統呼叫,構成了網路程式的主體
  • 在Linux系統中,socket 屬於檔案系統的一部分,網路通訊可以被看作是對檔案的讀取,使得我們對網路的控制和對檔案的控制一樣方便。

2

3

 UDP socket 處理過程 (來源

 21

TCP Socket 處理過程(來源

(2) 應用層處理流程

  1. 網路應用呼叫Socket API socket (int family, int type, int protocol) 建立一個 socket,該呼叫最終會呼叫 Linux system call socket() ,並最終呼叫 Linux Kernel 的 sock_create() 方法。該方法返回被建立好了的那個 socket 的 file descriptor。對於每一個 userspace 網路應用建立的 socket,在核心中都有一個對應的 struct socket和 struct sock。其中,struct sock 有三個佇列(queue),分別是 rx , tx 和 err,在 sock 結構被初始化的時候,這些緩衝佇列也被初始化完成;在收據收發過程中,每個 queue 中儲存要傳送或者接受的每個 packet 對應的 Linux 網路棧 sk_buffer 資料結構的例項 skb。
  2. 對於 TCP socket 來說,應用呼叫 connect()API ,使得客戶端和伺服器端通過該 socket 建立一個虛擬連線。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連線。預設地,該 API 會等到 TCP 握手完成連線建立後才返回。在建立連線的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因為 UDP 是面向無連線的協議,因此它是不需要該步驟的。
  3. 應用呼叫 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端
  4. sock_sendmsg 被呼叫,它使用 socket descriptor 獲取 sock struct,建立 message header 和 socket control message
  5. _sock_sendmsg 被呼叫,根據 socket 的協議型別,呼叫相應協議的傳送函式。
    1. 對於 TCP ,呼叫 tcp_sendmsg 函式。
    2. 對於 UDP 來說,userspace 應用可以呼叫 send()/sendto()/sendmsg() 三個 system call 中的任意一個來發送 UDP message,它們最終都會呼叫核心中的 udp_sendmsg() 函式。

4

1.1.2 傳輸層

傳輸層的最終目的是向它的使用者提供高效的、可靠的和成本有效的資料傳輸服務,主要功能包括 (1)構造 TCP segment (2)計算 checksum (3)傳送回覆(ACK)包 (4)滑動視窗(sliding windown)等保證可靠性的操作。TCP 協議棧的大致處理過程如下圖所示:

5

TCP 棧簡要過程:

  1. tcp_sendmsg 函式會首先檢查已經建立的 TCP connection 的狀態,然後獲取該連線的 MSS,開始 segement 傳送流程。
  2. 構造 TCP 段的 playload:它在核心空間中建立該 packet 的 sk_buffer 資料結構的例項 skb,從 userspace buffer 中拷貝 packet 的資料到 skb 的 buffer。
  3. 構造 TCP header。
  4. 計算 TCP 校驗和(checksum)和 順序號 (sequence number)。
    1. TCP 校驗和是一個端到端的校驗和,由傳送端計算,然後由接收端驗證。其目的是為了發現TCP首部和資料在傳送端到接收端之間發生的任何改動。如果接收方檢測到校驗和有差錯,則TCP段會被直接丟棄。TCP校驗和覆蓋 TCP 首部和 TCP 資料。
    2. TCP的校驗和是必需的
  5. 發到 IP 層處理:呼叫 IP handler 控制代碼 ip_queue_xmit,將 skb 傳入 IP 處理流程。

UDP 棧簡要過程:

  1. UDP 將 message 封裝成 UDP 資料報
  2. 呼叫 ip_append_data() 方法將 packet 送到 IP 層進行處理。

1.1.3 IP 網路層 – 新增header 和 checksum,路由處理,IP fragmentation

網路層的任務就是選擇合適的網間路由和交換結點, 確保資料及時傳送。網路層將資料鏈路層提供的幀組成資料包,包中封裝有網路層包頭,其中含有邏輯地址資訊- -源站點和目的站點地址的網路地址。其主要任務包括 (1)路由處理,即選擇下一跳 (2)新增 IP header(3)計算 IP header checksum,用於檢測 IP 報文頭部在傳播過程中是否出錯 (4)可能的話,進行 IP 分片(5)處理完畢,獲取下一跳的 MAC 地址,設定鏈路層報文頭,然後轉入鏈路層處理。

IP 頭:

6

IP 棧基本處理過程如下圖所示:

7

  1. 首先,ip_queue_xmit(skb)會檢查skb->dst路由資訊。如果沒有,比如套接字的第一個包,就使用ip_route_output()選擇一個路由。
  2. 接著,填充IP包的各個欄位,比如版本、包頭長度、TOS等。
  3. 中間的一些分片等,可參閱相關文件。基本思想是,當報文的長度大於mtu,gso的長度不為0就會呼叫 ip_fragment 進行分片,否則就會呼叫ip_finish_output2把資料傳送出去。ip_fragment 函式中,會檢查 IP_DF 標誌位,如果待分片IP資料包禁止分片,則呼叫 icmp_send()向傳送方傳送一個原因為需要分片而設定了不分片標誌的目的不可達ICMP報文,並丟棄報文,即設定IP狀態為分片失敗,釋放skb,返回訊息過長錯誤碼。
  4. 接下來就用 ip_finish_ouput2 設定鏈路層報文頭了。如果,鏈路層報頭快取有(即hh不為空),那就拷貝到skb裡。如果沒,那麼就呼叫neigh_resolve_output,使用 ARP 獲取。

1.1.4 資料鏈路層 

 功能上,在物理層提供位元流服務的基礎上,建立相鄰結點之間的資料鏈路,通過差錯控制提供資料幀(Frame)在通道上無差錯的傳輸,並進行各電路上的動作系列。資料鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的作用包括:實體地址定址、資料的成幀、流量控制、資料的檢錯、重發等。在這一層,資料的單位稱為幀(frame)。資料鏈路層協議的代表包括:SDLC、HDLC、PPP、STP、幀中繼等。

實現上,Linux 提供了一個 Network device 的抽象層,其實現在 linux/net/core/dev.c。具體的物理網路裝置在裝置驅動中(driver.c)需要實現其中的虛擬函式。Network Device 抽象層呼叫具體網路裝置的函式。

8

1.1.5 物理層 – 物理層封裝和傳送

9

  1. 物理層在收到傳送請求之後,通過 DMA 將該主存中的資料拷貝至內部RAM(buffer)之中。在資料拷貝中,同時加入符合乙太網協議的相關header,IFG、前導符和CRC。對於乙太網網路,物理層傳送採用CSMA/CD,即在傳送過程中偵聽鏈路衝突。
  2. 一旦網絡卡完成報文傳送,將產生中斷通知CPU,然後驅動層中的中斷處理程式就可以刪除儲存的 skb 了。

1.1.6 簡單總結

10 (來源

1.2 接收端

1.2.1 物理層和資料鏈路層

1122

簡要過程:

  1. 一個 package 到達機器的物理網路介面卡,當它接收到資料幀時,就會觸發一箇中斷,並將通過 DMA 傳送到位於 linux kernel 記憶體中的 rx_ring。
  2. 網絡卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程式主要進行以下一些操作,包括分配 skb_buff 資料結構,並將接收到的資料幀從網路介面卡I/O埠拷貝到skb_buff 緩衝區中;從資料幀中提取出一些資訊,並設定 skb_buff 相應的引數,這些引數將被上層的網路協議使用,例如skb->protocol;
  3. 終端處理程式經過簡單處理後,發出一個軟中斷(NET_RX_SOFTIRQ),通知核心接收到新的資料幀。
  4. 核心 2.5 中引入一組新的 API 來處理接收的資料幀,即 NAPI。所以,驅動有兩種方式通知核心:(1) 通過以前的函式netif_rx;(2)通過NAPI機制。該中斷處理程式呼叫 Network device的 netif_rx_schedule 函式,進入軟中斷處理流程,再呼叫 net_rx_action 函式。
  5. 該函式關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。
  6. netif_receive_skb 是鏈路層接收資料報的最後一站。它根據註冊在全域性陣列 ptype_all 和 ptype_base 裡的網路層資料報型別,把資料報遞交給不同的網路層協議的接收函式(INET域中主要是ip_rcv和arp_rcv)。該函式主要就是呼叫第三層協議的接收函式處理該skb包,進入第三層網路層處理。

1.2.2 網路層

12   23

  1. IP 層的入口函式在 ip_rcv 函式。該函式首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合併),然後 packet 呼叫已經註冊的 Pre-routing netfilter hook ,完成後最終到達 ip_rcv_finish 函式。
  2. ip_rcv_finish 函式會呼叫 ip_router_input 函式,進入路由處理環節。它首先會呼叫 ip_route_input 來更新路由,然後查詢 route,決定該 package 將會被髮到本機還是會被轉發還是丟棄:
    1. 如果是發到本機的話,呼叫 ip_local_deliver 函式,可能會做 de-fragment(合併多個 IP packet),然後呼叫 ip_local_deliver 函式。該函式根據 package 的下一個處理層的 protocal number,呼叫下一層介面,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對於 TCP 來說,函式 tcp_v4_rcv 函式會被呼叫,從而處理流程進入 TCP 棧。
    2. 如果需要轉發 (forward),則進入轉發流程。該流程需要處理 TTL,再呼叫 dst_input 函式。該函式會 (1)處理 Netfilter Hook (2)執行 IP fragmentation (3)呼叫 dev_queue_xmit,進入鏈路層處理流程。

13  24

1.2.3 傳輸層 (TCP/UDP)

  1. 傳輸層 TCP 處理入口在 tcp_v4_rcv 函式(位於 linux/net/ipv4/tcp ipv4.c 檔案中),它會做 TCP header 檢查等處理。
  2. 呼叫 _tcp_v4_lookup,查詢該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。
  3. 如果socket 和 connection 一切正常,呼叫 tcp_prequeue 使 package 從核心進入 user space,放進 socket 的 receive queue。然後 socket 會被喚醒,呼叫 system call,並最終呼叫 tcp_recvmsg 函式去從 socket recieve queue 中獲取 segment。

1.2.4 接收端 – 應用層

  1. 每當使用者應用呼叫  read 或者 recvfrom 時,該呼叫會被對映為/net/socket.c 中的 sys_recv 系統呼叫,並被轉化為 sys_recvfrom 呼叫,然後呼叫 sock_recgmsg 函式。
  2. 對於 INET 型別的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被呼叫,它會呼叫相關協議的資料接收方法。
  3. 對 TCP 來說,呼叫 tcp_recvmsg。該函式從 socket buffer 中拷貝資料到 user buffer。
  4. 對 UDP 來說,從 user space 中可以呼叫三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統呼叫最終都會呼叫核心中的 udp_recvmsg 方法。

1.2.5 報文接收過程簡單總結

14

 2. Linux sk_buff struct 資料結構和佇列(Queue)

2.1 sk_buff

2.1.1 sk_buff 是什麼

當網路包被核心處理時,底層協議的資料被傳送更高層,當資料傳送時過程反過來。由不同協議產生的資料(包括頭和負載)不斷往下層傳遞直到它們最終被髮送。因為這些操作的速度對於網路層的表現至關重要,核心使用一個特定的結構叫 sk_buff, 其定義檔案在 skbuffer.h。Socket buffer被用來在網路實現層交換資料而不用拷貝來或去資料包 –這顯著獲得速度收益。

  • sk_buff 是 Linux 網路的一個核心資料結構,其定義檔案在 skbuffer.h
  • socket kernel buffer (skb) 是 Linux 核心網路棧(L2 到 L4)處理網路包(packets)所使用的 buffer,它的型別是 sk_buffer。簡單來說,一個 skb 表示 Linux 網路棧中的一個 packet;TCP 分段和 IP 分組生產的多個 skb 被一個 skb list 形式來儲存。
  • struct sock 有三個 skb 佇列(sk_buffer queue),分別是 rx , tx 和 err。

15

它的主要結構成員:

複製程式碼
struct sk_buff {
    /* These two members must be first. */ # packet 可以存在於 list 或者 queue 中,這兩個成員用於連結串列處理
    struct sk_buff        *next;
    struct sk_buff        *prev;
    struct sk_buff_head    *list; #該 packet 所在的 list
 ...
    struct sock        *sk;      #跟該 skb 相關聯的 socket
    struct timeval        stamp; # packet 傳送或者接收的時間,主要用於 packet sniffers
    struct net_device    *dev;  #這三個成員跟蹤該 packet 相關的 devices,比如接收它的裝置等
    struct net_device    *input_dev;
    struct net_device    *real_dev;

    union {                  #指向各協議層 header 結構
        struct tcphdr    *th;
        struct udphdr    *uh;
        struct icmphdr    *icmph;
        struct igmphdr    *igmph;
        struct iphdr    *ipiph;
        struct ipv6hdr    *ipv6h;
        unsigned char    *raw;
    } h;

    union {
        struct iphdr    *iph;
        struct ipv6hdr    *ipv6h;
        struct arphdr    *arph;
        unsigned char    *raw;
    } nh;

    union {
        unsigned char    *raw;
    } mac;

    struct  dst_entry    *dst; #指向該 packet 的路由目的結構,告訴我們它會被如何路由到目的地
    char            cb[40];    # SKB control block,用於各協議層儲存私有資訊,比如 TCP 的順序號和幀的重發狀態
    unsigned int        len, #packet 的長度
                data_len,
                mac_len,       # MAC header 長度
                csum;          # packet 的 checksum,用於計算儲存在 protocol header 中的校驗和。傳送時,當 checksum offloading 時,不設定;接收時,可以由device計算

    unsigned char        local_df, #用於 IPV4 在已經做了分片的情況下的再分片,比如 IPSEC 情況下。
                cloned:1, #在 skb 被 cloned 時設定,此時,skb 各成員是自己的,但是資料是shared的
                nohdr:1,  #用於支援 TSO
                pkt_type, #packet 型別
                ip_summed; # 網絡卡能支援的校驗和計算的型別,NONE 表示不支援,HW 表示支援,

    __u32            priority; #用於 QoS
    unsigned short        protocol, # 接收 packet 的協議
                security;
複製程式碼

2.1.2 skb 的主要操作

(1)分配 skb = alloc_skb(len, GFP_KERNEL)

16

(2)新增 payload (skb_put(skb, user_data_len))

17

(3)使用 skb->push 新增 protocol header,或者 skb->pull 刪除 header

18

  2.2 Linux 網路棧使用的驅動佇列 (driver queue)

2.2.1 佇列

19

在 IP 棧和 NIC 驅動之間,存在一個 driver queue (驅動佇列)。典型地,它被實現為 FIFO ring buffer,簡單地可以認為它是固定大小的。這個佇列不包含 packet data,相反,它只是儲存 socket kernel buffer (skb)的指標,而 skb 的使用如上節所述是貫穿核心網路棧處理過程的始終的。

該佇列的輸入時 IP 棧處理完畢的 packets。這些packets 要麼是本機的應用產生的,要麼是進入本機又要被路由出去的。被 IP 棧加入佇列的 packets 會被網路裝置驅動(hardware driver)取出並且通過一個數據通道(data bus)發到 NIC 硬體裝置並傳輸出去。

在不使用 TSO/GSO 的情況下,IP 棧發到該佇列的 packets 的長度必須小於 MTU。

2.2.2 skb 大小 – 預設最大大小為 NIC MTU

絕大多數的網絡卡都有一個固定的最大傳輸單元(maximum transmission unit, MTU)屬性,它是該網路裝置能夠傳輸的最大幀(frame)的大小。對乙太網來說,預設值為 1500 bytes,但是有些乙太網絡可以支援巨幀(jumbo frame),最大能到 9000 bytes。在 IP 網路棧內,MTU 表示能發給 NIC 的最大 packet 的大小。比如,如果一個應用向一個 TCP socket 寫入了 2000 bytes 資料,那麼 IP 棧需要建立兩個 IP packets 來保持每個 packet 的大小等於或者小於 1500 bytes。可見,對於大資料傳輸,相對較小的 MTU 會導致產生大量的小網路包(small packets)並被傳入 driver queue。這成為 IP 分片 (IP fragmentation)。

下圖表示 payload 為 1500 bytes 的 IP 包,在 MTU 為 1000 和 600 時候的分片情況:

20