一個 TCP 接收緩衝區問題的解析

文本做爲一個 TCP 發送緩衝區問題的解析的姊妹篇存在。此次說的是接收緩衝區的問題。html

問題模型

image

Clinet 與 Server 之間創建一條 TCP 鏈接,Server 經過 SO_RCVBUF 選項設置鏈接的接收緩衝區爲 2048 字節。Clinet 每隔 100 ms 經過 send() 一個載荷長度很小(2 字節)的 TCP 報文,但 Server 端調用 recv(),這意味着 Server 收到的 TCP 報文都會存放在接收緩衝區,而當接收緩衝區滿時,便應該向 Client 通告零窗口(Zero-Window)git

但實際狀況是:github

  1. Server 最後並無發送零窗口,它最小的通告窗口也有 874
  2. Server 沒有迴應 Clinet 的重傳報文,致使 Clinet 重傳次數過多後斷開

下面是抓包的結果算法

image

因此,爲何會這樣?segmentfault

雙倍接收緩衝區

與發送緩衝區的實現一致,當用戶經過 SO_RCVBUF 選項設置緩衝區大小時,內核會將這個設置值加倍,好比咱們在這裏設置 2048 字節,內核實際的緩衝區大小時 4096 字節緩存

case SO_RCVBUF:
        ......
        /*
         * We double it on the way in to account for
         * "struct sk_buff" etc. overhead.   Applications
         * assume that the SO_RCVBUF setting they make will
         * allow that much actual data to be received on that
         * socket.
         *
         * Applications are unaware that "struct sk_buff" and
         * other overheads allocate from the receive buffer
         * during socket buffer allocation.
         *
         * And after considering the possible alternatives,
         * returning the value we actually used in getsockopt
         * is the most desirable behavior.
         */
        sk->sk_rcvbuf = max_t(u32, val * 2, SOCK_MIN_RCVBUF);
        break;

這樣作的緣由註釋中有寫,是由於sk_buff有額外開銷(下面會細說),而接收緩衝區的大小是指用於緩存全部收到的sk_buff 的內存,它會包含額外開銷,因此這裏內核將這個值擴大一倍。app

sk上還有兩個字段十分重要,sk->sk_rmem_alloc表示當前已經使用的接收緩衝區內存,sk->forward_alloc表示預先向內核分配到的內存。這兩個字段有什麼關係呢?socket

打個比方,sk->forward_alloc就比如充值的點卡,sk->sk_rmem_alloc則用來記錄實際花銷。當須要花費內存時,內核老是先看sk->forward_alloc有沒有,若是沒有,則向系統申請內存,放到sk->forward_alloc裏,以後再花費時,發現sk->forward_alloc還有,就直接扣這裏面的就好了。而當sk->forward_alloc花光時,則又會從新充值.tcp

內核一次充值sk->forward_alloc的大小爲 1 個 PAGESIZE,通常爲 4K。ide

sk_buff 究竟佔據多大的空間

sk_buff的結構在 skbuff詳解或者skb_data中已經有詳細描述。我畫了一張圖

image

sk_buff分爲sk_buff結構自己、線性區域(linear area)和非線性區域(nonlinear area)。sk_buff結構自己的大小在不一樣版本內核略有不一樣,但通過 2 的整次冥向上取整以後都爲 256 Bytes,線性區域又分爲數據存儲區(skb->headskb->end之間的部分)和skb_shared_info區域, 整個區域大小根據建立sk_buff時請求的大小肯定,好比咱們的例子中的包含 2B 用戶數據的 TCP 報文,其線性區域大小通過向上取整後爲 512B (預留頭 64 Bytes + MAC頭14 Bytes + IP頭20 Bytes + TCP頭 32 Bytes + 用戶數據 2 Bytes + skb_shared_info結構 320 Bytes = 452 Bytes)。非線性區域不必定存在,若是存在則是掛在skb_shared_infofrag槽中,skb_shared_info一共有MAX_SKB_FRAGS+1個槽,在PAGESIZE=4K的狀況下,這個值爲 18。

三部分佔用的內存總和纔是一個sk_buff真正佔用的內存(也就是應該受到sk->rcvbuf限制的),它的值記錄在skb->truesize,而skb->datalen表示非線性區域用戶數據的長度,skb->len表示線性+非線性區域用戶數據的長度。

