Linux-服務器的全連接隊列(Accept隊列)
上篇文章:一文講解從Linux內(nèi)核角度分析服務器Listen細節(jié)分析了服務器Listen的底層細節(jié),其中也分析了Listen系統(tǒng)調(diào)用的backlog參數(shù),其決定了服務器Listen過程中全連接隊列(Accept隊列)的最大長度。本文將更進一步分析全連接隊列(Accept隊列)以及backlog參數(shù)是如何影響中全連接隊列(Accept隊列)的,并通過小實驗直觀了解backlog參數(shù)對全連接隊列(Accept隊列)的影響。
全連接隊列是什么?
全連接隊列存儲3次握手成功并已建立的連接,將其稱為全連接隊列,也可稱為接收隊列(Accept隊列),本文中的描述將稱為Accept隊列或全連接隊列。如下紅框中所示,全連已成功建立三次握手,當前的TCP狀態(tài)為ESTABLISHED,但是服務端還未Accept的隊列。

那么這全連接隊列(Accept隊列)** 在Linux內(nèi)核中用什么數(shù)據(jù)結(jié)構(gòu)進行表示?**
連接請求塊- 存儲隊列
在介紹Accept隊列前先看一下連接請求塊:存儲相關(guān)連接請求的隊列的結(jié)構(gòu)體
連接請求塊的存儲隊列是對SYN同步隊列(半連接)隊列(服務端收到客戶端SYN請求并回復SYN+ACK的隊列)、接收(全連接)隊列的描述。在Linux內(nèi)核中使用request_sock_queue 進行表示,如下結(jié)構(gòu)體所示:
該結(jié)構(gòu)體描述了兩種隊列的相關(guān)信息,第一個是半連接隊列的長度,使用atomic_t qlen來表示,第二個是Accept隊列鏈表,使用struct request_sock *rskq_accept_head;來表示 Accept隊列鏈表的頭部,struct request_sock *rskq_accept_tail;表示Accept隊列鏈表的尾部。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ? ??


由連接請求塊-存儲隊列的結(jié)構(gòu)體可以看到全連接隊列-Accept隊列由struct request_sock結(jié)構(gòu)體進行表示,如下所示,服務器端收到SYN請求之后,內(nèi)核會建立連接請求塊(req)
結(jié)構(gòu)體成員變量?request_sock *dl_next
?指向隊列中下一個Accept隊列節(jié)點,Accept隊列與存儲隊列直接的關(guān)系如下圖所示:

