Linux中listen()系統調用的backlog參數分析

   這篇文章是對上一篇博客網絡編程經常使用接口的內核實現----sys_listen()的補充,上篇文章中我說listen()系統調用的backlog參數既是鏈接隊列的長度,也指定了半鏈接隊列的長度(不能說等於),而不是《Unix網絡編程》中講到的是半鏈接隊列和鏈接隊列之和的上限,也就是說這個說法對Linux不適用。這篇文章中經過具體的代碼來講明這個結論,而且會分析若是鏈接隊列和半鏈接隊列都滿的話,內核會怎樣處理。linux

  首先來看半鏈接隊列的上限是怎麼計算和存儲的。半鏈接隊列長度的上限值存儲在listen_sock結構的max_qlen_log成員中。若是找到監聽套接字的sock實例,調用inet_csk()能夠獲取inet_connection_sock實例,inet_connection_sock結構是描述支持面向鏈接特性的描述塊,其成員icsk_accept_queue是用來管理鏈接隊列和半鏈接隊列的結構,類型是request_sock_queue。listen_sock實例就存儲在request_sock_queue結構的listen_opt成員中,它們之間的關係以下圖所示(注:原本下面的圖應該橫着畫,可是橫着CSDN會顯示不全):編程



  

   半鏈接隊列的長度上限在reqsk_queue_alloc()中計算並設置的,代碼片斷以下所示:服務器


[cpp] view plaincopycookie

  1. int reqsk_queue_alloc(struct request_sock_queue *queue,  網絡

  2.               unsigned int nr_table_entries)  socket

  3. {  tcp

  4.     .......  ide

  5.       

  6.     nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);  函數

  7.     nr_table_entries = max_t(u32, nr_table_entries, 8);  測試

  8.     nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);  

  9.     ......  

  10.       

  11.     ......  

  12.       

  13.     for (lopt->max_qlen_log = 3;  

  14.          (1 << lopt->max_qlen_log) < nr_table_entries;  

  15.          lopt->max_qlen_log++);  

  16.       

  17.     ......  

  18.   

  19. }  


  前面的三行代碼是調整存儲半鏈接的哈希表的大小,能夠看到這個值還受系統配置sysctl_max_syn_backlog的影響,因此若是想調大監聽套接字的半鏈接隊列,除了增大listen()的backlog參數外,還須要調整sysctl_max_syn_backlog系統配置的值,這個配置量對應的proc文件爲/proc/sys/net/ipv4/tcp_max_syn_backlog。後面的for循環是計算nr_table_entries以2爲底的對數,計算的結果就存儲在max_qlen_log成員中。

  接着來看鏈接隊列長度的上限,這個比較簡單,存儲在sock結構的sk_max_ack_backlog成員中,在inet_listen()中設置,以下所示:


[cpp] view plaincopy

  1. int inet_listen(struct socket *sock, int backlog)  

  2. {  

  3.     ......  

  4.       

  5.     sk->sk_max_ack_backlog = backlog;  

  6.     err = 0;  

  7.   

  8. out:  

  9.     release_sock(sk);  

  10.     return err;  

  11. }  

  接下來咱們看若是鏈接隊列滿了的話,內核會如何處理。先寫個測試程序,構造鏈接隊列滿的狀況。測試程序說明以下:

   一、服務器端地址爲192.168.1.188,監聽端口爲80;客戶端地址爲192.168.1.192


   二、服務器端在80端口創建一個監聽套接字,listen()的backlog參數設置的是300,將sysctl_max_syn_backlog和sysctl_somaxconn系統配置都調整爲4096,特別要注意的             是服務器端必定不要調用accept()來接收鏈接,在創建起監聽後,讓進程睡眠等待。關鍵代碼以下:


[cpp] view plaincopy

  1. ........  

  2. if ((ret = listen(fd, 300)) < 0) {  

  3.          perror("listen");  

  4.          goto err_out;  

  5.  }  

  6.   

  7.   

  8.  /* wait connection */  

  9.  while (1) {  

  10.          sleep(3);  

  11.  }  

  12.  ........  



   三、客戶端經過一個循環發起1000個鏈接請求,爲了後面進一步的分析,在第401鏈接創建後打印輸出其本地端口,而且發送了兩次數據。關鍵代碼以下:


