1. 程式人生 > 其它 >使用者態協議棧分析

使用者態協議棧分析

一、前言

  在講網路協議棧前,先理解一個數據包在網路傳輸是一個怎麼樣的流程,如下圖所示。

  正常的流程是網絡卡接收到資料後,把資料copy到協議棧(sk_buff),協議棧把sk_buff資料解析完後再把資料放到recv_buff,此時應用程式呼叫recv把資料從協議棧copy到應用程式;傳送資料包,則與之相反,應用程式呼叫send把資料包copy到send_buff,協議棧從send_buff取資料放到sk_buff,交給網絡卡傳送出去。這個過程有多次拷貝,為避免多次拷貝,使用dma的方式(零拷貝),把網絡卡的資料直接對映到記憶體,再由應用程式訪問記憶體。

二、資料包分析

  從網絡卡接收到一幀完整的資料包,可以使用原生的socket、netmap、dpdk等,完整的一幀資料由乙太網頭、IP頭、tcp/udp頭、使用者資料構成,這些層級涉及到7層網路模型OSI,如udp協議分佈到7層OSI如下圖所示。

  由上圖可知,乙太網頭屬於鏈路層、IP頭屬於網路層、UDP頭屬於傳輸層,而實際的使用者資料在應用層。

  (1)乙太網頭

    乙太網頭分佈如下圖所示。

    對應結構體如下,由此可知MAC地址存在乙太網頭。

#define ETH_LEN 6

//14位元組乙太網頭---->鏈路層---->MAC地址,
struct ethhdr{
    unsigned char dst[ETH_LEN];//6位元組 目的地址即MAC地址
    unsigned char src[ETH_LEN];//6位元組 源地址
    unsigned short proto;//2位元組 協議型別,形容網路層使用的協議
} //在計算上沒有那個韌體叫MAC地址,IP地址、埠 //所謂的MAC地址,IP地址、埠只不過是協議棧裡面一個欄位名而已,不要與韌體捆綁

  (2)IP頭

    IP頭結構如下圖所示。

    其資料結構如下所示,IP地址在IP頭中,屬於網路層。

//iphdr(ip頭)---->網路層--->IP地址
struct iphdr{
    unsigned char version:4,//4位版本
                    hdrlen:4;//4位首部長度
    unsigned char tos;//8位服務型別
    unsigned short totlen;//16位總長度,有65535也就是說一次可傳64k,注意MTU是1500這是網絡卡的限制,在網絡卡傳輸資料是它會分片傳送,一個片就是一個MTU
unsigned short id;//16位標識,每一個數據包都有一個id,與tcp裡面的seq num沒有關係 unsigned short flag:3, offset:13; unsigned char ttl;// ttl = 64 - 路由數量/閘道器,當ttl為0,就會返回無法訪問目標地址,不可達 unsigned char proto;//8位協議型別,形容傳輸層使用什麼協議 unsigned short check;//16位首部校驗和,計算的是首部的校驗和,計算校驗前一定要賦值為0,再計算,否則接收端無法收到資料 unsigned int sip;//源ip unsigned int dip;//目的ip }

  (3)協議頭

    該層涉及到具體的不同協議,就有不同的結構,本文主要分析udp和tcp

    (a)udp 協議結構如圖所示。

    其資料結構如下,由此可知埠在協議頭裡面,屬於傳輸層。

//udp頭(8個位元組頭)---->傳輸層--->埠
struct udphdr{
    unsigned short sport;//源埠
    unsigned short dport;//目的埠
    
    unsigned short length;//長度
    unsigned short check;//校驗和
}

   (b) tcp 協議頭如圖所示。

    其資料結構如下,屬於傳輸層。

struct tcphdr{
    unsigned short sport;//源埠
    unsigned short dport;//目的埠
    
    unsigned int seqnum;//序號:包的序號,唯一id,隨機起始值,之後就遞增
    unsigned int acknum;
    
    unsigned char hdrlen:4,//頭長度
                  resv:4;//保留位
                  
    //以下的標識置1,對應的欄位有效
    unsigned char cwr:1,
                  ece:1,
                  urg:1,
                  ack:1,
                  psh:1,
                  rst:1,
                  syn:1,
                  fin:1;
                    
    unsigned short win;//視窗大小
    
    unsigned short check;
    
    unsigned short urg_pointer;
    
}

  (c)arp和icmp

   arp和icmp頭定義如下。

//arp_head
struct arphdr {
    unsigned short h_type;
    unsigned short h_proto;
    unsigned char h_addrlen;
    unsigned char protolen;
    unsigned short oper;
    unsigned char smac[ETH_ALEN];//源mac
    unsigned int sip;//源ip
    unsigned char dmac[ETH_ALEN];//目的mac
    unsigned int dip;//目的ip
};

//icmp_head
struct icmphdr {
    unsigned char type;
    unsigned char code;
    unsigned short check;
    unsigned short identifier;
    unsigned short seq;
    unsigned char data[32];
};

  arp是地址解析協議,在區域網中,每一臺主機都會對區域網內每一臺機器進行廣播arp包,當收到對端主機arp請求包後,把本機的IP和MAC地址做為響應傳送回請求方,發出請求的主機便可獲得整個區域網內所有主機的IP和MAC地址,並儲存到arp表中,記錄著區域網所有機器的IP和MAC地址資訊;當arp表中某臺機器的arp資訊超時後,就會從arp表中刪除,導致收不到資料;所以在區域網內網路通訊看似是通過IP,其實是通過MAC地址。

  ICMP是Internet控制報文協議,在命令列上ping + IP地址,此時傳送的就是向目標主機發送ICMP請求,目標主機收到ICMP求情後,就會響應ICMP,表明兩臺主機的網路是暢通的。