在上一篇文章中:Linux內(nèi)核角度分析服務器Listen細節(jié)分析服務器listen函數(shù)調(diào)用時,發(fā)現(xiàn)到listen()
將調(diào)用inet_csk_listen_start()
,后者將調(diào)用reqsk_queue_alloc()
創(chuàng)建struct request_sock queue icsk_accept_queue
,即創(chuàng)建存儲隊列的結(jié)構(gòu)體。然后進行一些隊列長度相關(guān)參數(shù)的設(shè)定。
在分析長度相關(guān)參數(shù)的設(shè)置代碼之前,回顧一下用戶傳入的backlog參數(shù)在內(nèi)核中最終如何取值的,如下代碼所示,內(nèi)核變量backlog的最終取值為?backlog = Min(用戶傳入的backlog值,somaxconn),其中somaxconn的值是Linux系統(tǒng)的默認值:128,該值可以通過?/proc/sys/net/core/somaxconn進行設(shè)置? 。經(jīng)常會有一個問題:Listen時backlog參數(shù)越大,Accept隊列會越大嗎?
從上面的分析也可以看出來答案,內(nèi)核中backlog變量的最終取值是Listen系統(tǒng)調(diào)用傳入的backlog與系統(tǒng)默認值兩者之間的最小值,?所以在Listen時backlog的需求超過系統(tǒng)默認值128時,需要修改系統(tǒng)默認值以滿足更大的需求。
下面分析長度相關(guān)參數(shù)的設(shè)置,如下代碼節(jié)選自inet_csk_listen_start函數(shù),sk_max_ack_backlog
是對Accept隊列最大長度進行限制,sk_ack_backlog
是對當前Accept隊列的長度進行計數(shù),最開始初始化為0,也就是計數(shù)從0開始。
下面舉例分析一下Accept隊列并分析sk_ack_backlog如何對Accept隊列進行計數(shù)、sk_max_ack_backlog = backlog如何對Accept接收隊列長度的限制:
1、服務器收到客戶端三次握手最后一個ACK時:
收到客戶端最后一個ACK后·,服務器調(diào)用tcp_v4_rcv->tcp_v4_syn_rcv_sock,然后通過tcp_check_req函數(shù)進行檢查,如果一切檢查正常的話,使用回調(diào)syn_recv_sock處理去創(chuàng)建子套接口(child),之后由函數(shù)inet_csk_complete_hashdance中設(shè)置req->sk = child,然后將req放入全連接隊列icsk_accept_queue里面。
如下是tcp_check_req函數(shù):
syn_recv_sock對應的回調(diào)函數(shù)首先是對Accept隊列進行判斷:當前的Accept隊列是否滿,未滿的情況下才會去創(chuàng)建子套接口
判斷隊列是否滿:sk_acceptq_is_full(sk),從此也看到了sk_max_ack_backlog對Accept接收隊列的限制。
inet_csk_complete_hashdance將創(chuàng)建好的子套接口添加到Accept隊列如下:
inet_csk_complete_hashdance函數(shù)的參數(shù):own_req僅當tcp_check_req函數(shù)中成功創(chuàng)建child子套接口才會為真。inet_csk_reqsk_queue_add函數(shù)會將設(shè)置req->sk = child,然后將req放入Accept隊列icsk_accept_queue里面如下所示:
2、服務端接收到客戶端最后一個ACK并加入Accept隊列后
? ? ? 服務器Accept獲取Accept隊列的請求套接口,并刪除該請求套接口時
在1、中也提到:收到客戶端最后一個ACK后·,服務器調(diào)用tcp_v4_rcv->tcp_v4_syn_rcv_sock,然后通過tcp_check_req函數(shù)進行檢查,如果一切檢查正常的話,使用回調(diào)syn_recv_sock處理去創(chuàng)建子套接口,之后由函數(shù)inet_csk_complete_hashdance將子套接口添加到ACCEPT隊列中。那么當添加Accept接收隊列后,就要進行隊列的計數(shù),inet_csk_reqsk_queue_add函數(shù)調(diào)用sk_accepttq_added(sk):
子套接口接入到Accept隊列后調(diào)用該函數(shù)進行:sk->sk_ack_backlog++
,從而對隊列進行計數(shù)管理。
并且當服務執(zhí)行accept()后,accept()將返回已建立的連接,此時需要刪除該請求套接口,刪除過程如下:
函數(shù)reqsk_queue_remove為簡單的鏈表移除單個元素的操作,rskq_accept_head為鏈表的頭,注意ACCEPT隊列總是從頭部開始移除隊列中的子套接口元素,即用戶層的accept操作總是取走隊列中的第一個子套接口,如下圖所示,綠色的線即頭部重新指向被移除的next元素。

reqsk_queue_remove函數(shù)完成以上移除隊列元素的操作之前執(zhí)行:?sk_acceptq_remove(parent)
的操作,如下所示:
sk->sk_ack_backlog--
操作對隊列元素的個數(shù)進行更新
綜上分析,sk_ack_backlog是對Accept接收隊列的計數(shù),sk_max_ack_backlog限制了Accept接收隊列的最大長度,sk_max_ack_backlog也正是Listen系統(tǒng)調(diào)用傳入的參數(shù)backlog。
小實驗
1、首先創(chuàng)建一個服務端:
服務端開啟Listen,并設(shè)置Listen函數(shù)的backlog參數(shù)為5,即全連接隊列最大長度只能到6(由上面的內(nèi)核分析也可知,sk_ack_backlog對Accept隊列的計數(shù)是從0開始的,長度限制變量sk_max_ack_backlog就是Min(用戶傳入的backlog,系統(tǒng)默認值128),也就是說sk_max_ack_backlog為5,也就是說最大長度為6),重要的一點是服務端不進行Accept處理:
2、運行服務端程序:

3、編寫客戶端程序:要求向服務端發(fā)起多次連接(大于6次),使用Go語言編寫的客戶端程序如下,并發(fā)10個去連接服務端
4、運行客戶端,可以看到連接了6次

通過ss命令:ss -nlt
-l:?顯示正在監(jiān)聽(Listening)的socket
-n :不解析服務器名稱
-t :只顯示 tcp socket
Recv-Q:當前全連接隊列的大小,也就是當前已完成三次握手并等待服務端 accept() 的 TCP 連接;
Send-Q:當前全連接最大隊列長度(從0開始計數(shù)),上面服務器的最大全連接長度為6(0~5);

可以看到服務端 127.0.0.1:5200的Send-Q為5(0~5),即最大全連接長度為6,Recv-Q是當前的Accept隊列的長度為6。10個并行連接只有6個成功完成3次握手,剩下4個都未完成三次握手。說明TCP 全連接隊列過小,就容易溢出,當發(fā)生 TCP 全連接隊溢出的時候,后續(xù)的請求就會被丟棄。
