Linux中listen()系統呼叫的backlog引數分析
這篇文章是對上一篇部落格網路程式設計常用介面的核心實現----sys_listen()的補充,上篇文章中我說listen()系統呼叫的backlog引數既是連線佇列的長度,也指定了半連線佇列的長度(不能說等於),而不是《Unix網路程式設計》中講到的是半連線佇列和連線佇列之和的上限,也就是說這個說法對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 plaincopyprint?- int reqsk_queue_alloc(struct request_sock_queue *queue,
- unsigned int nr_table_entries)
- {
- .......
- 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 = roundup_pow_of_two(nr_table_entries + 1);
- ......
- ......
- for (lopt->max_qlen_log = 3;
- (1 << lopt->max_qlen_log) < nr_table_entries;
- lopt->max_qlen_log++);
- ......
- }
int reqsk_queue_alloc(struct request_sock_queue *queue, unsigned int nr_table_entries) { ....... 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 = roundup_pow_of_two(nr_table_entries + 1); ...... ...... for (lopt->max_qlen_log = 3; (1 << lopt->max_qlen_log) < nr_table_entries; lopt->max_qlen_log++); ...... }
前面的三行程式碼是調整儲存半連線的雜湊表的大小,可以看到這個值還受系統配置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 plaincopyprint?- int inet_listen(struct socket *sock, int backlog)
- {
- ......
- sk->sk_max_ack_backlog = backlog;
- err = 0;
- out:
- release_sock(sk);
- return err;
- }
int inet_listen(struct socket *sock, int backlog)
{
......
sk->sk_max_ack_backlog = backlog;
err = 0;
out:
release_sock(sk);
return err;
}
接下來我們看如果連線佇列滿了的話,核心會如何處理。先寫個測試程式,構造連線佇列滿的情況。測試程式說明如下:
1、伺服器端地址為192.168.1.188,監聽埠為80;客戶端地址為192.168.1.192
2、伺服器端在80埠建立一個監聽套接字,listen()的backlog引數設定的是300,將sysctl_max_syn_backlog和sysctl_somaxconn系統配置都調整為4096,特別要注意的 是伺服器端一定不要呼叫accept()來接收連線,在建立起監聽後,讓程序睡眠等待。關鍵程式碼如下:
[cpp] view plaincopyprint?- ........
- if ((ret = listen(fd, 300)) < 0) {
- perror("listen");
- goto err_out;
- }
- /* wait connection */
- while (1) {
- sleep(3);
- }
- ........
........
if ((ret = listen(fd, 300)) < 0) {
perror("listen");
goto err_out;
}
/* wait connection */
while (1) {
sleep(3);
}
........
3、客戶端通過一個迴圈發起1000個連線請求,為了後面進一步的分析,在第401連線建立後列印輸出其本地埠,並且傳送了兩次資料。關鍵程式碼如下:
[cpp] view plaincopyprint?- ......
- ret = connect(fd, (struct sockaddr *)&sa, sizeof(sa));
- if (ret < 0) {
- fprintf(stderr, "connect fail in %d times, reason: %s.\n", i + 1, strerror(errno));
- return -1;
- }
- connections[i] = fd;
- fprintf(stderr, "Connection success, times: %d, connections: %d.\n", i + 1,
- check_connection_count(connections, i + 1));
- if (i == 400) {
- len = sizeof(sa);
- ret = getsockname(fd, (struct sockaddr *)&sa, &len);
- if (ret < 0) {
- fprintf(stderr, "getsockname fail, ret=%d.\n", ret);
- return -1;
- }
- fprintf(stderr, "connecton %d, local port: %u.\n", i,ntohs(sa.sin_port));
- str = "if i can write ,times 1";
- ret = write(fd, str, strlen(str));
- fprintf(stderr, "first writ in connection %d, ret = %d.\n", i, ret);
- str = "if i can write ,times 2";
- ret = write(fd, str, strlen(str));
- fprintf(stderr, "second writ in connection %d, ret = %d.\n", i, ret);
- }
- .......
......
ret = connect(fd, (struct sockaddr *)&sa, sizeof(sa));
if (ret < 0) {
fprintf(stderr, "connect fail in %d times, reason: %s.\n", i + 1, strerror(errno));
return -1;
}
connections[i] = fd;
fprintf(stderr, "Connection success, times: %d, connections: %d.\n", i + 1,
check_connection_count(connections, i + 1));
if (i == 400) {
len = sizeof(sa);
ret = getsockname(fd, (struct sockaddr *)&sa, &len);
if (ret < 0) {
fprintf(stderr, "getsockname fail, ret=%d.\n", ret);
return -1;
}
fprintf(stderr, "connecton %d, local port: %u.\n", i,ntohs(sa.sin_port));
str = "if i can write ,times 1";
ret = write(fd, str, strlen(str));
fprintf(stderr, "first writ in connection %d, ret = %d.\n", i, ret);
str = "if i can write ,times 2";
ret = write(fd, str, strlen(str));
fprintf(stderr, "second writ in connection %d, ret = %d.\n", i, ret);
}
.......
在啟動測試程式之前,在客戶端使用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 plaincopyprint?- int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
- {
- ......
- if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
- #ifdef CONFIG_SYN_COOKIES
- if (sysctl_tcp_syncookies) {
- want_cookie = 1;
- } else
- #endif
- goto drop;
- }
- /* Accept backlog is full. If we have already queued enough
- * of warm entries in syn queue, drop request. It is better than
- * clogging syn queue with openreqs with exponentially increasing
- * timeout.
- */
- if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
- goto drop;
- req = inet_reqsk_alloc(&tcp_request_sock_ops);
- if (!req)
- goto drop; ......
- if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)
- goto drop_and_free;
- inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
- return 0;
- drop_and_release:
- dst_release(dst);
- drop_and_free:
- reqsk_free(req);
- drop:
- return 0;
- }
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
......
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
req = inet_reqsk_alloc(&tcp_request_sock_ops);
if (!req)
goto drop; ......
if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)
goto drop_and_free;
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
return 0;
drop_and_release:
dst_release(dst);
drop_and_free:
reqsk_free(req);
drop:
return 0;
}
我們主要看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 plaincopyprint?- /*
- * Process an incoming packet for SYN_RECV sockets represented
- * as a request_sock.
- */
- struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
- struct request_sock *req,
- struct request_sock **prev)
- {
- ......
- /* OK, ACK is valid, create big socket and
- * feed this segment to it. It will repeat all
- * the tests. THIS SEGMENT MUST MOVE SOCKET TO
- * ESTABLISHED STATE. If it will be dropped after
- * socket is created, wait for troubles.
- */
- child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
- if (child == NULL)
- goto listen_overflow;
- .......
- listen_overflow:
- if (!sysctl_tcp_abort_on_overflow) {
- inet_rsk(req)->acked = 1;
- return NULL;
- }
- ......
- }
/*
* Process an incoming packet for SYN_RECV sockets represented
* as a request_sock.
*/
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct request_sock **prev)
{
......
/* OK, ACK is valid, create big socket and
* feed this segment to it. It will repeat all
* the tests. THIS SEGMENT MUST MOVE SOCKET TO
* ESTABLISHED STATE. If it will be dropped after
* socket is created, wait for troubles.
*/
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
goto listen_overflow;
.......
listen_overflow:
if (!sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = 1;
return NULL;
}
......
}
在listen_overflow處,會設定inet_request_sock的acked成員,該標誌設定時表示已接收到第三次握手的ACK段,但是由於伺服器繁忙或其他原因導致未能建立起連線,此時可根據該標誌重新給客戶端傳送SYN+ACK段,再次進行連線的建立。具體檢查是否需要重傳是在syn_ack_recalc()函式中進行的,其程式碼如下所示:
[cpp] view plaincopyprint?- /* Decide when to expire the request and when to resend SYN-ACK */
- staticinlinevoid syn_ack_recalc(struct request_sock *req, constint thresh,
- constint max_retries,
- const u8 rskq_defer_accept,
- int *expire, int *resend)
- {
- if (!rskq_defer_accept) {
- *expire = req->retrans >= thresh;
- *resend = 1;
- return;
- }
- *expire = req->retrans >= thresh &&
- (!inet_rsk(req)->acked || req->retrans >= max_retries);
- /*
- * Do not resend while waiting for data after ACK,
- * start to resend on end of deferring period to give
- * last chance for data or ACK to create established socket.
- */
- *resend = !inet_rsk(req)->acked ||
- req->retrans >= rskq_defer_accept - 1;
- }
/* Decide when to expire the request and when to resend SYN-ACK */
static inline void syn_ack_recalc(struct request_sock *req, const int thresh,
const int max_retries,
const u8 rskq_defer_accept,
int *expire, int *resend)
{
if (!rskq_defer_accept) {
*expire = req->retrans >= thresh;
*resend = 1;
return;
}
*expire = req->retrans >= thresh &&
(!inet_rsk(req)->acked || req->retrans >= max_retries);
/*
* Do not resend while waiting for data after ACK,
* start to resend on end of deferring period to give
* last chance for data or ACK to create established socket.
*/
*resend = !inet_rsk(req)->acked ||
req->retrans >= rskq_defer_accept - 1;
}
在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,到這裡我們的分析算是徹底結束了,在分析的過程中忽略了一些細節的東西,感興趣的可以自己結合原始碼看一看。