因此在咱們例子中,Server端收到的sk_buff的長度信息應該是

skb->truesize = 768 
skb->datalen  = 0
skb->len      = 2

Merge sk_buff

上面的例子中,一個sk_buff存儲 2 字節用戶數據,卻要佔用 768 字節的內存。若是收到的每一個報文都要這麼保存,那麼接收緩衝區會被當即耗盡。所以內核在接收隊列有sk_buff尚未被用戶讀取時,若是再收到sk_buff,會先嚐試將後一個sk_buff的數據放到接收隊列的最後一個sk_buff中。

static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
          bool *fragstolen)
{
    int eaten;
    struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);

    __skb_pull(skb, hdrlen);
    eaten = (tail && tcp_try_coalesce(sk, tail, skb, fragstolen)) ? 1 : 0;
    tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
    if (!eaten) {
        __skb_queue_tail(&sk->sk_receive_queue, skb);
        skb_set_owner_r(skb, sk);
    }
    return eaten;
}

sk_buff也不是隨意就能夠 Merge 的,它須要目標sk_buff有足夠的空間容納它的數據。

咱們知道sk_buff分爲線性區域和非線性區域,內核在合併sk_buff時,若是此時尚未使用非線性區域(skb_shared_info->nr_frag = 0),而且線性區域的剩餘空間能裝下新到的sk_buff,那麼就將用戶數據拷貝到目標sk_buff,不然再去嘗試佔用一個非線性區域中空閒的槽

那麼,在咱們的環境中,隊列尾的sk_buff線性區域還能添加多少用戶數據呢?

答案是 512 - 452 = 60 字節。也就是說,第 2 到第 31 個sk_buff能夠直接合併到第 1 個sk_buff的線性區域,這些報文並不會增長緩衝區的內存佔用sk->rmem_alloc,由於它們已經計算在sk->truesize了.

此時

tail_skb->truesize = 768 
tail_skb->datalen  = 0
tail_skb->len      = 62

sk->sk_rmem_alloc    = 768
sk->sk_forward_alloc = 3328

第 32 個報文時到達時,因爲線性區域已經滿了,它只能去佔據非線性區域的槽

那麼,它將佔用多少接收緩衝區的內存呢?

第 32 個報文的信息和此時 sk 的狀態爲

skb->truesize = 768
skb->datalen  = 0
skb->len = 34  // 這裏還有 32 字節的 TCP 首部

它將佔用除了skb->truesize - sizeof(struct sk_buff) = 512的接收緩衝區空間,換句話說,存入非線性區域能節省一個sk_buff結構的空間。

因此,第 32 個報文到達後,

tail_skb->truesize = 1280
tail_skb->datalen  = 2
tail_skb->len      = 64

sk->sk_rmem_alloc    = 1280
sk->sk_forward_alloc = 2816

以此類推,第 33 個報文到達時,會變成

tail_skb->truesize = 1792
tail_skb->datalen  = 4
tail_skb->len      = 66

sk->sk_rmem_alloc    = 1792
sk->sk_forward_alloc = 2304

到第 37 個報文,

tail_skb->truesize = 3840
tail_skb->datalen  = 12
tail_skb->len      = 74

sk->sk_rmem_alloc    = 3840
sk->sk_forward_alloc = 256

也就是說,sk->sk_rmem_alloc增長多少,sk->sk_forward_alloc就相應減小多少。

sk_forward_alloc 充值

sk->sk_forward_alloc = 256已經不夠一個skb->truesize = 768時,內核會調用下面的 tcp_try_rmem_schedule()

此時,sk->sk_rmem_alloc並無超過sk->sk_rcvbuf,因此下面的第一個條件並不知足,此時便會調用sk_rmem_schedule()sk->sk_forward_alloc進行充值

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb, unsigned int size)
{
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
        !sk_rmem_schedule(sk, skb, size)) {

        if (tcp_prune_queue(sk) < 0)
            return -1;
        // code omitted
    }
}

TCP prune(修剪)

以後的報文依然會會消耗sk->sk_forward_alloc,同時增長sk->sk_rmem_alloc相同的數值,甚至超過sk->sk_rcvbuf

而當sk->sk_forward_alloc再次耗盡,內核再次調用tcp_try_rmem_schedule()時,此時sk->sk_rmem_alloc已經超過了sk->sk_rcvbuf,所以內核會調用tcp_prune_queue對接收隊列上的報文進行修剪。

