* [PATCH v1 net-next 1/8] tcp: Clean up the first loop in tcp_collapse().
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 2/8] tcp: Don't collapse high density skb Kuniyuki Iwashima
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
tcp_collapse() detects skb to start collapsing from in the
first loop.
If the last skb has SYN or FIN, the condition is tested
again after the loop.
skb_frags_readable() is placed inside and outside of the
loop as well.
If we goto skip_this in both cases, we can remove the second
tests and simply replace end_of_skbs with the inverted loop
condition.
Then, we do not need a test for tcp_collapse_one() too.
Also, skb_frags_readable() should be tested after the fully
overlapping check.
Let's clean up the loop.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
net/ipv4/tcp_input.c | 28 ++++++++++------------------
1 file changed, 10 insertions(+), 18 deletions(-)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index a2074a1af422..32e63d089db8 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5839,23 +5839,17 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
{
struct sk_buff *skb = head, *n;
struct sk_buff_head tmp;
- bool end_of_skbs;
/* First, check that queue is collapsible and find
* the point where collapsing can be useful.
*/
restart:
- for (end_of_skbs = true; skb != NULL && skb != tail; skb = n) {
+ for (; skb && skb != tail; skb = n) {
n = tcp_skb_next(skb, list);
- if (!skb_frags_readable(skb))
- goto skip_this;
-
/* No new bits? It is possible on ofo queue. */
if (!before(start, TCP_SKB_CB(skb)->end_seq)) {
skb = tcp_collapse_one(sk, skb, list, root);
- if (!skb)
- break;
goto restart;
}
@@ -5864,27 +5858,25 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
* - bloated or contains data before "start" or
* overlaps to the next one and mptcp allow collapsing.
*/
- if (!(TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN)) &&
- (tcp_win_from_space(sk, skb->truesize) > skb->len ||
- before(TCP_SKB_CB(skb)->seq, start))) {
- end_of_skbs = false;
+ if (!skb_frags_readable(skb) ||
+ TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN))
+ goto skip_this;
+
+ if (tcp_win_from_space(sk, skb->truesize) > skb->len ||
+ before(TCP_SKB_CB(skb)->seq, start))
break;
- }
if (n && n != tail && skb_frags_readable(n) &&
tcp_skb_can_collapse_rx(skb, n) &&
- TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(n)->seq) {
- end_of_skbs = false;
+ TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(n)->seq)
break;
- }
skip_this:
/* Decided to skip this, advance start seq. */
start = TCP_SKB_CB(skb)->end_seq;
}
- if (end_of_skbs ||
- (TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN)) ||
- !skb_frags_readable(skb))
+
+ if (!skb || skb == tail)
return;
__skb_queue_head_init(&tmp);
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 2/8] tcp: Don't collapse high density skb.
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 1/8] tcp: Clean up the first loop in tcp_collapse() Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 3/8] tcp: Stop collapsing if not bloated Kuniyuki Iwashima
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
tcp_collapse() starts collapsing skb to a new skb if either
of the following is true:
1. skb is less dense compared to tp->scaling_ratio
2. skb has read data before @start
However, this could make situation worse.
The new skb size is SKB_TRUESIZE(SKB_MAX_ORDER(0, 0)) at most,
and with PAGE_SIZE being 4K, its scaling_ratio is 222 (86%).
If 4K-MSS GRO skb updates tp->scaling_ratio, it can be larger
than 222.
Then, even the newly collapsed skb could be detected as bloated
by the next tcp_collapse().
Also, even when only a few bytes are read from GRO skb, it is
collapsed by the 2nd rule, making less-efficient skb.
Let's cap tp->scaling_ratio with the collapsed skb's maximum
scaling_ratio and use unread bytes for bloat detection.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
net/ipv4/tcp_input.c | 24 ++++++++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 32e63d089db8..e07f43347409 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5808,6 +5808,19 @@ static struct sk_buff *tcp_skb_next(struct sk_buff *skb, struct sk_buff_head *li
return skb_rb_next(skb);
}
+#define TCP_COLLAPSE_MAX_SIZE SKB_MAX_ORDER(0, 0)
+#define TCP_COLLAPSE_SCALING_RATIO \
+ ((TCP_COLLAPSE_MAX_SIZE << TCP_RMEM_TO_WIN_SCALE) / \
+ (SKB_TRUESIZE(TCP_COLLAPSE_MAX_SIZE)))
+
+static bool tcp_collapse_skb_bloated(const struct sk_buff *skb,
+ u32 start, u8 scaling_ratio)
+{
+ int unread = TCP_SKB_CB(skb)->end_seq - start;
+
+ return __tcp_win_from_space(scaling_ratio, skb->truesize) > unread;
+}
+
static struct sk_buff *tcp_collapse_one(struct sock *sk, struct sk_buff *skb,
struct sk_buff_head *list,
struct rb_root *root)
@@ -5839,6 +5852,10 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
{
struct sk_buff *skb = head, *n;
struct sk_buff_head tmp;
+ u8 scaling_ratio;
+
+ scaling_ratio = min(tcp_sk(sk)->scaling_ratio,
+ TCP_COLLAPSE_SCALING_RATIO);
/* First, check that queue is collapsible and find
* the point where collapsing can be useful.
@@ -5855,15 +5872,14 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
/* The first skb to collapse is:
* - not SYN/FIN and
- * - bloated or contains data before "start" or
+ * - the unread portion starting from "start" is bloated, or
* overlaps to the next one and mptcp allow collapsing.
*/
if (!skb_frags_readable(skb) ||
TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN))
goto skip_this;
- if (tcp_win_from_space(sk, skb->truesize) > skb->len ||
- before(TCP_SKB_CB(skb)->seq, start))
+ if (tcp_collapse_skb_bloated(skb, start, scaling_ratio))
break;
if (n && n != tail && skb_frags_readable(n) &&
@@ -5882,7 +5898,7 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
__skb_queue_head_init(&tmp);
while (before(start, end)) {
- int copy = min_t(int, SKB_MAX_ORDER(0, 0), end - start);
+ int copy = min_t(int, TCP_COLLAPSE_MAX_SIZE, end - start);
struct sk_buff *nskb;
nskb = alloc_skb(copy, GFP_ATOMIC);
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 3/8] tcp: Stop collapsing if not bloated.
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 1/8] tcp: Clean up the first loop in tcp_collapse() Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 2/8] tcp: Don't collapse high density skb Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 4/8] net: Move SKB_SMALL_HEAD_CACHE_SIZE to skbuff.h Kuniyuki Iwashima
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
Even when a skb is not bloated, tcp_collapse() starts collapsing
if the next skb has overlapping data.
Also, tcp_collapse() does not stop until it reaches the tail or
unmergable skb (SYN/FIN, unreadable frags).
Assume we detect a low-density skb followed by fully-packed 4K-MSS
GRO skbs:
skb->len truesize scaling_ratio
skb #1 1448 2944 125 (49%)
skb #2 65536 66432 252 (98%)
skb #3 65536 66432 252 (98%)
...
skb #9 65536 66432 252 (98%)
--------------------------------------------
total 534400
Then, even the high-density skbs are repacked into lower-density
skbs, and we end up consuming much more memory.
skb->len truesize scaling_ratio
nskb #1 3776 4352 222 (86%)
nskb #2 3776 4352 222 (86%)
...
nskb #139 3776 4352 222 (86%)
nskb #140 872 2304 96 (37%)
--------------------------------------------
total 607232 (+72832!)
Even if we find a collapsible skb, we should not collapse
subsequent skbs with scaling_ratio >= TCP_COLLAPSE_SCALING_RATIO.
Conversely, even if tp->scaling_ratio is smaller than
TCP_COLLAPSE_SCALING_RATIO, we should repack skbs once we
start collapsing, which results in higher-density skb.
Let's remove the next skb condition in the first loop,
search for collapsible bloated skb range, and repeat
collapsing for each range.
Note that even when the next skb is not bloated, we must
collapse it for OOO rbtree if it has overlapping data.
if ((list || !before(TCP_SKB_CB(n)->seq, sub_end)) &&
!tcp_collapse_skb_bloated(n, sub_end,
TCP_COLLAPSE_SCALING_RATIO))
For example, bloated skb_1 and non-bloated skb_2 have overlapping
data and only skb_1 is collapsed to nskb_1 and nskb_2.
skb_1 = [1000, 5000]
skb_2 = [2000, 66000]
Then, nskb_2 will be inserted after skb_2 in rbtree, wrongly
setting tp->ooo_last_skb to 5000.
nskb_1 = [1000, 4776]
skb_2 = [2000, 66000]
nskb_2 = [4776, 5000]
This case is still inefficient, but a later patch handles this.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
net/ipv4/tcp_input.c | 66 +++++++++++++++++++++++++-------------------
1 file changed, 37 insertions(+), 29 deletions(-)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index e07f43347409..5bd42a74c6f5 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5839,7 +5839,7 @@ static struct sk_buff *tcp_collapse_one(struct sock *sk, struct sk_buff *skb,
}
/* Collapse contiguous sequence of skbs head..tail with
- * sequence numbers start..end.
+ * sequence numbers start..sub_end.
*
* If tail is NULL, this means until the end of the queue.
*
@@ -5848,15 +5848,18 @@ static struct sk_buff *tcp_collapse_one(struct sock *sk, struct sk_buff *skb,
*/
static void
tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
- struct sk_buff *head, struct sk_buff *tail, u32 start, u32 end)
+ struct sk_buff *head, struct sk_buff *tail, u32 start)
{
struct sk_buff *skb = head, *n;
struct sk_buff_head tmp;
u8 scaling_ratio;
+ u32 sub_end;
scaling_ratio = min(tcp_sk(sk)->scaling_ratio,
TCP_COLLAPSE_SCALING_RATIO);
+ __skb_queue_head_init(&tmp);
+
/* First, check that queue is collapsible and find
* the point where collapsing can be useful.
*/
@@ -5872,38 +5875,48 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
/* The first skb to collapse is:
* - not SYN/FIN and
- * - the unread portion starting from "start" is bloated, or
- * overlaps to the next one and mptcp allow collapsing.
+ * - the unread portion starting from "start" is bloated.
*/
- if (!skb_frags_readable(skb) ||
- TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN))
- goto skip_this;
-
- if (tcp_collapse_skb_bloated(skb, start, scaling_ratio))
- break;
-
- if (n && n != tail && skb_frags_readable(n) &&
- tcp_skb_can_collapse_rx(skb, n) &&
- TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(n)->seq)
+ if (skb_frags_readable(skb) &&
+ !(TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN)) &&
+ tcp_collapse_skb_bloated(skb, start, scaling_ratio))
break;
-skip_this:
/* Decided to skip this, advance start seq. */
start = TCP_SKB_CB(skb)->end_seq;
}
if (!skb || skb == tail)
- return;
+ goto end;
- __skb_queue_head_init(&tmp);
+ /* Second, find consecutive collapsible skb. */
+ sub_end = TCP_SKB_CB(skb)->end_seq;
+
+ for (n = tcp_skb_next(skb, list); n && n != tail; n = tcp_skb_next(n, list)) {
+ if (!skb_frags_readable(n) ||
+ TCP_SKB_CB(n)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN) ||
+ !tcp_skb_can_collapse_rx(skb, n))
+ break;
+
+ if (!after(TCP_SKB_CB(n)->end_seq, sub_end))
+ continue;
+
+ /* seq and end_seq must increase monotonically for rbtree. */
+ if ((list || !before(TCP_SKB_CB(n)->seq, sub_end)) &&
+ !tcp_collapse_skb_bloated(n, sub_end,
+ TCP_COLLAPSE_SCALING_RATIO))
+ break;
- while (before(start, end)) {
- int copy = min_t(int, TCP_COLLAPSE_MAX_SIZE, end - start);
+ sub_end = TCP_SKB_CB(n)->end_seq;
+ }
+
+ while (before(start, sub_end)) {
+ int copy = min_t(int, TCP_COLLAPSE_MAX_SIZE, sub_end - start);
struct sk_buff *nskb;
nskb = alloc_skb(copy, GFP_ATOMIC);
if (!nskb)
- break;
+ goto end;
memcpy(nskb->cb, skb->cb, sizeof(skb->cb));
TCP_SKB_CB(nskb)->has_rxtstamp = false;
@@ -5942,15 +5955,11 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
}
if (!before(start, TCP_SKB_CB(skb)->end_seq)) {
skb = tcp_collapse_one(sk, skb, list, root);
- if (!skb ||
- skb == tail ||
- !tcp_skb_can_collapse_rx(nskb, skb) ||
- (TCP_SKB_CB(skb)->tcp_flags & (TCPHDR_SYN | TCPHDR_FIN)) ||
- !skb_frags_readable(skb))
- goto end;
}
}
}
+
+ goto restart;
end:
skb_queue_walk_safe(&tmp, skb, n)
tcp_rbtree_insert(root, skb);
@@ -5989,7 +5998,7 @@ static void tcp_collapse_ofo_queue(struct sock *sk)
if (range_truesize != head->truesize ||
end - start >= SKB_WITH_OVERHEAD(PAGE_SIZE)) {
tcp_collapse(sk, NULL, &tp->out_of_order_queue,
- head, skb, start, end);
+ head, skb, start);
} else {
sum_tiny += range_truesize;
if (sum_tiny > sk->sk_rcvbuf >> 3)
@@ -6103,8 +6112,7 @@ static int tcp_prune_queue(struct sock *sk, const struct sk_buff *in_skb)
if (!skb_queue_empty(&sk->sk_receive_queue))
tcp_collapse(sk, &sk->sk_receive_queue, NULL,
skb_peek(&sk->sk_receive_queue),
- NULL,
- tp->copied_seq, tp->rcv_nxt);
+ NULL, tp->copied_seq);
if (tcp_can_ingest(sk, in_skb))
return 0;
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 4/8] net: Move SKB_SMALL_HEAD_CACHE_SIZE to skbuff.h.
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
` (2 preceding siblings ...)
2026-11-14 4:00 ` [PATCH v1 net-next 3/8] tcp: Stop collapsing if not bloated Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 5/8] tcp: Skip collapsing if collapsed skb is less-dense Kuniyuki Iwashima
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
The next patch needs SKB_SMALL_HEAD_CACHE_SIZE to precalculate
skb truesize for tcp_collapse().
Let's move SKB_SMALL_HEAD_CACHE_SIZE and its dependencies to
skbuff.h.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
include/linux/skbuff.h | 18 ++++++++++++++++++
include/net/gro.h | 3 ---
include/net/tcp.h | 1 -
net/core/skbuff.c | 13 -------------
4 files changed, 18 insertions(+), 17 deletions(-)
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 1d6832878930..d36fbb88b3f6 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -269,6 +269,24 @@
#define SKB_MAX_HEAD(X) (SKB_MAX_ORDER((X), 0))
#define SKB_MAX_ALLOC (SKB_MAX_ORDER(0, 2))
+#define MAX_TCP_HEADER L1_CACHE_ALIGN(128 + MAX_HEADER)
+
+/* This should be increased if a protocol with a bigger head is added. */
+#define GRO_MAX_HEAD (MAX_HEADER + 128)
+#define GRO_MAX_HEAD_PAD (GRO_MAX_HEAD + NET_SKB_PAD + NET_IP_ALIGN)
+
+#define SKB_SMALL_HEAD_SIZE SKB_HEAD_ALIGN(max(MAX_TCP_HEADER, \
+ GRO_MAX_HEAD_PAD))
+
+/* SKB_SMALL_HEAD_CACHE_SIZE is the size used for the skbuff_small_head
+ * kmem_cache. The non-power-of-2 padding is kept for historical reasons and
+ * to avoid potential collisions with generic kmalloc bucket sizes.
+ */
+#define SKB_SMALL_HEAD_CACHE_SIZE \
+ (is_power_of_2(SKB_SMALL_HEAD_SIZE) ? \
+ (SKB_SMALL_HEAD_SIZE + L1_CACHE_BYTES) : \
+ SKB_SMALL_HEAD_SIZE)
+
/* return minimum truesize of one skb containing X bytes of data */
#define SKB_TRUESIZE(X) ((X) + \
SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \
diff --git a/include/net/gro.h b/include/net/gro.h
index 7a19d4486456..4eb755c33a26 100644
--- a/include/net/gro.h
+++ b/include/net/gro.h
@@ -11,9 +11,6 @@
#include <net/udp.h>
#include <net/hotdata.h>
-/* This should be increased if a protocol with a bigger head is added. */
-#define GRO_MAX_HEAD (MAX_HEADER + 128)
-
struct napi_gro_cb {
union {
struct {
diff --git a/include/net/tcp.h b/include/net/tcp.h
index 25d11fab1b03..9fcc5e5223f8 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -67,7 +67,6 @@ static inline void tcp_orphan_count_dec(void)
void tcp_time_wait(struct sock *sk, int state, int timeo);
-#define MAX_TCP_HEADER L1_CACHE_ALIGN(128 + MAX_HEADER)
#define MAX_TCP_OPTION_SPACE 40
#define TCP_MIN_SND_MSS 48
#define TCP_MIN_GSO_SIZE (TCP_MIN_SND_MSS - MAX_TCP_OPTION_SPACE)
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 2e81aacc998b..421776e3526e 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -105,19 +105,6 @@
static struct kmem_cache *skbuff_ext_cache __ro_after_init;
#endif
-#define GRO_MAX_HEAD_PAD (GRO_MAX_HEAD + NET_SKB_PAD + NET_IP_ALIGN)
-#define SKB_SMALL_HEAD_SIZE SKB_HEAD_ALIGN(max(MAX_TCP_HEADER, \
- GRO_MAX_HEAD_PAD))
-
-/* SKB_SMALL_HEAD_CACHE_SIZE is the size used for the skbuff_small_head
- * kmem_cache. The non-power-of-2 padding is kept for historical reasons and
- * to avoid potential collisions with generic kmalloc bucket sizes.
- */
-#define SKB_SMALL_HEAD_CACHE_SIZE \
- (is_power_of_2(SKB_SMALL_HEAD_SIZE) ? \
- (SKB_SMALL_HEAD_SIZE + L1_CACHE_BYTES) : \
- SKB_SMALL_HEAD_SIZE)
-
#define SKB_SMALL_HEAD_HEADROOM \
SKB_WITH_OVERHEAD(SKB_SMALL_HEAD_CACHE_SIZE)
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 5/8] tcp: Skip collapsing if collapsed skb is less-dense.
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
` (3 preceding siblings ...)
2026-11-14 4:00 ` [PATCH v1 net-next 4/8] net: Move SKB_SMALL_HEAD_CACHE_SIZE to skbuff.h Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 6/8] tcp: Allocate all new skb before collapsing Kuniyuki Iwashima
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
Once tcp_collapse() finds a collapsible skb, it collapses
consecutive bloated skbs.
However, the end result could be the same or larger than
as is in some cases:
1. skb is already minimum
If SKB_HEAD_ALIGN(len) <= SKB_SMALL_HEAD_CACHE_SIZE,
skb is allocated from skbuff_small_head, and skb->truesize
never changes.
2. SKB_HEAD_ALIGN() and kmalloc_size_roundup() in alloc_skb()
Assume two bloated skbs with len = 1448 B (truesize =
2304 B, scaling_ratio = 160) and len = 300 B (truesize =
896 B, scaling_ratio = 85), whose total truesize is
3200 B.
Collapsing them calls alloc_skb(1748), which rounds up
SKB_HEAD_ALIGN(1748) = 2112 B to 4096 B and allocates a
new skb with truesize = 4352 B (+1152 B!).
Even worse, in case 1, such a skb is reallocated repeatedly
by every tcp_collapse() in vain.
Let's calculate the final collapsed truesize before collapsing
skbs and skip the range if it does not shrink.
We could allocate skbs first and calculate the new truesize,
but given tcp_collapse() is called under memory pressure,
we want to avoid unnecessary allocation.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
include/linux/skbuff.h | 7 +++++++
net/ipv4/tcp_input.c | 24 +++++++++++++++++++++++-
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index d36fbb88b3f6..618e629df15a 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -292,6 +292,13 @@
SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \
SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
+/* return estimated truesize allocated by alloc_skb(X) via kmalloc_reserve() */
+#define SKB_ALLOCSIZE(X) \
+ ((SKB_HEAD_ALIGN(X) <= SKB_SMALL_HEAD_CACHE_SIZE ? \
+ SKB_SMALL_HEAD_CACHE_SIZE : \
+ kmalloc_size_roundup(SKB_HEAD_ALIGN(X))) + \
+ SKB_DATA_ALIGN(sizeof(struct sk_buff)))
+
struct net_device;
struct scatterlist;
struct pipe_inode_info;
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 5bd42a74c6f5..fe71305f6e67 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5850,8 +5850,10 @@ static void
tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
struct sk_buff *head, struct sk_buff *tail, u32 start)
{
+ int old_truesize, new_truesize;
struct sk_buff *skb = head, *n;
struct sk_buff_head tmp;
+ int remainder, nskbs;
u8 scaling_ratio;
u32 sub_end;
@@ -5891,6 +5893,7 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
/* Second, find consecutive collapsible skb. */
sub_end = TCP_SKB_CB(skb)->end_seq;
+ old_truesize = skb->truesize;
for (n = tcp_skb_next(skb, list); n && n != tail; n = tcp_skb_next(n, list)) {
if (!skb_frags_readable(n) ||
@@ -5898,8 +5901,10 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
!tcp_skb_can_collapse_rx(skb, n))
break;
- if (!after(TCP_SKB_CB(n)->end_seq, sub_end))
+ if (!after(TCP_SKB_CB(n)->end_seq, sub_end)) {
+ old_truesize += n->truesize;
continue;
+ }
/* seq and end_seq must increase monotonically for rbtree. */
if ((list || !before(TCP_SKB_CB(n)->seq, sub_end)) &&
@@ -5908,6 +5913,23 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
break;
sub_end = TCP_SKB_CB(n)->end_seq;
+ old_truesize += n->truesize;
+ }
+
+ /* Third, check if the new skbs have high density. */
+ nskbs = (sub_end - start) / TCP_COLLAPSE_MAX_SIZE;
+ new_truesize = nskbs * SKB_TRUESIZE(TCP_COLLAPSE_MAX_SIZE);
+
+ remainder = (sub_end - start) % TCP_COLLAPSE_MAX_SIZE;
+ if (remainder) {
+ nskbs++;
+ new_truesize += SKB_ALLOCSIZE(remainder);
+ }
+
+ if (old_truesize <= new_truesize) {
+ start = sub_end;
+ skb = n;
+ goto restart;
}
while (before(start, sub_end)) {
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 6/8] tcp: Allocate all new skb before collapsing.
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
` (4 preceding siblings ...)
2026-11-14 4:00 ` [PATCH v1 net-next 5/8] tcp: Skip collapsing if collapsed skb is less-dense Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 7/8] tcp: Batch memory accounting in tcp_collapse() Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 8/8] selftest: packetdrill: Add test for tcp_collapse() Kuniyuki Iwashima
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
At the final stage, tcp_collapse() iterates over collapsible
candidate skbs and repacks them to new skbs.
In each iteration, a new skb is allocated and inserted to the
receive queue or a temporary list for RB tree.
If alloc_skb() fails while splitting a large bloated skb across
multiple new skbs, the partially-copied skb is not removed and
new skbs remain in the queue as duplicates, consuming more memory.
It is only when all candidates are properly collapsed that
we can save memory.
To avoid complicated rollback, let's preallocate skbs before
collapsing.
Now all skb in [start, sub_end) must be allocated in a batch,
so the section size is normally capped at TCP_COLLAPSE_MAX_TOTAL_SIZE
(16 skbs), except when a single bloated skb or overlapping OOO skbs
must be collapsed together to free the original skb or keep the
rbtree monotonic.
Once the size exceeds the limit, the remaining skbs are re-evaluated
from restart:.
Note that the next skb can be skipped if its density already meets
tp->scaling_ratio, and each cut adds at most one remainder skb
(~3.4% worst-case truesize overhead on 4 KiB pages), which is a
small tradeoff to bound the temporary allocation.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
net/ipv4/tcp_input.c | 29 +++++++++++++++++++++--------
1 file changed, 21 insertions(+), 8 deletions(-)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index fe71305f6e67..9f32577b0b53 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5812,6 +5812,7 @@ static struct sk_buff *tcp_skb_next(struct sk_buff *skb, struct sk_buff_head *li
#define TCP_COLLAPSE_SCALING_RATIO \
((TCP_COLLAPSE_MAX_SIZE << TCP_RMEM_TO_WIN_SCALE) / \
(SKB_TRUESIZE(TCP_COLLAPSE_MAX_SIZE)))
+#define TCP_COLLAPSE_MAX_TOTAL_SIZE (TCP_COLLAPSE_MAX_SIZE * 16)
static bool tcp_collapse_skb_bloated(const struct sk_buff *skb,
u32 start, u8 scaling_ratio)
@@ -5852,8 +5853,9 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
{
int old_truesize, new_truesize;
struct sk_buff *skb = head, *n;
- struct sk_buff_head tmp;
+ struct sk_buff_head tmp, pool;
int remainder, nskbs;
+ struct sk_buff *nskb;
u8 scaling_ratio;
u32 sub_end;
@@ -5861,6 +5863,7 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
TCP_COLLAPSE_SCALING_RATIO);
__skb_queue_head_init(&tmp);
+ __skb_queue_head_init(&pool);
/* First, check that queue is collapsible and find
* the point where collapsing can be useful.
@@ -5908,8 +5911,9 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
/* seq and end_seq must increase monotonically for rbtree. */
if ((list || !before(TCP_SKB_CB(n)->seq, sub_end)) &&
- !tcp_collapse_skb_bloated(n, sub_end,
- TCP_COLLAPSE_SCALING_RATIO))
+ (TCP_SKB_CB(n)->end_seq - start >= TCP_COLLAPSE_MAX_TOTAL_SIZE ||
+ !tcp_collapse_skb_bloated(n, sub_end,
+ TCP_COLLAPSE_SCALING_RATIO)))
break;
sub_end = TCP_SKB_CB(n)->end_seq;
@@ -5924,6 +5928,8 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
if (remainder) {
nskbs++;
new_truesize += SKB_ALLOCSIZE(remainder);
+ } else {
+ remainder = TCP_COLLAPSE_MAX_SIZE;
}
if (old_truesize <= new_truesize) {
@@ -5932,14 +5938,19 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
goto restart;
}
- while (before(start, sub_end)) {
- int copy = min_t(int, TCP_COLLAPSE_MAX_SIZE, sub_end - start);
- struct sk_buff *nskb;
-
- nskb = alloc_skb(copy, GFP_ATOMIC);
+ while (nskbs--) {
+ nskb = alloc_skb(nskbs ? TCP_COLLAPSE_MAX_SIZE : remainder,
+ GFP_ATOMIC);
if (!nskb)
goto end;
+ __skb_queue_tail(&pool, nskb);
+ }
+
+ while (before(start, sub_end)) {
+ int copy = min_t(int, TCP_COLLAPSE_MAX_SIZE, sub_end - start);
+
+ nskb = __skb_dequeue(&pool);
memcpy(nskb->cb, skb->cb, sizeof(skb->cb));
TCP_SKB_CB(nskb)->has_rxtstamp = false;
skb_copy_decrypted(nskb, skb);
@@ -5985,6 +5996,8 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
end:
skb_queue_walk_safe(&tmp, skb, n)
tcp_rbtree_insert(root, skb);
+
+ __skb_queue_purge_reason(&pool, SKB_CONSUMED);
}
/* Collapse ofo queue. Algorithm: select contiguous sequence of skbs
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 7/8] tcp: Batch memory accounting in tcp_collapse().
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
` (5 preceding siblings ...)
2026-11-14 4:00 ` [PATCH v1 net-next 6/8] tcp: Allocate all new skb before collapsing Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
2026-11-14 4:00 ` [PATCH v1 net-next 8/8] selftest: packetdrill: Add test for tcp_collapse() Kuniyuki Iwashima
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
Once tcp_collapse_one() calls sock_rfree(), skb->truesize is
reclaimed to sk->sk_forward_alloc by sk_mem_uncharge().
However, sk_mem_uncharge() rounds down sk->sk_forward_alloc
immediately once it reaches 4K since commit 4890b686f408 ("net:
keep sk->sk_forward_alloc as small as possible").
This unnecessarily reclaims memory for newly allocated skbs,
and the following skb_set_owner_r() for them underflows
sk->sk_forward_alloc without sk_forced_mem_schedule().
Let's cache the delta during tcp_collapse() and batch accounting
at the end.
Then, we no longer need sk_forced_mem_schedule(), and instead
DEBUG_NET_WARN_ON_ONCE() is added to catch a future issue.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
net/ipv4/tcp_input.c | 34 +++++++++++++++++++++++++++++-----
1 file changed, 29 insertions(+), 5 deletions(-)
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 9f32577b0b53..598463f33987 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -5822,6 +5822,24 @@ static bool tcp_collapse_skb_bloated(const struct sk_buff *skb,
return __tcp_win_from_space(scaling_ratio, skb->truesize) > unread;
}
+static int tcp_collapse_uncharge(struct sk_buff *skb)
+{
+ DEBUG_NET_WARN_ON_ONCE(skb->destructor != sock_rfree);
+
+ skb->sk = NULL;
+ skb->destructor = NULL;
+
+ return skb->truesize;
+}
+
+static int tcp_collapse_charge(struct sk_buff *skb, struct sock *sk)
+{
+ skb->sk = sk;
+ skb->destructor = sock_rfree;
+
+ return -skb->truesize;
+}
+
static struct sk_buff *tcp_collapse_one(struct sock *sk, struct sk_buff *skb,
struct sk_buff_head *list,
struct rb_root *root)
@@ -5857,6 +5875,7 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
int remainder, nskbs;
struct sk_buff *nskb;
u8 scaling_ratio;
+ int released = 0;
u32 sub_end;
scaling_ratio = min(tcp_sk(sk)->scaling_ratio,
@@ -5874,6 +5893,7 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
/* No new bits? It is possible on ofo queue. */
if (!before(start, TCP_SKB_CB(skb)->end_seq)) {
+ released += tcp_collapse_uncharge(skb);
skb = tcp_collapse_one(sk, skb, list, root);
goto restart;
}
@@ -5959,11 +5979,8 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
__skb_queue_before(list, skb, nskb);
else
__skb_queue_tail(&tmp, nskb); /* defer rbtree insertion */
- /* nskb is charged before the skbs it replaces are freed.
- * Make sure sk->sk_forward_alloc can not become negative.
- */
- sk_forced_mem_schedule(sk, nskb->truesize);
- skb_set_owner_r(nskb, sk);
+
+ released += tcp_collapse_charge(nskb, sk);
mptcp_skb_ext_move(nskb, skb);
/* Copy data, releasing collapsed skbs. */
@@ -5987,6 +6004,7 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
start += size;
}
if (!before(start, TCP_SKB_CB(skb)->end_seq)) {
+ released += tcp_collapse_uncharge(skb);
skb = tcp_collapse_one(sk, skb, list, root);
}
}
@@ -5998,6 +6016,12 @@ tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root,
tcp_rbtree_insert(root, skb);
__skb_queue_purge_reason(&pool, SKB_CONSUMED);
+
+ if (released) {
+ DEBUG_NET_WARN_ON_ONCE(released < 0);
+ atomic_sub(released, &sk->sk_rmem_alloc);
+ sk_mem_uncharge(sk, released);
+ }
}
/* Collapse ofo queue. Algorithm: select contiguous sequence of skbs
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH v1 net-next 8/8] selftest: packetdrill: Add test for tcp_collapse().
2026-11-14 4:00 [PATCH v1 net-next 0/8] tcp: Prevent tcp_collapse() from bloating queue Kuniyuki Iwashima
` (6 preceding siblings ...)
2026-11-14 4:00 ` [PATCH v1 net-next 7/8] tcp: Batch memory accounting in tcp_collapse() Kuniyuki Iwashima
@ 2026-11-14 4:00 ` Kuniyuki Iwashima
7 siblings, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-14 4:00 UTC (permalink / raw)
To: Eric Dumazet, Neal Cardwell, David S . Miller, Jakub Kicinski,
Paolo Abeni
Cc: Simon Horman, Kuniyuki Iwashima, Kuniyuki Iwashima, netdev
tcp_collapse() now does not collapse high-density skbs and a
low-density skb sandwitched by hihg-density skbs.
tcp_collapse.pkt verifies the behaviour by
1. set a large SO_RCVBUF
2. send one high-density skb with 100 bytes read()
3. send one high-density skb
4. send one low-density skb
5. send one high-density skb
6. send 30 low-density skbs
7. set a small SO_RCVBUF
8. send a small data to trigger tcp_collapse()
9. check TcpExtTCPRcvCollapsed via nstat
Without the series, the first high-density skb is collapsed.
In addition, the first low-density skb drags the following
high-density skb into collapsing, consuming more memory.
If we remove sk_forced_mem_schedule() without this series,
sk->sk_forwad_alloc becomes negative at the end of tcp_collapse().
Note that 30 segments are to make the memory savings large
enough so that the margin can absorb minor size delta of
struct sk_buff or L1_CACHE_BYTES across configs.
skb truesize, TCP_COLLAPSE_MAX_SIZE, TCP_COLLAPSE_SCALING_RATIO
depend on PAGE_SIZE being 4K, so the test is skipped if getconf
returns non-4K PAGE_SIZE.
Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
.../selftests/net/packetdrill/ksft_runner.sh | 9 ++
.../net/packetdrill/tcp_collapse.pkt | 135 ++++++++++++++++++
2 files changed, 144 insertions(+)
create mode 100644 tools/testing/selftests/net/packetdrill/tcp_collapse.pkt
diff --git a/tools/testing/selftests/net/packetdrill/ksft_runner.sh b/tools/testing/selftests/net/packetdrill/ksft_runner.sh
index 0a97d5ae3469..250d143bb9f7 100755
--- a/tools/testing/selftests/net/packetdrill/ksft_runner.sh
+++ b/tools/testing/selftests/net/packetdrill/ksft_runner.sh
@@ -44,6 +44,15 @@ if [ -z "$(which packetdrill)" ]; then
exit "$KSFT_SKIP"
fi
+page_size=$(grep -E '^--define="?PAGE_SIZE=' $script | sed -E 's/.*PAGE_SIZE="?([0-9]+)"?.*/\1/')
+if [[ -n "$page_size" ]]; then
+ cur_page_size=$(getconf PAGESIZE 2>/dev/null)
+ if [[ "$cur_page_size" != "$page_size" ]]; then
+ ktap_skip_all "requires PAGE_SIZE=$page_size (current: ${cur_page_size:-unknown})"
+ exit "$KSFT_SKIP"
+ fi
+fi
+
declare -a optargs
failfunc=ktap_test_fail
diff --git a/tools/testing/selftests/net/packetdrill/tcp_collapse.pkt b/tools/testing/selftests/net/packetdrill/tcp_collapse.pkt
new file mode 100644
index 000000000000..90e8be86968f
--- /dev/null
+++ b/tools/testing/selftests/net/packetdrill/tcp_collapse.pkt
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// Test that tcp_collapse() only collapses bloated skb runs, skips leading
+// and trailing high-density skbs, skips isolated bloated skbs when collapsing
+// them would not shrink truesize, and properly recovers receive buffer space.
+
+--mss=1000
+--define="PAGE_SIZE=4096"
+
+`./defaults.sh`
+
+// Establish a connection with a large receive buffer.
+// __sock_set_rcvbuf() doubles, so 2000000
+ +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+ +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+ +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1000000], 4) = 0
+ +0 bind(3, ..., ...) = 0
+ +0 listen(3, 1) = 0
+
+ +0 < S 0:0(0) win 32792 <mss 1000,nop,nop,sackOK,nop,wscale 7>
+ +0 > S. 0:0(0) ack 1 win 65535 <mss 1460,nop,nop,sackOK,nop,wscale 4>
+ +0 < . 1:1(0) ack 1 win 65535
+
+ +0 accept(3, ..., ...) = 4
+
+// 1. Queue two high-density 62,000 B segments (each adds 15 pages of frag
+// truesize with enough margin across NET_SKB_PAD, L1_CACHE_BYTES, and
+// MAX_SKB_FRAGS configs).
+// Read 100 bytes from the first one so before(skb->seq, tp->copied_seq) is true.
+ +0 < P. 1:62001(62000) ack 1 win 65535
+ * > . 1:1(0) ack 62001
+ +0 read(4, ..., 100) = 100
+
+ +0 < P. 62001:124001(62000) ack 1 win 65535
+ * > . 1:1(0) ack 124001
+
+// 2. Queue an isolated bloated 1,000 B segment.
+ +0 < P. 124001:125001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 125001
+
+// 3. Queue another high-density 62,000 B segment.
+ +0 < P. 125001:187001(62000) ack 1 win 65535
+ * > . 1:1(0) ack 187001
+
+// 4. Queue a run of 30 bloated 1,000 B segments.
+ +0 < P. 187001:188001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 188001
+ +0 < P. 188001:189001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 189001
+ +0 < P. 189001:190001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 190001
+ +0 < P. 190001:191001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 191001
+ +0 < P. 191001:192001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 192001
+ +0 < P. 192001:193001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 193001
+ +0 < P. 193001:194001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 194001
+ +0 < P. 194001:195001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 195001
+ +0 < P. 195001:196001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 196001
+ +0 < P. 196001:197001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 197001
+ +0 < P. 197001:198001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 198001
+ +0 < P. 198001:199001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 199001
+ +0 < P. 199001:200001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 200001
+ +0 < P. 200001:201001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 201001
+ +0 < P. 201001:202001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 202001
+ +0 < P. 202001:203001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 203001
+ +0 < P. 203001:204001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 204001
+ +0 < P. 204001:205001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 205001
+ +0 < P. 205001:206001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 206001
+ +0 < P. 206001:207001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 207001
+ +0 < P. 207001:208001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 208001
+ +0 < P. 208001:209001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 209001
+ +0 < P. 209001:210001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 210001
+ +0 < P. 210001:211001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 211001
+ +0 < P. 211001:212001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 212001
+ +0 < P. 212001:213001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 213001
+ +0 < P. 213001:214001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 214001
+ +0 < P. 214001:215001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 215001
+ +0 < P. 215001:216001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 216001
+ +0 < P. 216001:217001(1000) ack 1 win 65535
+ * > . 1:1(0) ack 217001
+
+// 5. Queue a trailing high-density 62,000 B segment.
+ +0 < P. 217001:279001(62000) ack 1 win 65535
+ * > . 1:1(0) ack 279001
+
+// Reset SNMP counters.
+ +0 `nstat -n`
+
+// Shrink SO_RCVBUF so sk_rcvbuf (320,000) is smaller than current
+// sk_rmem_alloc (334,592 on x86_64 default), but larger than sk_rmem_alloc
+// after collapsing the 30 bloated skbs (300,288 on x86_64 default).
+ +0 setsockopt(4, SOL_SOCKET, SO_RCVBUF, [160000], 4) = 0
+
+// Inject the next 1,000 B segment with a changed window to bypass
+// header prediction. This triggers tcp_prune_queue() -> tcp_collapse().
+// Only the 30 bloated segments should be collapsed; the high-density
+// segments and the isolated bloated segment are skipped. sk_rmem_alloc
+// shrinks below sk_rcvbuf, so tcp_can_ingest() succeeds and the packet
+// is accepted.
+ +0 < P. 279001:280001(1000) ack 1 win 65534
+ * > . 1:1(0) ack 280001
+
+// Verify exactly 30 skbs were collapsed.
+ +0 `nstat -s | grep TcpExtTCPRcvCollapsed | grep -q " 30 "`
+
+// Verify no packets were dropped.
+ +0 `test -z "$(nstat -s | grep TcpExtTCPRcvQDrop)"`
+
+// Read all remaining queued data to verify data integrity.
+ +0 read(4, ..., 300000) = 279900
--
2.56.0.611.g1030f0b940-goog
^ permalink raw reply related [flat|nested] 9+ messages in thread