TCP 的backlog詳解及半連接隊列和全連接隊列

注:本文分析基於3.10.107內核版本

問題1:backlog是什麼?
問題2:backlog怎麼設置?
問題3:backlog怎麼影響TCP的建鏈?
問題4:如何驗證backlog的設置?

以上就是我的疑問,因此我開始去從代碼中瞭解backlog。

man手冊

首先,要知道這個backlog是listen()函數裏的第二個參數。

int listen(int sockfd, int backlog);

因此我們看下man手冊裏的解釋:

這裏寫圖片描述
所以說backlog控制的是一個隊列的長度,至於控制的是什麼隊列那就得從TCP的三次握手說起了。

TCP三次握手的隊列

在服務端要接收多個客戶端發起的連接,因此必不可少要使用隊列來管理這些連接。其中在TCP三次握手中有兩個隊列,分別是半連接狀態隊列和全連接隊列。

半連接狀態隊列:每個客戶端發來的SYN報文,服務器都會把這個報文放到隊列裏管理,這個隊列就是半連接隊列,即SYN隊列,此時服務器端口處於SYN_RCVD狀態。之後服務器會向客戶端發送SYN+ACK報文。

全連接狀態隊列:當服務器接收到客戶端的ACK報文後,就會將上述半連接隊列裏面對應的報文轉移(注:其實不是同一個結構,會新建一個結構掛到全連接隊列裏)到另一個隊列裏管理,這個隊列就是全連接隊列,即ACCEPT隊列,此時服務器端口處於ESTABLISHED狀態。

用代碼說話

1、listen()函數

從listen中來,到listen中去。我們先看下listen()系統調用的實現。

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
	struct socket *sock;
	int err, fput_needed;
	int somaxconn;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
		if ((unsigned int)backlog > somaxconn)
			backlog = somaxconn;

		err = security_socket_listen(sock, backlog);
		if (!err)
			err = sock->ops->listen(sock, backlog);

		fput_light(sock->file, fput_needed);
	}
	return err;
}

可以看到,listen()函數裏傳遞的backlog並不是直接使用,而是取的min(backlog, somaxconn),其中somaxconn就是/proc/sys/net/core/somaxconn的值,默認爲128。

再往下調用listen指針指向的函數,在ipv4裏調用的是inet_listen()。

/*
 *	Move a socket into listening state.
 */
int inet_listen(struct socket *sock, int backlog)
{
	struct sock *sk = sock->sk;
	unsigned char old_state;
	int err;

	lock_sock(sk);

	err = -EINVAL;
	if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
		goto out;

	old_state = sk->sk_state;
	if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
		goto out;

		//此時socket的狀態仍爲TCP_CLOSE狀態
	if (old_state != TCP_LISTEN) {
		//暫不分析這個分支
		if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) != 0 &&
		    inet_csk(sk)->icsk_accept_queue.fastopenq == NULL) {
			if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) != 0)
				err = fastopen_init_queue(sk, backlog);
			else if ((sysctl_tcp_fastopen &
				  TFO_SERVER_WO_SOCKOPT2) != 0)
				err = fastopen_init_queue(sk,
				    ((uint)sysctl_tcp_fastopen) >> 16);
			else
				err = 0;
			if (err)
				goto out;
		}
		//這個函數裏有一些關鍵的初始化操作,我們稍後再分析
		err = inet_csk_listen_start(sk, backlog);
		if (err)
			goto out;
	}
	//請注意這個賦值,又一次使用到了backlog入參
	sk->sk_max_ack_backlog = backlog;
	err = 0;

out:
	release_sock(sk);
	return err;
}

2、SYN報文的接收

我們再看服務端收到客戶端SYN報文的處理流程。在TCP層的處理大概如下:

tcp_v4_do_rcv
    |--> tcp_rcv_state_process
               |--> tcp_v4_conn_request

那我們就來看下這個tcp_v4_conn_request()函數。

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	....
	//這裏就是在判斷半連接狀態隊列是否滿
	if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
		want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
		if (!want_cookie)
			goto drop;
	}

	//這裏便是全連接隊列是否滿的判斷
	if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
	....
}

按圖索驥,我們接着看到底是怎麼判斷這兩個隊列滿的,這樣我們就能知道這兩個隊列的長度到底是由什麼參數決定的。

半連接隊列:

static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
	return reqsk_queue_is_full(&inet_csk(sk)->icsk_accept_queue);
}

static inline int reqsk_queue_is_full(const struct request_sock_queue *queue)
{
	//重點在max_qlen_log
	return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
}

由此可以看出半連接隊列的長度上限和max_qlen_log相關。其實這個值就是在listen()函數流程裏設置的,在上節梳理listen()函數時inet_csk_listen_start()沒有繼續往下梳理,放到這說明比較連貫。

3、inet_csk_listen_start()的初始化內容

//由listen()函數可知,這裏的nr_table_entries入參就是backlog
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
	struct inet_sock *inet = inet_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	//分配請求的socket結構,並進行初始化操作
	int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);

	if (rc != 0)
		return rc;

	sk->sk_max_ack_backlog = 0;
	sk->sk_ack_backlog = 0;
	inet_csk_delack_init(sk);

	sk->sk_state = TCP_LISTEN;//設置socket狀態爲listen,所以之前的狀態應該是closed
	if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
		inet->inet_sport = htons(inet->inet_num);

		sk_dst_reset(sk);
		sk->sk_prot->hash(sk);

		return 0;
	}

	sk->sk_state = TCP_CLOSE;
	__reqsk_queue_destroy(&icsk->icsk_accept_queue);
	return -EADDRINUSE;
}