此次的手段是將非線性區域的用戶數據移動到線性區域。

但是線性區域不是滿了嗎?

內核採用的作法是將sk_buffheadroom 騰出來(不要預留區域、MAC頭、IP頭、TCP頭了),而後進行非線性區域到線性區域的移動。

好比在咱們的例子中,接收隊列的tail_skb的基本信息爲

prune 以前,

tail_skb->truesize = 7936
tail_skb->datalen  = 28
tail_skb->len      = 90
tail_skb->data - tail_skb->head = 130
tail_skb->end  - tail_skb->tail = 0

sk->sk_rmem_alloc    = 7936
sk->sk_forward_alloc = 256

而在 prune 以後,

tail_skb->truesize = 768
tail_skb->datalen  = 0
tail_skb->len      = 90
tail_skb->data - tail_skb->head =  0
tail_skb->end  - tail_skb->tail =  130

sk->sk_rmem_alloc    = 768
sk->sk_forward_alloc = 3328

通過這一番騰挪,tail_skb沒有了非線性區域,而且線性區域的尾部就又能夠裝 130 字節(65 個報文)。

而當又收到 65 個報文以後,tail_skb的線性區域又耗盡了,此後的報文又會開始往非線性區域累加。

直到sk->sk_rmem_alloc又接近sk->sk_rcvbuf

tail_skb->truesize = 7936
tail_skb->datalen  = 28
tail_skb->len      = 220
tail_skb->data - tail_skb->head =  0
tail_skb->end  - tail_skb->tail =  0

sk->sk_rmem_alloc    = 7936
sk->sk_forward_alloc = 256

這個時候內核會對tail_skb的線性空間進行擴容,擴容以後線性空間能容納 684 字節的用戶數據了(其中 220 字節已經被使用)

tail_skb->truesize = 1280
tail_skb->datalen  = 0
tail_skb->len      = 220
tail_skb->data - tail_skb->head =  0
tail_skb->end  - tail_skb->tail =  484

sk->sk_rmem_alloc    = 1280
sk->sk_forward_alloc = 2816

再這以後,又是先填充線性區域,再填充非線性區域,再填充和擴容.....

詭異的 TCP 的通告窗口

RFC 中規定 TCP 使用滑動窗口來接收報文,而且在首部中向對端通告可用的窗口大小

那麼應該通告多大的窗口呢? 不一樣內核有不一樣的實現

初始通告窗口來講。Linux 的初始通告窗口設置爲接收接收緩衝區的一半,且向下調整爲 MSS 的整數倍。在咱們的實驗環境中,MSS 爲 1448 Bytes,而接收緩衝區一半大小爲 4096/2 = 2048 Bytes,所以,Server 的初始通告窗口也就是 1448 Bytes。

<p align="center"><img src="/assets/img/sk_rcvbuf/window-init.PNG"></p>

一樣,後續的 TCP 報文也須要向對方通告接收窗口大小,但 RFC 1122 要求 TCP 還須要採起必定措施避免出現糊塗窗口綜合徵(SWS)

A TCP MUST include a SWS avoidance algorithm in the receiver.

RFC 1122 將接收緩衝區分爲了如下三部分:其中,RCV.USER爲收到而且已經ACK,但尚未被用戶讀取走的部分。RCV.WND爲通告的窗口大小,靜默狀態下(沒有報文傳輸時),RCV.USER=0,RCV.WND=RCV.BUFF.

|<---------- RCV.BUFF ---------------->|
                      1             2            3
                 |<-RCV.USER->|<--- RCV.WND ---->|
             ----|------------|------------------|------|----
                           RCV.NXT               
                                     

             1 - RCV.USER =  data received but not yet consumed;
             2 - RCV.WND =   space advertised to sender;
             3 - Reduction = space available but not yet advertised.
The solution to receiver SWS is to avoid advancing the right window edge RCV.NXT+RCV.WND in small increments, even if data is received from the network in small segments.

對於接收端來講,它應該避免小步幅(small increments)地推動窗口的右邊沿(RCV.NXT+RCV.WND),怎麼作呢? RFC 的推薦算法是