[cpp] view plaincopy

  1.  ......  

  2.   

  3.  ret = connect(fd, (struct sockaddr *)&sa, sizeof(sa));  

  4.  if (ret < 0) {  

  5.      fprintf(stderr, "connect fail in %d times, reason: %s.\n", i + 1, strerror(errno));  

  6.      return -1;  

  7.  }  

  8.   

  9.  connections[i] = fd;  

  10.  fprintf(stderr, "Connection success, times: %d, connections: %d.\n", i + 1,   

  11.          check_connection_count(connections, i + 1));  

  12.  if (i == 400) {  

  13.      len = sizeof(sa);  

  14.      ret = getsockname(fd, (struct sockaddr *)&sa, &len);  

  15.      if (ret < 0) {  

  16.          fprintf(stderr, "getsockname fail, ret=%d.\n", ret);  

  17.          return -1;  

  18.      }  

  19.      fprintf(stderr, "connecton %d, local port: %u.\n", i,ntohs(sa.sin_port));  

  20.        

  21.      str = "if i can write ,times 1";  

  22.      ret = write(fd, str, strlen(str));  

  23.      fprintf(stderr, "first writ in connection %d, ret = %d.\n", i, ret);  

  24.   

  25.      str = "if i can write ,times 2";  

  26.      ret = write(fd, str, strlen(str));  

  27.      fprintf(stderr, "second writ in connection %d, ret = %d.\n", i, ret);  

  28.  }  

  29. .......  

  在啓動測試程序以前,在客戶端使用tcpdump抓包,並將輸出結果經過-w選項存儲在192.cap文件中,便於後續使用wireshark來分析。


  測試發現,在客戶端創建300個鏈接後,客戶端創建鏈接的速度明顯慢了不少,並且最終創建完1000個鏈接花了20分鐘左右。使用wireshark打開192.cap文件,來看抓包的狀況,發如今300個鏈接以後有大量的ack包重傳,以下圖所示:

  在wireshark的過濾器中選擇本地端口爲49274的鏈接來具體分析,該鏈接抓包狀況以下所示:


上面的圖中能夠看到,SYN包重傳了一次;在正常的三次握手以後,服務器又發送了SYN+ACK包給客戶端,致使客戶段再次發送ACK,並且這個過程重複了5次。在wireshark中過濾其餘鏈接,發現狀況也是如此。

  問題來了,爲何要重傳SYN包?爲何在三次握手以後,服務器端還要重複發送SYN+ACK包?爲何重複了5次以後就再也不發了呢?要解答這些問題,咱們須要深刻到內核代碼中看三次握手過程當中內核是如何處理的,以及在鏈接隊列滿以後是怎麼處理。內核中處理客戶端發送的SYN包是在tcp_v4_conn_request()函數中,關鍵代碼以下所示:


[cpp] view plaincopy

  1. int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)  

  2. {  

  3.     ......  

  4.   

  5.     if (inet_csk_reqsk_queue_is_full(sk) && !isn) {  

  6. #ifdef CONFIG_SYN_COOKIES   

  7.         if (sysctl_tcp_syncookies) {  

  8.             want_cookie = 1;  

  9.         } else  

  10. #endif  

  11.         goto drop;  

  12.     }  

  13.   

  14.     /* Accept backlog is full. If we have already queued enough 

  15.      * of warm entries in syn queue, drop request. It is better than 

  16.      * clogging syn queue with openreqs with exponentially increasing 

  17.      * timeout. 

  18.      */  

  19.     if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)  

  20.         goto drop;  

  21.   

  22.     req = inet_reqsk_alloc(&tcp_request_sock_ops);  

  23.     if (!req)  

  24.         goto drop;                                                                  ......                                                                            

  25.     if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)  

  26.         goto drop_and_free;  

  27.   

  28.     inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);  

  29.     return 0;  

  30.   

  31. drop_and_release:  

  32.     dst_release(dst);  

  33. drop_and_free:  

  34.     reqsk_free(req);  

  35. drop:  

  36.     return 0;  

  37. }  