//這個函數進行的初始化操作和半連接隊列有密切關係
int reqsk_queue_alloc(struct request_sock_queue *queue,
		      unsigned int nr_table_entries)
{
	size_t lopt_size = sizeof(struct listen_sock);
	struct listen_sock *lopt;
	
	//可以看到這裏又進行了取值,其中sysctl_max_syn_backlog就是/proc/sys/net/ipv4/tcp_max_syn_backlog的值
	//因此 8 <= nr_table_entries <= tcp_max_syn_backlog
	nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
	nr_table_entries = max_t(u32, nr_table_entries, 8);
	//這裏做的是取與(nr_table_entries + 1)最接近的2的指數次冪,
	//如果nr_table_entries=7,那該函數的返回值便是8,如果nr_table_entries=8,那麼就返回16,以此類推。
	nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
	lopt_size += nr_table_entries * sizeof(struct request_sock *);
	if (lopt_size > PAGE_SIZE)
		lopt = vzalloc(lopt_size);
	else
		lopt = kzalloc(lopt_size, GFP_KERNEL);
	if (lopt == NULL)
		return -ENOMEM;

	//這個for循環就有意思了,求的其實就是2的幾次方開始大於nr_table_entries
	//最終結果保存在max_qlen_log變量中
	//即log2(nr_table_entries),結果向上取整
	//因爲nr_table_entries至少爲8,所以指數從3開始
	for (lopt->max_qlen_log = 3;
	     (1 << lopt->max_qlen_log) < nr_table_entries;
	     lopt->max_qlen_log++);

	get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
	rwlock_init(&queue->syn_wait_lock);
	queue->rskq_accept_head = NULL;
	lopt->nr_table_entries = nr_table_entries;

	write_lock_bh(&queue->syn_wait_lock);
	queue->listen_opt = lopt;
	write_unlock_bh(&queue->syn_wait_lock);

	return 0;
}

4、半連接隊列長度

知道max_qlen_log怎麼來的,這下就可以重新回到半連接隊列上。

queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log

所以可以得出結論,半連接隊列的長度爲

roundup_pow_of_two(max(8, min(tcp_max_syn_backlog, min(backlog, somaxconn))) + 1)

其中backlog爲listen()函數的第二個參數,somaxconn爲/proc/sys/net/core/somaxconn的值,tcp_max_syn_backlog爲/proc/sys/net/ipv4/tcp_max_syn_backlog的值。

不過這個算式有點複雜,我們稍微簡化一下,並考慮實際隊列長度一般都會設置成大於8,因此有如下計算方式:

roundup_pow_of_two(min(tcp_max_syn_backlog, backlog, somaxconn) + 1)

也就是三個參數取最小值後+1,再向上取離它最近的2次冪整數。

5、全連接隊列長度

全連接隊列的長度就比較簡單了。

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
	return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

有上可知accept隊列的長度上限爲sk_max_ack_backlog,這個值在上面inet_listen()函數裏有標註,這個值就是

min(backlog, somaxconn)

其中backlog爲listen()函數的第二個參數,somaxconn爲/proc/sys/net/core/somaxconn的值。

至此,對於backlog這個參數我們已經分析清楚了,下面我們就來實際驗證一下我們的分析結果。

6、實例驗證

首先寫一個很簡單的服務端程序,然後用hping工具來發起泛洪攻擊,這樣可以很方便的觀察到半連接狀態的連接。

服務端程序如下:

/* server.c */

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

#define PORT 5000
#define BACKLOG 5
#define BUFLEN 1024

int main(int argc, char *argv[])
{
	int listenfd, connfd;
	struct sockaddr_in servaddr;
    char buf[BUFLEN];
	
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("socket() failed\n");
        exit(1);
    }
 
    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        printf("bind() failed\n");
        exit(1);
    }
    
    if(listen(listenfd, BACKLOG) < -1)
    {
        printf("listen() failed\n");
        exit(1);
    }

    while(1){
        if((connfd = accept(listenfd, NULL, NULL)) < 0)
		{
            printf("accept() failed\n");
            exit(1);
        }

    }
    close(listenfd); 
	close(connfd);
    return 0;
}

此次測試端口號設置爲5000,listen()函數的backlog設置爲500,此時系統內核參數如下:

linux-yuk6:/ # cat /proc/sys/net/ipv4/tcp_max_syn_backlog
512
linux-yuk6:/ # cat /proc/sys/net/core/somaxconn
128

按照上述總結的公式可得半連接隊列上限爲:`

roundup_pow_of_two(max(8, min(512, min(500, 128))) + 1)
即,roundup_pow_of_two(129)=256

開啓三個終端窗口,一個執行服務器端程序,等待客戶端連接;一個執行hping3程序,發起泛洪攻擊;剩下一個用netstat命令查看處於SYN_RECV狀態的連接數。

hping3程序的安裝請參考:hping3的編譯和安裝

此次測試使用具體命令爲:

./hping3 -d 120 -S -w 64 -p 5000 -i u100 --rand-source 192.168.2.116

測試結果如下圖:
這裏寫圖片描述

由圖可知,與我們的分析結果一致。

以下幾組數據供大家參考:
這裏寫圖片描述


參考資料:
1、http://blog.csdn.net/raintungli/article/details/37913765
2、http://www.cnblogs.com/Orgliny/p/5780796.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章