The suggested SWS avoidance algorithm for the receiver is to keep RCV.NXT+RCV.WND fixed until the reduction satisfies:
RCV.BUFF - RCV.USER - RCV.WND >= min( Fr * RCV.BUFF, Eff.snd.MSS )

Fr的推薦值爲 1/2。翻譯過來就是,當知足可用的緩衝區超過了 min(緩衝區的一半,有效MSS) 時,將RCV.WND設置爲RCV.BUFF-RCV.USER,不然接收端就不推動右邊沿。

回到咱們環境中, 咱們能夠發現 Linux 並無採用 RFC 的建議,除了初始接收窗口的選擇以外,因爲咱們 Server 沒有調用recv(),按照 RFC 的算法,當收到報文時,RCV.NXT 推動,窗口又邊沿不變,必然使得 RCV.WND 變小。

但從抓包結果來看,Server 通告的窗口大小變化趨勢是先在必定時間內保持不變,後來才逐漸減少,再不變一段時間,再減少....。

窗口選擇

爲何會這樣呢?

在內核__tcp_select_window()的註釋中是這麼寫的

/*
 * Unfortunately, the recommended algorithm breaks header prediction,
 * since header prediction assumes th->window stays fixed.
 *
 * Strictly speaking, keeping th->window fixed violates the receiver
 * side SWS prevention criteria. The problem is that under this rule
 * a stream of single byte packets will cause the right side of the
 * window to always advance by a single byte.
 */

意思是 Linux 內核本身的首部預測算法與 RFC 中建議的接收端糊塗窗口綜合徵避免算法自己就是有衝突的。而 Linux 選擇了使用首部預測算法。

什麼是首部預測?

在 Linux 內核中,有兩條路徑處理收到的報文:快速路徑(fast path)和慢速路徑(slow path)。正如其名,快速路徑處理預期中的報文,慢速路徑則能夠處理預期外的報文,好比亂序報文、緊急數據等。

Linux 老是傾向於走快速路徑,這須要報文知足預測條件,內核將預測報文存放在 tp->pred_flags, 該標誌預測收到報文的 TCP 首部中的 [11:14] 偏移的內容。

0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |  <---- tp->pred_flags 
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

能夠看到,通告窗口就在[11:14]範圍內,因此走快速的條件之一是收到報文的通告窗口不能變化

還有一個走快速路徑的條件是,收到報文的大小不能超過sk->sk_forward_alloc的剩餘值,若果超過,則說明sk 上預先分配的內存空間不夠了,須要走慢速路徑從新分配內存.

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
             const struct tcphdr *th, unsigned int len)
{
    // code omitted...
    if ((int)skb->truesize > sk->sk_forward_alloc)
        goto step5;
}

TCP 報文中通告窗口的值由 tcp_select_window()決定

static u16 tcp_select_window(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 old_win = tp->rcv_wnd;
    u32 cur_win = tcp_receive_window(tp);
    u32 new_win = __tcp_select_window(sk); // 計算新的窗口

    /* Never shrink the offered window */
    if (new_win < cur_win) {
        if (new_win == 0)
            NET_INC_STATS(sock_net(sk),
                      LINUX_MIB_TCPWANTZEROWINDOWADV);
        new_win = ALIGN(cur_win, 1 << tp->rx_opt.rcv_wscale);
    }
    
    // code omitted
}

上面的邏輯關鍵在於使用 __tcp_select_window(sk) 計算新的窗口大小。注意那個條件判斷,它的依據是

RFC 規定了 offered 過窗口不能 shrink,這並非說通告窗口的值只能變大不能變小,而是說滑動窗口右邊沿不能左移。比方說在某個時刻 Server 的通告窗口爲 1448,當它收到 X 字節用戶數據後,不容許將窗口減少爲比 1448 - X 還小的值。

來看看新的窗口是怎麼作的