咱們主要看inet_csk_reqsk_queue_is_full()函數和sk_acceptq_is_full()函數的部分,這兩個函數分別用來判斷半鏈接隊列和鏈接隊列是否已滿。結合上面的代碼,在兩種狀況下會丟掉SYN包。一種是在半鏈接隊列已滿的狀況下,isn的值其實TCP_SKB_CB(skb)->when的值,when在tcp_v4_rcv()中被清零,因此!isn老是爲真;第二種狀況是在鏈接隊列已滿而且半鏈接隊列中還有未重傳過的半鏈接(經過inet_csk_reqsk_queue_young()來判斷)。至於咱們看到的源端口爲49274的鏈接是在哪一個位置丟掉的就不知道了,這要看可是半鏈接隊列的狀況。由於有專門的定時器函數來維護半鏈接隊列,因此在第二次發送SYN包時,包沒有丟棄,因此內核會調用__tcp_v4_send_synack()函數來發送SYN+ACK包,而且分配內存用來描述當前的半鏈接狀態。當服務器發送的SYN+ACK包到達客戶端時,客戶端的狀態會從SYN_SENT狀態變爲ESTABLISHED狀態,也就是說客戶端認爲TCP鏈接已經創建,而後發送ACK給服務器端,來完成三次握手。在正常狀況下,服務器端接收到客戶端發送的ACK後,會將描述半鏈接的request_sock實例從半鏈接隊列移除,而且創建描述鏈接的sock結構,可是在鏈接隊列已滿的狀況下,內核並非這樣處理的。

  當客戶端發送的ACK到達服務器後,內核會調用tcp_check_req()來檢查這個ACK包是不是正確,從TCP層的接收函數tcp_v4_rcv()到tcp_check_req()的代碼流程以下圖所示:


 若是是正確的ACK包,tcp_check_req()會調用tcp_v4_syn_recv_sock()函數建立新的套接字,在tcp_v4_syn_recv_sock()中會首先檢查鏈接隊列是否已滿,若是已滿的話,會直接返回NULL。當tcp_v4_syn_recv_sock()返回NULL時,會跳轉到tcp_check_req()函數的listen_overflow標籤處執行,以下所示:


[cpp] view plaincopy

  1. /* 

  2.  *  Process an incoming packet for SYN_RECV sockets represented 

  3.  *  as a request_sock. 

  4.  */  

  5. struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,  

  6.                struct request_sock *req,  

  7.                struct request_sock **prev)  

  8. {  

  9.     ......  

  10.       

  11.     /* OK, ACK is valid, create big socket and 

  12.      * feed this segment to it. It will repeat all 

  13.      * the tests. THIS SEGMENT MUST MOVE SOCKET TO 

  14.      * ESTABLISHED STATE. If it will be dropped after 

  15.      * socket is created, wait for troubles. 

  16.      */  

  17.     child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);  

  18.     if (child == NULL)  

  19.         goto listen_overflow;  

  20.           

  21.      .......  

  22.   

  23. listen_overflow:  

  24.     if (!sysctl_tcp_abort_on_overflow) {  

  25.         inet_rsk(req)->acked = 1;  

  26.         return NULL;  

  27.     }  

  28.   

  29.     ......  

  30. }  

  在listen_overflow處,會設置inet_request_sock的acked成員,該標誌設置時表示已接收到第三次握手的ACK段,可是因爲服務器繁忙或其餘緣由致使未能創建起鏈接,此時可根據該標誌從新給客戶端發送SYN+ACK段,再次進行鏈接的創建。具體檢查是否須要重傳是在syn_ack_recalc()函數中進行的,其代碼以下所示:



[cpp] view plaincopy

  1. /* Decide when to expire the request and when to resend SYN-ACK */  

  2. static inline void syn_ack_recalc(struct request_sock *req, const int thresh,  

  3.                   const int max_retries,  

  4.                   const u8 rskq_defer_accept,  

  5.                   int *expire, int *resend)  

  6. {  

  7.     if (!rskq_defer_accept) {  

  8.         *expire = req->retrans >= thresh;  

  9.         *resend = 1;  

  10.         return;  

  11.     }  

  12.     *expire = req->retrans >= thresh &&  

  13.           (!inet_rsk(req)->acked || req->retrans >= max_retries);  

  14.     /* 

  15.      * Do not resend while waiting for data after ACK, 

  16.      * start to resend on end of deferring period to give 

  17.      * last chance for data or ACK to create established socket. 

  18.      */  

  19.     *resend = !inet_rsk(req)->acked ||  

  20.           req->retrans >= rskq_defer_accept - 1;  

  21. }  

在SYN+ACK的重傳次數未到達上限或者已經接收到第三次握手的ACK段後,因爲繁忙或其餘緣由致使未能創建起鏈接時會重傳SYN+ACK。


  至此,咱們不難理解爲何服務器老是會重複發送SYN+ACK。當客戶端的第三次握手的ACK到達服務器端後,服務器檢查ACK沒有問題,接着調用tcp_v4_syn_recv_sock()來建立套接字,發現鏈接隊列已滿,由於直接返回NULL,並設置acked標誌,在定時器中稍後從新發送SYN+ACK,嘗試完成鏈接的創建。當服務器段發送的SYN+ACK到達客戶端後,客戶端會從新發送ACK給服務器,在這個過程當中服務器端是主動方,客戶端只是被動地發送響應,從抓包的狀況也能看出。那若是重試屢次仍是不能創建鏈接呢,服務器會一直重複發送SYN+ACK嗎?答案確定是否認的,重傳的次數受系統配置sysctl_tcp_synack_retries的影響,該值默認爲5,所以咱們在抓包的時候看到在重試5次以後,服務器段就不再重發SYN+ACK包了。若是重試了5次以後仍是不能創建鏈接,內核會將這個半鏈接從半鏈接隊列上移除並釋放。

  到這裏咱們先前的全部問題都解決了,可是又有了一個新的問題,當服務器端發送SYN+ACK給客戶端時,服務器端可能還處於半鏈接狀態,沒有建立描述鏈接的sock結構,可是咱們知道客戶端在接收到服務器端的SYN+ACK後,按照三次握手過程當中的狀態遷移這時會從SYN_SENT狀態變爲ESTABLISHED狀態,能夠參考《Unix網絡編程》上的圖2.5,以下所示:


因此在鏈接隊列已滿的狀況下,客戶端會在鏈接還沒有完成的時候誤認爲鏈接已經創建,若是在這種狀況下發送數據到服務器端是沒有辦法處理的。這種狀況即便調用getsockopt()來檢查SO_ERROR選項也是檢測不到的。假設客戶端在接收到第一個SYN+ACK包後,就發送數據給服務器段,服務器端並無創建鏈接。當數據包傳送到TCP層的接收函數tcp_v4_rcv()中處理時,由於沒有找到sock實例,會直接丟掉數據包。可是在客戶端調用write()發送數據時,將要發送的數據拷貝到內核緩衝區後就會返回成功,客戶端依然發現不了鏈接其實還沒有徹底創建。當write返回後,TCP協議棧將數據發送到服務器端時不會受到ACK包,只能重傳。由於服務器段不存在這個鏈接,即便重傳無數次也沒有用,固然服務器端的協議棧也不能容許客戶端無限制地重複這樣的過程,最後會以服務器端發送的RST包完全結束這個沒有正確創建的「鏈接」。也就是說在這種極限狀況下,TCP協議的可靠性無法保證。

  咱們在客戶端的測試程序中打印出了第401個「鏈接」的端口號,咱們經過這個鏈接就能夠驗證咱們的結論,其抓包狀況以下所示:


在客戶端程序中write()系統調用返回成功,可是咱們在圖中能夠看到發送的數據一直在重傳而沒有收到確認包,直到最終接收到服務器端發送的RST包。

 OK,到這裏咱們的分析算是完全結束了,在分析的過程當中忽略了一些細節的東西,感興趣的能夠本身結合源碼看一看。

相關文章
相關標籤/搜索