|
點擊上方“C語言與CPP編程”,選擇“關注/置頂/星標公眾號”
干貨福利,第一時間送達!
最近有小伙伴說沒有收到當天的文章推送,這是因為微信更改了推送機制,導致沒有星標公眾號的小伙伴刷不到當天推送的文章,無法接收到一些比較實用的知識和資訊。所以建議大家加個星標??,以后就能第一時間收到推送了。
0y3b54tfogw64027523218.png (399.52 KB, 下載次數(shù): 1)
下載附件
保存到相冊
0y3b54tfogw64027523218.png
2024-11-27 01:07 上傳
原文:https://segmentfault.com/a/1190000021488755
最近遇到一個問題,簡化模型如下:
tpjclnzofiz64027523318.png (21.94 KB, 下載次數(shù): 0)
下載附件
保存到相冊
tpjclnzofiz64027523318.png
2024-11-27 01:07 上傳
Client 創(chuàng)建一個 TCP 的 socket,并通過 SO_SNDBUF 選項設置它的發(fā)送緩沖區(qū)大小為 4096 字節(jié),連接到 Server 后,每 1 秒發(fā)送一個 TCP 數(shù)據(jù)段長度為 1024 的報文。Server 端不調(diào)用 recv()。預期的結果分為以下幾個階段:
Phase 1 Server 端的 socket 接收緩沖區(qū)未滿,所以盡管 Server 不會 recv(),但依然能對 Client 發(fā)出的報文回復 ACK;
Phase 2 Server 端的 socket 接收緩沖區(qū)被填滿了,向 Client 端通告零窗口(Zero Window)。Client 端待發(fā)送的數(shù)據(jù)開始累積在 socket 的發(fā)送緩沖區(qū);
Phase 3 Client 端的 socket 的發(fā)送緩沖區(qū)滿了,用戶進程阻塞在 send() 上。
實際執(zhí)行時,表現(xiàn)出來的現(xiàn)象也"基本"符合預期。
不過當我們在 Client 端通過 ss -nt 不時監(jiān)控 TCP 連接的發(fā)送隊列長度時,發(fā)現(xiàn)這個值竟然從 0 最終增長到 14480,它輕松地超了之前設置的 SO_SNDBUF 值(4096)
# ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 1024 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 2048 192.168.183.130:52454 192.168.183.130:14465
......
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 13312 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 14336 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 14480 192.168.183.130:52454 192.168.183.130:14465有必要解釋一下這里的 Send-Q 的含義。我們知道,TCP 是的發(fā)送過程是受到滑動窗口限制。
chuz3q1ffhi64027523418.png (26.71 KB, 下載次數(shù): 2)
下載附件
保存到相冊
chuz3q1ffhi64027523418.png
2024-11-27 01:07 上傳
這里的 Send-Q 就是發(fā)送端滑動窗口的左邊沿到所有未發(fā)送的報文的總長度。
那么為什么這個值超過了 SO_SNDBUF 呢?
雙倍 SO_SNDBUF當用戶通過 SO_SNDBUF 選項設置套接字發(fā)送緩沖區(qū)時,內(nèi)核將其記錄在 sk->sk_sndbuf 中。
@sock.c: sock_setsockopt
{
case SO_SNDBUF:
.....
sk->sk_sndbuf = mat_x(u32, val * 2, SOCK_MIN_SNDBUF)
}注意,內(nèi)核在這里玩了一個小 trick,它在 sk->sk_sndbuf 記錄的的不是用戶設置的 val, 而是 val 的兩倍!
也就是說,當 Client 設置 4096 時,內(nèi)核記錄的是 8192 !
那么,為什么內(nèi)核需要這么做呢?我認為是因為內(nèi)核用 sk_buff 保存用戶數(shù)據(jù)有額外的開銷,比如 sk_buff 結構本身、以及 skb_shared_info 結構,還有 L2、L3、L4 層的首部大小.這些額外開銷自然會占據(jù)發(fā)送方的內(nèi)存緩沖區(qū),但卻不應該是用戶需要 care 的,所以內(nèi)核在這里將這個值翻個倍,保證即使有一半的內(nèi)存用來存放額外開銷,也能保證用戶的數(shù)據(jù)有足夠內(nèi)存存放。
但是,問題現(xiàn)象還不能解釋,因為即使是 8192 字節(jié)的發(fā)送緩沖區(qū)內(nèi)存全部用來存放用戶數(shù)據(jù)(額外開銷為 0,當然這是不可能的),也達不到 Send-Q 最后達到的 14480 。
sk_wmem_queued既然設置了 sk->sk_sndbuf, 那么內(nèi)核就會在發(fā)包時檢查當前的發(fā)送緩沖區(qū)已使用內(nèi)存值是否超過了這個限制,前者使用 sk->wmem_queued 保存。
需要注意的是,sk->wmem_queued = 待發(fā)送數(shù)據(jù)占用的內(nèi)存 + 額外開銷占用的內(nèi)存,所以它應該大于 Send-Q
@sock.h
bool sk_stream_memory_free(const struct sock* sk)
{
if (sk->sk_wmem_queued >= sk->sk_sndbuf) // 如果當前 sk_wmem_queued 超過 sk_sndbuf,則返回 false,表示內(nèi)存不夠了
return false;
.....
}sk->wmem_queued 是不斷變化的,對 TCP socket 來說,當內(nèi)核將 skb 塞入發(fā)送隊列后,這個值增加 skb->truesize (truesize 正如其名,是指包含了額外開銷后的報文總大小);而當該報文被 ACK 后,這個值減小 skb->truesize。
tcp_sendmsg以上都是鋪墊,讓我們來看看 tcp_sendmsg 是怎么做的?偟膩碚f內(nèi)核會根據(jù)發(fā)送隊列(write queue)是否有待發(fā)送的報文,決定是 創(chuàng)建新的 sk_buff,或是將用戶數(shù)據(jù)追加(append)到 write queue 的最后一個 sk_buff
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
mss_now = tcp_send_mss(sk, &size_goal, flags);
// code committed
while (msg_data_left(msg)) {
int copy = 0;
int max = size_goal;
skb = tcp_write_queue_tail(sk);
if (tcp_send_head(sk)) {
......
copy = max - skb->len;
}
if (copy 0) {
/* case 1:alloc new skb */
new_segment:
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf; // 如果發(fā)送緩沖區(qū)滿了 就阻塞進程 然后睡眠
skb = sk_stream_alloc_skb(sk,
select_size(sk, sg),
sk->sk_allocation,
skb_queue_empty(&sk->sk_write_queue));
}
......
/* case 2:copy msg to last skb */
......
}Case 1.創(chuàng)建新的 sk_buff在我們這個問題中,Client 在 Phase 1 是不會累積 sk_buff 的。也就是說,這時每個用戶發(fā)送的報文都會通過 sk_stream_alloc_skb 創(chuàng)建新的 sk_buff。
在這之前,內(nèi)核會檢查發(fā)送緩沖區(qū)內(nèi)存是否已經(jīng)超過限制,而在Phase 1 ,內(nèi)核也能通過這個檢查。
static inline bool sk_stream_memory_free(const struct sock* sk)
{
if (sk-?sk_wmem_queued >= sk->sk_sndbuf)
return false;
......
}Case 2.將用戶數(shù)據(jù)追加到最后一個 sk_buff而在進入 Phase 2 后,Client 的發(fā)送緩沖區(qū)已經(jīng)有了累積的 sk_buff,這時,內(nèi)核就會嘗試將用戶數(shù)據(jù)(msg中的內(nèi)容)追加到 write queue 的最后一個 sk_buff。
需要注意的是,這種搭便車的數(shù)據(jù)也是有大小限制的,它用 copy 表示
@tcp_sendmsg
int max = size_goal;
copy = max - skb->len;這里的 size_goal 表示該 sk_buff 最多能容納的用戶數(shù)據(jù),減去已經(jīng)使用的 skb->len, 剩下的就是還可以追加的數(shù)據(jù)長度。
那么 size_goal 是如何計算的呢?
tcp_sendmsg
|-- tcp_send_mss
|-- tcp_xmit_size_goal
static unsigned int tcp_xmit_size_goal(struct sock* sk, u32 mss_now, int large_allowed)
{
if (!large_allowed || !sk_can_gso(sk))
return mss_now;
.....
size_goal = tp->gso_segs * mss_now;
.....
return max(size_goal, mss_now);
}繼續(xù)追蹤下去,可以看到,size_goal 跟使用的網(wǎng)卡是否使能了 GSO 功能有關。
GSO Enable:size_goal = tp->gso_segs * mss_nowGSO Disable: size_goal = mss_now
在我的實驗環(huán)境中,TCP 連接的有效 mss_now 是 1448 字節(jié),用 systemtap 加了探測點后,發(fā)現(xiàn) size_goal 為 14480 字節(jié)!是 mss_now 的整整 10 倍。
所以當 Clinet 進入 Phase 2 時,tcp_sendmsg 計算出 copy = 14480 - 1024 = 13456 字節(jié)。
可是最后一個 sk_buff 真的能裝這么多嗎?
在實驗環(huán)境中,Phase 1 階段創(chuàng)建的 sk_buff ,其 skb->len = 1024, skb->truesize = 4372 (4096 + 256,這個值的詳細來源請看 sk_stream_alloc_skb)
這樣看上去,這個 sk_buff 也容納不下 14480 啊。
再繼續(xù)看內(nèi)核的實現(xiàn),再 skb_copy_to_page_nocache() 拷貝之前,會進行 sk_wmem_schedule()
tcp_sendmsg
{
/* case 2:copy msg to last skb */
......
if (!sk_wmem_schedule(sk, copy))
goto wait_for_memory;
err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
pfrag->page,
pfrag->offset,
copy);
}而在 sk_wmem_schedule 內(nèi)部,會進行 sk_buff 的擴容(增大可以存放的用戶數(shù)據(jù)長度).
tcp_sendmsg
|--sk_wmem_schedule
|-- __sk_mem_schedule
__sk_mem_schedule(struct sock* sk, int size, int kind)
{
sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
allocated = sk_memory_allocated_add(sk, amt, &parent_status);
......
// 后面有一堆檢查,比如如果系統(tǒng)內(nèi)存足夠,就不去看他是否超過 sk_sndbuf
}通過這種方式,內(nèi)核可以讓 sk->wmem_queued 在超過 sk->sndbuf 的限制。
我并不覺得這樣是優(yōu)雅而合理的行為,因為它讓用戶設置的 SO_SNDBUF 形同虛設!那么我可以增么修改呢?
關掉網(wǎng)卡 GSO 特性修改內(nèi)核代碼, 將檢查發(fā)送緩沖區(qū)限制移動到 while 循環(huán)的開頭。
while (msg_data_left(msg)) {
int copy = 0;
int max = size_goal;
+ if (!sk_stream_memory_free(sk))
+ goto wait_for_sndbuf;
skb = tcp_write_queue_tail(sk);
if (tcp_send_head(sk)) {
if (skb->ip_summed == CHECKSUM_NONE)
max = mss_now;
copy = max - skb->len;
}
if (copy - if (!sk_stream_memory_free(sk))
- goto wait_for_sndbuf;——EOF——你好,我是飛宇。日常分享C/C++、計算機學習經(jīng)驗、工作體會,歡迎點擊此處查看我以前的學習筆記&經(jīng)驗&分享的資源。
我組建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起進群交流。
i0dgmvxvfxr64027523518.png (195.91 KB, 下載次數(shù): 1)
下載附件
保存到相冊
i0dgmvxvfxr64027523518.png
2024-11-27 01:07 上傳
歡迎你添加我的微信,我拉你進技術交流群。此外,我也會經(jīng)常在微信上分享一些計算機學習經(jīng)驗以及工作體驗,還有一些內(nèi)推機會。
lxapwnphonv64027523618.png (281.08 KB, 下載次數(shù): 1)
下載附件
保存到相冊
lxapwnphonv64027523618.png
2024-11-27 01:07 上傳
加個微信,打開另一扇窗
經(jīng)常遇到有讀者后臺私信想要一些編程學習資源,這里分享 1T 的編程電子書、C/C++開發(fā)手冊、Github上182K+的架構路線圖、LeetCode算法刷題筆記等精品學習資料,點擊下方公眾號會回復"編程"即可免費領取~
感謝你的分享,點贊,在看三連
jg0jyt5t02064027523718.gif (88.16 KB, 下載次數(shù): 0)
下載附件
保存到相冊
jg0jyt5t02064027523718.gif
2024-11-27 01:07 上傳
|
|