u32 __tcp_select_window(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    /* MSS for the peer's data.  Previous versions used mss_clamp
     * here.  I don't know if the value based on our guesses
     * of peer's MSS is better for the performance.  It's more correct
     * but may be worse for the performance because of rcv_mss
     * fluctuations.  --SAW  1998/11/1
     */
    int mss = icsk->icsk_ack.rcv_mss;
    int free_space = tcp_space(sk);
    int allowed_space = tcp_full_space(sk);
    int full_space = min_t(int, tp->window_clamp, allowed_space);

這裏的 mss 取自icsk->icsk_ack.rcv_mss,這個值其實是估計值,用來估算對方的MSS,依據就是若是對方發送的最大的 TCP 報文中的載荷長度(在tcp_measure_rcv_mss()中更新),在咱們的環境中,

因爲 Clinet 一直都是發送的 2 字節的小包,因此這個值在咱們的環境中一直都是初始值 TCP_MSS_DEFAULT(536)

sysctl_tcp_adv_win_scale = 1時(默認值),allowed_space表示整個接收緩衝區的空間的一半,free_space則表示接收緩衝區中空閒空間的一半,

if (free_space < (allowed_space >> 4) || free_space < mss)
    return 0;

當空閒空間不到整個空間的 1/16 或者小於估算MSS時,本端計算出來的新窗口爲 0,但這只是計算出的值,TCP頭中通告窗口字段因爲 RFC 規定窗口不能收縮,所以並不必定真正會通告零窗口(Zero-Window)

接下來計算新窗口爲估算MSS的整數倍或者保持不變

/* Get the largest window that is a nice multiple of mss.
     * Window clamp already applied above.
     * If our current window offering is within 1 mss of the
     * free space we just keep it. This prevents the divide
         * and multiply from happening most of the time.
         * We also don't do any window rounding when the free space
         * is too small.
         */
        if (window <= free_space - mss || window > free_space)
            window = (free_space / mss) * mss;
        else if (mss == full_space &&
             free_space > window + (full_space >> 1))
            window = free_space;

問題發生的過程

前面鋪墊了這麼多,如今咱們來將它們串起來,看看本文最開始的問題是如何產生的。

Step 0 準備工做

經過 SO_RCVBUF 選項,設置接收緩衝區大小爲 2048 字節。內核將這個值加倍,因而sk->sk_rcvbuf = 4096,初始通告窗口爲 1448 字節。同時接收緩衝區內存消耗爲sk->sk_rmem_alloc = 0

Step 1 第一個用戶報文到達

第一個 skb (len = 2, truesieze = 768)到達時,此時接收隊列上沒有其餘 skb,因而直接掛在了隊列上,整個 skb 的報文內存佔用計入sk->rmem_alloc = 768.

Step 2 後續若干個用戶報文填入線性區域

後續若干個 skb 到達時,填入第一個 skb 的線性區域,sk->sk_rmem_alloc始終保持爲 768 字節,sk->sk_forward_alloc始終保持爲 3328 字節 。

對窗口來講,sk->sk_rmem_alloc 沒有變化,每次收到 2 字節數據,會讓計算出的cur_win = old_win - 2

old_win = 1448
cur_win = 1446
new_win = 1448

這個階段,爲了讓收包一直走快速路徑,Server 一直都向對端通告 1448 字節的窗口。

Step 3 後續若干個用戶報文填入非線性區域

在開始往非線性區域填充以後,每一個 skb 都將讓sk->sk_rmem_alloc增長 512 字節,sk->sk_forward_alloc 減少 512 字節。這樣 6 個報文以後, sk->sk_forward_alloc就消耗地差很少了,容不下下一個報文了。怎麼辦?再預分配啊,因而sk->sk_forward_alloc 變成了 4096 字節。以後又能夠愉快地再增長sk->sk_rmem_alloc了。

也許你會疑問,這個過程當中不會 check sk->sk_rcvbuf = 4096限制嗎?實際上,內核只會在慢速路徑爲sk->sk_forward_alloc分配內存時纔會去 check 是否超過了緩衝區大小,走快速路徑時壓根無論這個限制。

在咱們的例子中,內核第一次走慢速路徑爲sk->sk_forward_alloc預分配內存時,sk->sk_rmem_alloc只是接近但並無到 4096 字節,因此sk->sk_forward_alloc能順利地加上 4K 字節。這讓sk->sk_rmem_alloc以後能夠超過sk->sk_rcvbuf內核TCP的緩衝區限制是一條軟限制,它容許超過,但超事後就不能再爲sk->sk_forward_alloc預分配內存了。

再看窗口,第 1 個填入非線性區域的報文到達後,free_space 減少了,致使new_win的值也減少

old_win = 1448
cur_win = 1446
new_win = 1072 // 536 的 兩倍

雖然new_win只有 1072 字節,但因爲窗口不能收縮的原則,Server 端也只能老老實實地通告 cur_win = 1446 ,比以前的少了兩個字節。

當第二個報文到達以後,一樣的道理,通告窗口又減少 2 個字節,變成了 1444 字節。

接下來 144二、1440、143八、1436 .....

Step 4 TCP prune 發生

在 prune 以前,Server 通告窗口大小爲 1420 字節 (並非按照內存佔用算出這麼多,而是 TCP 只能減小到這麼多)

如前面所說,第一次 prune 以後,tail_skb 會騰出線性空間,接下來的報文又能夠往線性空間裏塞了,這段時間內,通告窗口將保持不變,依舊爲 1420 字節。

Step 5 後續若干個用戶報文填入線性區域

同 Step 2 相似,爲了讓收包一直走快速路徑,這個階段 Server 一直都向對端通告 1420 字節的窗口。

Step 6 後續若干個用戶報文填入非線性區域

同 Step 3 相似,這個階段受制於窗口不能收縮的原則,Server 通告的窗口緩慢地從 1420 字節開始減少。141八、141六、1414......直到下一次 prune 發生。


接下來,重複 Step 5 至 Step 6。Server 通告的發送窗口也就依次保持不變、減少、不變、減少.....

那麼這樣下去,是否窗口就會減到 0 呢?

Step 7 最後階段的 TCP prune

最後階段的 TCP prune 前夕,窗口大小爲 874 字節。

tail_skbsk 的狀態是這樣的:

tail_skb->truesize = 7936
tail_skb->datalen  = 22
tail_skb->len      = 1750
tail_skb->data - tail_skb->head =  0
tail_skb->end  - tail_skb->tail =  0

sk->sk_rmem_alloc    = 7936
sk->sk_forward_alloc = 256

此時收到一個新的報文,因爲 sk->sk_forward_alloc 已經不夠了,因此會走慢速路徑去 prune 接收隊列上的 tail_skb

prune 結果是這樣的:

tail_skb->truesize = 4352
tail_skb->datalen  = 0
tail_skb->len      = 1750
tail_skb->data - tail_skb->head =  0
tail_skb->end  - tail_skb->tail =  202

sk->sk_rmem_alloc    = 4352
sk->sk_forward_alloc = 3840

tail_skb->truesizesk->sk_rmem_alloc縮小到了 4352 字節,線性區域尾部騰出了 202 字節的空間,非線性區沒有了。

可是它佔用的內存超過了sk->sk_rcvbuf, 所以 tcp_prune_queue()tcp_try_rmem_schedule() 相繼返回了 -1 !丟棄此報文!

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
                 unsigned int size)
{
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
        !sk_rmem_schedule(sk, skb, size)) {

        if (tcp_prune_queue(sk) < 0)
            return -1;

        if (!sk_rmem_schedule(sk, skb, size)) {
            if (!tcp_prune_ofo_queue(sk))
                return -1;

            if (!sk_rmem_schedule(sk, skb, size))
                return -1;
        }
    }
    return 0;
}

