神奇的backlog參數對TCP鏈接創建的影響

曾經有人問我套接字編程中 listen的第二個參數 backlog是什麼意思?多大的值合適?我不假思索地回答它表示服務器能夠接受的併發請求的最大值。然而事實真的是這樣的嗎?

tcp-state-diagram.png

TCP經過三次握手創建鏈接的過程應該都不陌生了。從服務器的角度看,它分爲如下幾步html

  1. TCP狀態設置爲LISTEN狀態,開啓監聽客戶端的鏈接請求
  2. 收到客戶端發送的SYN報文後,TCP狀態切換爲SYN RECEIVED,併發送SYN ACK報文
  3. 收到客戶端發送的ACK報文後,TCP三次握手完成,狀態切換爲ESTABLISHED

Unix系統中,開啓監聽是經過listen完成。linux

int listen(int sockfd, int backlog)

listen有兩個參數,第一個參數sockfd表示要設置的套接字,本文主要關注的是其第二個參數backloggit

<Unix 網絡編程>將其描述爲已完成的鏈接隊列(ESTABLISHED)與未完成鏈接隊列(SYN_RCVD)之和的上限。編程

通常咱們將ESTABLISHED狀態的鏈接稱爲全鏈接,而將SYN_RCVD狀態的鏈接稱爲半鏈接ubuntu

圖片描述

當服務器收到一個SYN後,它建立一個子鏈接加入到SYN_RCVD隊列。在收到ACK後,它將這個子鏈接移動到ESTABLISHED隊列。最後當用戶調用accept()時,會將鏈接從ESTABLISHED隊列取出。小程序


是 Posix 不是 TCP

listen只是posix標準,不是TCP的標準!不是TCP標準就意味着不一樣的內核能夠有本身獨立的實現服務器

POSIX是這麼說的:cookie

The backlog argument provides a hint to the implementation which the implementation shall use to limit the number of outstanding connections in the socket's listen queue.

Linux是什麼行爲呢 ? 查看listenman page網絡

The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests.

什麼意思呢?就是說的在Linux 2.2之後, backlog只限制完成了三次握手,處於ESTABLISHED狀態等待accept的子鏈接的數目了。併發

真的是這樣嗎?因而我決定抄一個小程序驗證一下:

服務器監聽50001端口,而且設置backlog = 4。注意,我爲了將隊列塞滿,沒有調用accept

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BACKLOG 4

int main(int argc, char **argv)
{
    int listenfd;
    int connfd;
    struct sockaddr_in servaddr;

    listenfd = socket(PF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(50001);

    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    listen(listenfd, BACKLOG);
    while(1)
    {
        sleep(1);
    }
    
    return 0;
}

客戶端的代碼

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(PF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(50001);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (0 != connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))
    {
         printf("connect failed!\n");
    }
    else
    {
         printf("connect succeed!\n");
    }

    sleep(30);

    return 1;
}

爲了排除syncookie的干擾,我首先關閉了syncookie功能

echo 0 > /proc/sys/net/ipv4/tcp_syncookies

因爲我設置的backlog = 4而且服務器始終不會accept。所以預期會創建 4 個全鏈接, 但實際倒是

root@ubuntu-1:/home/user1/workspace/client# ./client &
[1] 12798
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[2] 12799
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[3] 12800
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[4] 12801
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[5] 12802
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[6] 12803
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[7] 12804
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[8] 12805
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[9] 12806
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[10] 12807
root@ubuntu-1:/home/user1/workspace/client# connect failed!

看!客戶器居然顯示成功創建了 9 次鏈接!

netstat看看TCP鏈接狀態

> netstat -t
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 localhost:50001         localhost:55792         ESTABLISHED
tcp        0      0 localhost:55792         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55798         localhost:50001         ESTABLISHED   
tcp        0      1 localhost:55806         localhost:50001         SYN_SENT
tcp        0      0 localhost:50001         localhost:55784         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55794         SYN_RECV
tcp        0      0 localhost:55786         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55800         localhost:50001         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55786         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55800         SYN_RECV
tcp        0      0 localhost:55784         localhost:50001         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55796         SYN_RECV
tcp        0      0 localhost:50001         localhost:55788         ESTABLISHED
tcp        0      0 localhost:55794         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55788         localhost:50001         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55790         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55798         SYN_RECV
tcp        0      0 localhost:55790         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55796         localhost:50001         ESTABLISHED

整理一下就是下面這樣
圖片描述