  由以上每個協議頭的定義,得到udp、tcp、arp、icmp它們的協議packet可定義如下。

struct arppkt {
    struct ethhdr eh;
    struct arphdr arp;//arp頭屬於網路層,與IP頭同一層
};

struct icmppkt {
    struct ethhdr eh;
    struct iphdr ip;
    struct icmphdr icmp;
};

struct udppkt {
    struct ethhdr eh;//14
    struct iphdr ip;//20
    struct udphdr udp;//8
        //使用者資料,柔性陣列相當於一個標籤,指向使用者資料的首地址
    unsigned char payload[0];//柔性陣列,使用條件:1.記憶體已經分配好,2.柔性陣列的長度可以通過其它方法計算出來
};
//sizeof(udppkt) = 44,為啥不是42,因為結構體設定了是以1個位元組對齊,導致有一個地方有2個位元組的空窗期

struct tcppkt {
    struct ethhdr eh;
    struct iphdr ip;
    struct tcphdr tcp;
    unsigned char payload[0];
};

三、深入理解網路協議棧

  從網路協議棧是如何實現tcp連線、傳輸資料、斷開連線,經過這3個方面加深對網路協議棧瞭解。

  (1)三次握手

    tcp三次握手流程圖如下。

  客戶端傳送syn包開始第一次握手,服務端收到syn後完成第一次握手;服務端傳送ack包開始第二次握手,acknum等於第一次握手seqnum+1,客戶端收到ack後完成第二次握手,客戶端傳送ack開始第三次握手,acknum等於第二次握手seqnum+1,服務端收到ack後完成第三次握手。

  第一次握手完成時,服務端從IP頭、TCP頭獲取到源IP、目的IP、源埠、目的埠、協議等資訊構成五元組,存到半連線佇列節點中;當第三次握手完成,遍歷半連線佇列找到對應的節點,並把節點移動到全連線佇列中,應用層呼叫accept消費全連線佇列資料,並分配fd,全連線佇列每個節點可以叫tcp控制塊,fd與tcp控制塊一一對應。

  在三次握手過程中存在3個狀態(狀態機):

    (a)listen:伺服器處於listen狀態;
    (b)syn_recv:伺服器接收到資料包之後進入syn_recv狀態;
    (c)established:在接收完資料後進入established狀態。
  以上3個狀態存在tcp控制塊中。

  應用層呼叫listen(fd, backlog),引數backlog有兩種理解,在linux系統中指的是半連線佇列的長度,在unix系統中指半連線佇列和全連線佇列大小之和。

  如果第三次握手ack包丟失,那麼第二次握手會不會重發ack包,答案是不會重發,沒有重發得意義,但是包的超時可以設定。這就引出了超時怎麼計算,客戶端發包到服務端,服務端發包到客戶端,客戶端發包開始記錄一個時間,到客戶端收到包時也記錄一個時間,服務端也類似發包記錄一個時間到收包也記錄一個時間,這個往返的時間叫做RTT,當前RTT往返時間 = 上一次RTT*0.9 + 下一次RTT*0.1。

  (2)資料傳輸

    tcp傳輸並不是發一個包回一個ack再發下一個包,這樣速度很慢,實際是多個包一起發,再等待ack確認。這樣導致不能保證先發的資料就是先到,後發的資料後到,tcp為了保證順序,引入了超時重傳的機制。收到一個包,啟動一個200ms定時器,等待接收到下一個包,如果在200ms內收到,就會重置定時器等待接收下一個包,如果200ms沒收到就會超時,超時後,就會遍歷那個包沒有收到,並回一個ack確認訊息告知傳送端那沒有收到,讓傳送端從該資料包開始,包括之後的資料包都要重新發。

  慢啟動,擁塞控制如下圖所示。

  一開始數量是指數級增長(慢啟動),到達初始化的閾值後線性增長,增長到對方接收資料時,回ack的包超過RTT時間,這時網路擁塞,資料包太多來不及處理,此時降一半。

  tcp重要的定時器有:

    (a)超時重傳

    (b)堅持定時器,當cwin=0時,接收端告訴傳送端不能再發資料了,如果客戶端想再發送資料,就會啟動一個堅持定時器,發一個探測包給接收端,告訴對端你能不能接收資料,接收端               recv_buff不滿時,就會回ack告知傳送端可以發資料了。

    (c)keepalive

    (d)time_wait,4次揮手中避免最後一次ack丟失

  (3)四次揮手

    四次揮手的流程如下圖所示。

    (a) 客戶端呼叫close(fd)傳送fin包,服務端收到fin包後,回ack包確認;

     (b) 服務端在處理完緩衝區的資料後,呼叫close(fd)關閉對應的fd,傳送fin包;

     (c) 客戶端收到fin包後,回ack包確認,等待2msl(2個數據包傳送週期)後釋放連線;

     (d)服務端收到ack包後,釋放連線。

  思考:

    (1)服務端大量出現close_wait如何解?

      原因:

        服務端recv()返回0後,處理資料不及時,導致close呼叫不及時。

      解決思路:

        (a)檢查程式碼有沒有呼叫close;

        (b)把處理資料做成非同步處理,即拋到執行緒非同步處理;

    (2)客戶端出現大量的fin_wait_2如何解?

      原因:

        服務端recv返回0後,不呼叫close,客戶端就會出現fin_wait_2

      解決思路:    

        (a)服務端處理;

        (b)客戶端 kill;

        (c)重新建個連線。

     (3)客戶端出現大量的time_wait?

        原因是客戶端傳送的最後一次ack包,服務端沒有收到,超時後服務端重發fin包,導致客戶端出現大量的time_wait。