探析如何使用SystemTap觀測TCP?Backlog
什么是TCP Backlog
本文所使用的Linux內(nèi)核版本信息
5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
backlog的中文含義是 積壓
的意思,在Linux網(wǎng)絡(luò)中,意味著網(wǎng)絡(luò)數(shù)據(jù)包的積壓,在Linux表現(xiàn)為半連接隊(duì)列和全連接隊(duì)列存儲(chǔ)這些積壓的數(shù)據(jù)包。backlog參數(shù)的大小,則會(huì)影響半連接隊(duì)列和全連接隊(duì)列緩存數(shù)據(jù)包的多少。
其中,半連接隊(duì)列和全連接隊(duì)列的含義如圖所示(此處引用張師傅博客中的圖)
- 半連接隊(duì)列(Incomplete connection queue),又稱 SYN 隊(duì)列
- 全連接隊(duì)列(Completed connection queue),又稱 Accept 隊(duì)列
從服務(wù)端角度看待TCP三次握手的過程,有以下幾步:
- 調(diào)用 listen 函數(shù)時(shí),TCP 的狀態(tài)被從 CLOSE 狀態(tài)變?yōu)?LISTEN,此時(shí)內(nèi)核就創(chuàng)建了半連接隊(duì)列和全連接隊(duì)列。backlog參數(shù)就是在
listen
的時(shí)候指定的。
int listen(int sockfd, int backlog);
- 在TCP進(jìn)行三次握手的時(shí)候,收到SYN報(bào)文會(huì)先將數(shù)據(jù)包放到半連接隊(duì)列,然后發(fā)出SYN+ACK
- 接著當(dāng)收到對(duì)端的SYN+ACK的時(shí)候,再將這個(gè)連接請(qǐng)求的數(shù)據(jù)包移動(dòng)到全連接隊(duì)列,等待應(yīng)用程序通過
accept()
函數(shù)讀取。
我們可以通過listen函數(shù)傳入backlog參數(shù)值,且backlog參數(shù)值會(huì)影響到半連接隊(duì)列和全連接隊(duì)列的大小,但是我們?cè)撛趺从^測到最終操作系統(tǒng)使用的backlog的大小呢?又怎么觀測到半連接隊(duì)列、全連接隊(duì)列中的緩存的包數(shù)量呢?backlog參數(shù)和半連接隊(duì)列、全連接隊(duì)列的大小之間又有什么關(guān)系呢?
實(shí)驗(yàn)環(huán)境搭建
先在本地電腦上啟動(dòng)了兩個(gè)虛擬機(jī),Linux虛擬機(jī)1(命名為L1,ip: 10.211.55.6)和Linux虛擬機(jī)2(命名為L2,ip: 10.211.55.8),以 L1 作為服務(wù)器,L2作為客戶端。
觀測Linux最終采用的backlog大小
為確定backlog值通過listen函數(shù)設(shè)置進(jìn)去之后,操作系統(tǒng)最終采用的數(shù)值,可以通過systemtap工具來確定。安裝好systemtap工具之后,編寫探測腳本如下:
probe kernel.function("tcp_v4_conn_request") { tcphdr = __get_skb_tcphdr($skb); dport = __tcp_skb_dport(tcphdr); if (dport == 9090) { printf("reach here\n"); printf("socket struct: %s \n", $sk$); syn_qlen = @cast($sk, "struct inet_connection_sock")->icsk_accept_queue->qlen; max_backlog=$sk->sk_max_ack_backlog; printf("qlen: %d, max_backlog: %d \n", syn_len, max_backlog); } }
這個(gè)腳本做的事情,就是對(duì)linux中 tcp_v4_conn_request
這個(gè)內(nèi)核函數(shù)做了探針,只要調(diào)用到這個(gè)內(nèi)核函數(shù),且端口號(hào)為9090,就會(huì)執(zhí)行一系列的打印操作。其中,會(huì)將socket對(duì)象打印出來,也會(huì)將socket對(duì)象中的 sk_max_ack_backlog
變量打印出來,這個(gè)變量正是linux最終采用的backlog值。
將這個(gè)腳本放到機(jī)器L1中的任一用戶目錄下,腳本命名為 tcp_backlog.stp
,然后用命令執(zhí)行:
sudo stap -v tcp_backlog.stp
如果運(yùn)行成功,則會(huì)看到在終端上顯示正在運(yùn)行的提示:
此時(shí),為避免編程語言的干擾,用C語言準(zhǔn)備一段服務(wù)器的啟動(dòng)代碼,backlog值可以通過修改常量來更改,這里使用backlog值為20
// main.c #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/shm.h> #define MYPORT 9090 #define BACKLOG 20 #define BUFFER_SIZE 1024 int main() { ///定義sockfd int server_sockfd = socket(AF_INET,SOCK_STREAM, 0); ///定義sockaddr_in struct sockaddr_in server_sockaddr; server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(MYPORT); server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); ///bind,成功返回0,出錯(cuò)返回-1 if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1) { perror("bind"); exit(1); } ///listen,成功返回0,出錯(cuò)返回-1 if(listen(server_sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } ///客戶端套接字 char buffer[BUFFER_SIZE]; char message[100] = "已成功接收!"; struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); ///成功返回非負(fù)描述字,出錯(cuò)返回-1 int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length); if(conn<0) { perror("connect"); exit(1); } while(1) { memset(buffer,0,sizeof(buffer)); int size = read(conn, buffer, 1024); if(strcmp(buffer,"exit\n")==0) break; strncat(buffer, message, 100); fputs(buffer, stdout); write(conn,buffer,strlen(buffer)+1); } close(conn); close(server_sockfd); return 0; }
在L1上通過命令編譯sk.c 并啟動(dòng):
gcc main.c -o sk.o && ./sk.o
啟動(dòng)后,在L2上通過nc命令連接L1的9090端口:
nc 10.211.55.6 9090
接著觀察 tcp_backlog.stp
探針腳本的輸出:
可見此時(shí)使用的backlog值為20,通過這個(gè)方法,我們可以觀測到linux最終采用的 backlog值的大小是多少了。
<>系統(tǒng)變量對(duì)backlog大小的影響
backlog雖然可以通過listen設(shè)置進(jìn)去,但是按照張師傅的博客所說,最終的大小會(huì)受到操作系統(tǒng)的配置影響??赏ㄟ^sysctl命令查看這兩個(gè)系統(tǒng)變量:
sysctl net.ipv4.tcp_max_syn_backlog # net.ipv4.tcp_max_syn_backlog = 128 sysctl net.core.somaxconn # net.core.somaxconn = 4096
按照上述觀測的方法,函數(shù)傳入的backlog值分別在 小于128,大于128但小于4096,大于4096這三個(gè)區(qū)間取一個(gè)值。設(shè)置backlog大小為 20、200、6000,分別觀測操作系統(tǒng)最終采用的backlog值如下:
listen backlog值為200時(shí),操作系統(tǒng)采用的backlog值為200
listen backlog值為6000時(shí),操作系統(tǒng)采用的backlog值為4096,和系統(tǒng)變量 net.core.somaxconn
保持一樣。
將上述測試數(shù)據(jù)總結(jié)如下:
listen backlog值 | 操作系統(tǒng)實(shí)際采用的backlog值 | |
---|---|---|
20 | 20 | |
200 | 200 | |
6000 | 4096 |
在張師傅的博客中提到, Linux內(nèi)核版本在3.10.0的時(shí)候,會(huì)受到 net.ipv4.tcp_max_syn_backlog
和 net.core.somaxconn
的影響,且受這兩個(gè)變量影響的邏輯還比較復(fù)雜。但是在 5.15.0版本中,已經(jīng)做了簡化,代碼如下:
// net/socket.c int __sys_listen(int fd, int backlog) { struct socket *sock; int err, fput_needed; int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { # sysctl_somaxconn對(duì)應(yīng)系統(tǒng)變量net.core.somaxconn的值 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; }
再簡化一下核心邏輯,核心邏輯的偽代碼如下:
backlog = listen_backlog; somaxconn = valuOf(`net.core.somaxconn`); if(backlog > somaxconn) { backlog = somaxconn; }
按張師傅的博客所說,在內(nèi)核版本為3.10.0中, backlog 值會(huì)在這個(gè)時(shí)候依次傳遞給 __sys_listen() -> inet_listen()->inet_csk_listen_start()->reqsk_queue_alloc(),最終在 reqsk_queue_alloc函數(shù)中根據(jù)這兩個(gè)系統(tǒng)變量經(jīng)歷一系列復(fù)雜的計(jì)算,最終得到操作系統(tǒng)使用的backlog值。但是這些操作,在5.x版本的內(nèi)核都去掉了,reqsk_queue_alloc函數(shù)中不再對(duì)backlog做過任何處理:
// net/ipv4/inet_connection_sock.c // 在這個(gè)函數(shù)中,雖然傳入了backlog,但是在后續(xù)的處理中完全沒有用上,由此證明backlog的賦值,在 __sys_listen 函數(shù)中已經(jīng)完成 int inet_csk_listen_start(struct sock *sk, int backlog) { struct inet_connection_sock *icsk = inet_csk(sk); struct inet_sock *inet = inet_sk(sk); int err = -EADDRINUSE; reqsk_queue_alloc(&icsk->icsk_accept_queue); sk->sk_ack_backlog = 0; inet_csk_delack_init(sk); /* There is race window here: we announce ourselves listening, * but this transition is still not validated by get_port(). * It is OK, because this socket enters to hash table only * after validation is complete. */ inet_sk_state_store(sk, TCP_LISTEN); if (!sk->sk_prot->get_port(sk, inet->inet_num)) { inet->inet_sport = htons(inet->inet_num); sk_dst_reset(sk); err = sk->sk_prot->hash(sk); if (likely(!err)) return 0; } inet_sk_set_state(sk, TCP_CLOSE); return err; }
// net/core/request_sock.c void reqsk_queue_alloc(struct request_sock_queue *queue) { spin_lock_init(&queue->rskq_lock); spin_lock_init(&queue->fastopenq.lock); queue->fastopenq.rskq_rst_head = NULL; queue->fastopenq.rskq_rst_tail = NULL; queue->fastopenq.qlen = 0; queue->rskq_accept_head = NULL; }
觀測半連接隊(duì)列大小
在三次握手的過程中,服務(wù)端收到握手請(qǐng)求包之后,會(huì)先把它放到半連接隊(duì)列中,然后回復(fù)SYN+ACK。接著接收到客戶端返回的ACK報(bào)文時(shí),再把這個(gè)數(shù)據(jù)包從半連接隊(duì)列移動(dòng)到全連接隊(duì)列中。在正常情況下,SYN報(bào)文在半連接隊(duì)列逗留的時(shí)間會(huì)很快,觀測半連接隊(duì)列大小要做點(diǎn)處理。
按照張師傅博客提供的方法,可以在客戶端設(shè)置防火墻,把服務(wù)端返回的ACK包都扔掉,這樣在服務(wù)端就不會(huì)收到ACK報(bào)文了。
// 在L2機(jī)器上設(shè)置這條防火墻規(guī)則 sudo iptables --append INPUT --match tcp --protocol tcp --src 10.211.55.6 --sport 9090 --tcp-flags SYN SYN --jump DROP // 查看防火墻規(guī)則是否設(shè)置成功 sudo iptables -L
接著用上述的服務(wù)端代碼啟動(dòng)服務(wù)后,在L2上通過nc命令連接上:
nc 10.211.55.6 9090
接著可以通過以下命令觀察到,當(dāng)前有多少個(gè)連接處于SYN_RECV狀態(tài):
sudo netstat -lnpa | grep :9090 | awk '{print $6}' | sort | uniq -c | sort -rn
處于SYN_RECV狀態(tài)的連接,意味著接收到了客戶端的SYN報(bào)文但未接收到ACK報(bào)文。此時(shí)連接就處于SYN_RECV狀態(tài)。通過這個(gè)點(diǎn)可以觀測到半連接隊(duì)列此時(shí)的大小是多少。你也可以在L2上通過程序發(fā)起多次連接,看看SYN_RECV狀態(tài)的連接數(shù)是否有變化,此處就不再敘述了。
觀測全連接隊(duì)列大小
當(dāng)請(qǐng)求收到ACK之后,就會(huì)從半連接隊(duì)列挪到全連接隊(duì)列,此時(shí)連接已經(jīng)完全建立,連接狀態(tài)就會(huì)從LISTEN
變成ESTABLISHED
狀態(tài),等待應(yīng)用程序調(diào)用accept
函數(shù)從全連接隊(duì)列中取走數(shù)據(jù)。所以,要觀察全連接隊(duì)列的大小,只要觀察ESTABLISHED
狀態(tài)的連接數(shù)即可。同樣可以采用netstat命令:
netstat -lnpa | grep :9090 | awk '{print $6}' | sort | uniq -c | sort -rn
也可以使用ss命令來進(jìn)行觀測。使用命令如下:
ss -lnt | grep :9090
- 處于 LISTEN 狀態(tài)的 socket,Recv-Q 表示 accept 隊(duì)列排隊(duì)的連接個(gè)數(shù),Send-Q 表示全連接隊(duì)列(也就是 accept 隊(duì)列)的總大小
- 對(duì)于非 LISTEN 狀態(tài)的 socket,Recv-Q 表示 receive queue 的字節(jié)大小,Send-Q 表示 send queue 的字節(jié)大小
總結(jié)
SystemTap是一個(gè)很有力的工具,用好這個(gè)工具,可以實(shí)實(shí)在在地觀測到Linux內(nèi)部的狀態(tài),讓自己對(duì)操作系統(tǒng)有個(gè)更深刻的認(rèn)識(shí)。
以上就是使用SystemTap觀測TCP Backlog過程解析的詳細(xì)內(nèi)容,更多關(guān)于SystemTap觀測TCP Backlog的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一鍵GHOST V8.3 Build 060425 硬盤版一鍵ghost使用方法說明[圖文詳細(xì)教程]
一鍵GHOST V8.3 Build 060425 硬盤版一鍵ghost使用方法說明[圖文詳細(xì)教程]...2007-01-01windows 系統(tǒng)防火墻 添加端口號(hào)方法
這篇文章主要介紹了windows 系統(tǒng)防火墻 添加端口號(hào)方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09Windows 2003如何轉(zhuǎn)換成PC版系統(tǒng)
Windows 2003如何轉(zhuǎn)換成PC版系統(tǒng)...2007-02-02華為鴻蒙OS之HelloWorld的實(shí)現(xiàn)
這篇文章主要介紹了華為鴻蒙OS之HelloWorld的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09DevEco Studio 2.0開發(fā)鴻蒙HarmonyOS應(yīng)用初體驗(yàn)全面測評(píng)(推薦)
這篇文章主要介紹了DevEco Studio 2.0開發(fā)鴻蒙HarmonyOS應(yīng)用初體驗(yàn)全面測評(píng),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09華為鴻蒙DevEco studio2.0的安裝和hello world運(yùn)行教程
這篇文章主要介紹了關(guān)于華為鴻蒙DevEco studio2.0的安裝和運(yùn)行第一個(gè)hello world的文章教程詳解,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-09-09XP系統(tǒng)和Ghost安裝版優(yōu)缺點(diǎn)比較
不少論壇或個(gè)人都推出了Ghost版系統(tǒng),裝機(jī)超簡單,節(jié)省時(shí)間,在網(wǎng)管和裝機(jī)商等人群中流傳極廣,只要用Ghost“恢復(fù)”一下,軟件、驅(qū)動(dòng)、補(bǔ)丁就全了,這其中最著名的應(yīng)該就是“番茄花園”了。2008-03-03