也就是說,此刻 Server 端的sk已經不去接收新的報文了!但它以前通告的窗口明明還有 874 字節, 因此 Clinet 纔會不停地重傳....

解決方案

那麼這個時候sk是真的裝不下新的報文了嗎?顯然不是,它本身的線性空間已經騰出 202 字節了,何況還有 3840 字節的非線性空間可使用。

因此咱們能夠將tcp_try_rmem_schedule修改成即便tcp_prune_queue失敗(沒有能把sk->sk_rmem_alloc壓縮到sk->sk_rcvbuf以內),若是此時接收隊列尾的sk_buff能夠容納這個報文的話,仍是讓其返回成功。

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
                 unsigned int size)
{
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
        !sk_rmem_schedule(sk, skb, size)) {

-       if (tcp_prune_queue(sk) < 0)
+       if (tcp_prune_queue(sk) < 0 &&
+           size > skb_tailroom(skb_peek_tail(sk)) &&       
+           size > sk->sk_forward_alloc)
+            
            return -1; 

        if (!sk_rmem_schedule(sk, skb, size)) {
            if (!tcp_prune_ofo_queue(sk))
            return -1;

        if (!sk_rmem_schedule(sk, skb, size))
            return -1;
        }
    }
    return 0;
}

本文完

個人我的主頁

網址二維碼.png

相關文章
相關標籤/搜索