從上面能夠看出,一共有5條鏈接對是ESTABLISHED<->ESTABLISHED鏈接, 但還有4條鏈接對是SYN_RECV<->ESTABLISHED鏈接, 這表示對客戶端三次握手已經完成了,但對服務器尚未! 回顧一下TCP三次握手的過程,形成這種鏈接對緣由只有多是服務器客戶端最後發送的握手ACK被丟棄了!

還有一個問題,我明明設置的backlog的值是 4,可爲何還能創建5個鏈接 ?!


去內核找緣由

我實驗用的機器內核是 4.4.0

前面提到過已完成鏈接隊列未完成鏈接隊列這兩個概念, Linux有這兩個隊列嗎 ? Linux 既有又沒有! 說有是由於內核中能夠獲得兩種鏈接各自的長度; 說沒有是由於 Linux只有已完成鏈接隊列實際存在, 而未完成鏈接隊列只有長度的記錄!

每個LISTEN狀態的套接字都有一個struct inet_connection_sock結構, 其中的accept_queue從名字上也能夠看出就是已完成三次握手的子鏈接隊列.只是這個結構裏還記錄了半鏈接請求的長度!

struct inet_connection_sock {    
    // code omitted 
    struct request_sock_queue icsk_accept_queue;
    // code omitted
}

struct request_sock_queue {
    // code omitted
    atomic_t        qlen;                // 半鏈接的長度
    atomic_t        young;               // 通常狀況, 這個值 = qlen

    struct request_sock    *rskq_accept_head;  // 已完成鏈接的隊列頭
    struct request_sock    *rskq_accept_tail;  // 已完成鏈接的隊列尾
    // code omitted
};

因此通常狀況下鏈接創建時,服務端的變化過程是這樣的:

  1. 收到SYN報文, qlen++,young++
  2. 收到ACK報文, 三次握手完成,將鏈接加入accept隊列,qlen--,young--
  3. 用戶使用accept,將鏈接從accept取出.

再來看內核收到SYN握手報文時的處理, 因爲我關閉了syncookie,因此一旦知足了下面代碼中的兩個條件之一就會丟棄報文

int tcp_conn_request(struct request_sock_ops *rsk_ops, 
             const struct tcp_request_sock_ops *af_ops,
             struct sock *sk, struct sk_buff *skb) 

    if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
         inet_csk_reqsk_queue_is_full(sk)) && !isn) {   // 條件1: 半鏈接 >= backlog
        want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
        if (!want_cookie)
            goto drop;
    } 

    if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { // 條件2: 全鏈接 > backlog 而且 半鏈接 > 1
        NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
        goto drop;
    } 

    // code omitted

下面是收到ACK握手報文時的處理

struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
                  struct request_sock *req,
                  struct dst_entry *dst,
                  struct request_sock *req_unhash,
                  bool *own_req) 
{
     // code omitted
     if (sk_acceptq_is_full(sk))           //  全鏈接 > backlog, 就丟棄
         goto exit_overflow;

     newsk = tcp_create_openreq_child(sk, req, skb); // 建立子套接字了
     // code omitted
}

因此這樣就能夠解釋實驗現象了!

  1. 4個鏈接請求均可以順利建立子鏈接, 全鏈接隊列長度 = backlog = 4, 半鏈接數目 = 0
  2. 5個鏈接請求, 因爲sk_acceptq_is_full的判斷條件是>而不是>=,因此依然能夠創建全鏈接
  3. 6-9個鏈接請求到來時,因爲半鏈接的數目尚未超過backlog,因此仍是能夠繼續回覆SYNACK,但收到ACK後已經不能再建立子套接字了,因此TCP狀態依然爲SYN_RECV.同時半鏈接的數目也增長到backlog.而對於客戶端,它既然能收到SYNACK握手報文,所以它能夠將TCP狀態變爲ESTABLISHED,
  4. 10個請求到來時, 因爲半鏈接的數目已經達到backlog,所以,這個SYN報文會被丟棄.

內核的問題

從以上的現象和分析中,我認爲內核存在如下問題

  1. accept隊列是否滿的判斷用>=>更合適, 這樣才能體現backlog的做用
  2. accept隊列滿了,就應該拒絕半鏈接了,由於即便半鏈接握手完成,也沒法加入accept隊列,不然就會出現SYN_RECV--ESTABLISHED這樣狀態的鏈接對!這樣的鏈接是不能進行數據傳輸的!

問題2在16年的補丁中已經修改了! 因此若是你在更新版本的內核中進行相同的實驗, 會發現客戶端只能鏈接成功5次了,固然這也要先關閉syncookie

但問題1尚未修改! 若是之後修改了,我也不會意外

(完)

REF

how-tcp-backlog-works-in-linux

相關文章
相關標籤/搜索