sock 接收skb超过mem限制后改为发送0 win
看下skb入进入receive-queue
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); bool fragstolen; int eaten; skb_dst_drop(skb); __skb_pull(skb, tcp_hdr(skb)->doff * 4); tp->rx_opt.dsack = 0; /* Queue data for delivery to the user. * Packets in sequence go to the receive queue. * Out of sequence packets to the out_of_order_queue. */ if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) { if (tcp_receive_window(tp) == 0) { if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN) goto queue_and_out; NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPZEROWINDOWDROP); goto out_of_window; } /* Ok. In sequence. In window. */ queue_and_out: if (skb_queue_len(&sk->sk_receive_queue) == 0) sk_forced_mem_schedule(sk, skb->truesize); else if (tcp_try_rmem_schedule(sk, skb, skb->truesize)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPRCVQDROP); sk->sk_data_ready(sk); goto drop; } eaten = tcp_queue_rcv(sk, skb, &fragstolen); if (skb->len) tcp_event_data_recv(sk, skb); if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN) tcp_fin(sk); if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) { tcp_ofo_queue(sk); /* RFC5681. 4.2. SHOULD send immediate ACK, when * gap in queue is filled. */ if (RB_EMPTY_ROOT(&tp->out_of_order_queue)) inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW; } if (tp->rx_opt.num_sacks) tcp_sack_remove(tp); tcp_fast_path_check(sk); if (eaten > 0) kfree_skb_partial(skb, fragstolen); if (!sock_flag(sk, SOCK_DEAD)) tcp_data_ready(sk); return; } -------------- drop: tcp_drop(sk, skb); return; }
如果 tcp_try_rmem_schedule 返回真,说明**“内存超限了,装不下了”**;也就是tcp_try_rmem_schedule 没有内存时;直接drop! 但是此时对端不知道,还是会继续重传,如何通知对端不要重传?
如何告知对端当前接收不了数据。
现在逻辑为:内核不再只是默默丢包,而是努力回复一个特殊的 ACK 包,并在包里把 “接收窗口” (Window Size) 设置为 0。
queue_and_out:
if (tcp_try_rmem_schedule(sk, skb, skb->truesize)) {
/* TODO: maybe ratelimit these WIN 0 ACK ? */
inet_csk(sk)->icsk_ack.pending |=
(ICSK_ACK_NOMEM | ICSK_ACK_NOW);
inet_csk_schedule_ack(sk);
sk->sk_data_ready(sk);
if (skb_queue_len(&sk->sk_receive_queue) && skb->len) {
reason = SKB_DROP_REASON_PROTO_MEM;
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPRCVQDROP);
goto drop;
}//如果队列里有数据,就把这个新包扔了;如果队列是空的,就硬着头皮把这个新包收下。”
sk_forced_mem_schedule(sk, skb->truesize);
}
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
if (skb->len)
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
tcp_ofo_queue(sk);
/* RFC5681. 4.2. SHOULD send immediate ACK, when
* gap in queue is filled.
*/
if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;
}
if (tp->rx_opt.num_sacks)
tcp_sack_remove(tp);
tcp_fast_path_check(sk);
if (eaten > 0)
kfree_skb_partial(skb, fragstolen);
if (!sock_flag(sk, SOCK_DEAD))
tcp_data_ready(sk);
return;
}
1、(ICSK_ACK_NOMEM | ICSK_ACK_NOW);设置Zero-window ACK
2、sk_data_ready队列满了说明应用程序处理得太慢了(或者卡住了)。我们发一个“有数据可读”的信号,催促应用程序赶紧把队列里旧的数据读
sk_forced_mem_schedule 只是记账;后续tcp_queue_rcv(sk, skb, &fragstolen)做“搬运” 和 “整理” 的工作。
-
动作: 把这个已经收到的数据包 (
skb),挂到 Socket 的接收队列 (sk_receive_queue) 尾部。 -
内存行为:
-
它做的是链表操作(指针指来指去)。
-
或者做数据合并 (Coalescing):如果这个包和队列里最后一个包是挨着的,它会把这个包的 Page(数据页)“偷”过来贴到前一个包后面。这叫
fragstolen。
-

对于接收端如何更新发生窗口
tcp_may_update_window这段代码是 Linux 内核 TCP 协议栈中非常核心的一个校验函数,函数名 tcp_may_update_window 意思是 “是否允许更新发送窗口”。
它的作用是:根据 RFC 793 协议的标准,判断刚收到的这个 ACK 包里携带的“窗口大小 (Window Size)”是否可信,是否应该用来更新我方记录的发送窗口 (snd_wnd)。
简单来说,这是为了防止 “历史重现”(旧包覆盖新包)或者 “窗口回缩”(非法缩小窗口)导致的问题。
1. 为什么需要这个检查?
网络是不可靠的,数据包可能会乱序到达。 假设我们现在的窗口是 2000。
-
Packet A (旧): 窗口 = 1000(这是几秒前发的,在路上堵车了)。
-
Packet B (新): 窗口 = 2000(这是刚发的,已经到了)。
如果我们先收到了 B,把窗口设为 2000。过了一会儿 A 才到,如果我们不加检查直接用 A 更新,窗口就会莫名其妙变成 1000。这会导致传输性能抖动。
这个函数就是用来过滤掉 Packet A 这种“过期信息”的。
-
snd_una:未确认的最早字节序号 -
snd_wl1:最近一次窗口更新时使用的 seq -
snd_wnd:当前接收窗口大小(对方的 rwnd) -
ack:当前 ACK 确认号 -
ack_seq:当前包的序号 -
nwin:对方告知的新接收窗口值(rwnd)
2. 代码逻辑逐行拆解
函数只要满足 以下三个条件中的任意一个,就返回 true(允许更新):
条件 1:确认了新数据 (Progress)
-
含义: 收到的
ack大于snd_una(最早未确认的字节号)。 -
解释: 这意味着对方确认收到了新的数据。
-
逻辑: 只要对方收到了新数据,它发回来的窗口信息就是最新且权威的,必须无条件采纳。
条件 2:如果这个包的序号比上一次用于更新窗口的序号更靠后 ⇒ 可以更新窗口
-
含义:
ack没变(没确认新数据),但这个包的 序列号 (Seq) 比上次更新窗口时的序列号 (snd_wl1) 要大。 -
解释: 这通常是一个 “纯窗口更新报文” (Window Update)。对方没收到新数据,但是应用层可能腾出了缓冲区,特意发个包来告诉你:“虽然数据没动,但我窗口变大了/变了,请更新”。
-
逻辑:因为 TCP 规定:
只有序号更“新”的包,才可以更新窗口。
条件 3:ACK 没变,序列号也没变 (Same Packet / Retransmission)
-
含义:
ack没变,而且ack_seq也等于snd_wl1。这说明这个包和上次更新窗口的包是同一个包(或者是同一个包的重传)。 -
子条件: 在这种情况下,一般是不更新的,除非满足以下特例:
-
nwin > tp->snd_wnd: 对方给的窗口变大了。永远不要拒绝“免费升舱”。即使是重传包,如果它宣称窗口变大了,我们通常会接受。 -
!nwin: 对方通知窗口为 0。这是一次“紧急刹车”信号(Zero Window)。为了防止把对方撑死,如果对方说窗口为 0,必须立刻接受并停止发送。!nwin如果对方窗口变成 0,也允许更新。这是为了支持 Zero Window 机制,发送方需要进入 persist 探测模式。
-
/* Check that window update is acceptable. * The function assumes that snd_una<=ack<=snd_next. */ static inline bool tcp_may_update_window(const struct tcp_sock *tp, const u32 ack, const u32 ack_seq, const u32 nwin) { return after(ack, tp->snd_una) || after(ack_seq, tp->snd_wl1) || (ack_seq == tp->snd_wl1 && (nwin > tp->snd_wnd || !nwin)); } /*snd_wl1: 记录上一次更新窗口的那个包的 Sequence Number(序列号)。 为什么要用 snd_wl1?(举例) 假设我们(发送方)收到了两个包,但网络乱序了: 包 A (旧包): Seq = 100, Window = 1000 包 B (新包): Seq = 200, Window = 2000 情况: 我们先收到了 包 B。 我们更新 snd_wnd = 2000。 记录 snd_wl1 = 200。 接着: 我们收到了迟到的 包 A。 我们检查 after(ack_seq, tp->snd_wl1),即检查 after(100, 200)。 结果是 False(100 在 200 之前)。 结论: 内核知道包 A 是个过期的旧包,拒绝将窗口从 2000 缩回 1000。 */
触发了“全局 TCP 内存限制” (Global TCP Limit) —— 最常见原因
Linux 内核对所有 TCP 连接消耗的总内存有一个全局限制。
-
参数:
net.ipv4.tcp_mem(包含 min, pressure, max 三个值)。 -
现象: 就算你的 Socket 接收队列是空的(
sk_rcvbuf还没用),但如果操作系统里有成千上万个其他连接正在传输大量数据,导致整个 TCP 协议栈消耗的内存页数超过了tcp_mem[2](max)。 -
结果: 内核会进入“内存压力状态”。为了防止系统崩溃,内核会拒绝任何新的内存分配请求,哪怕只是为了存一个小小的 ACK 包。
代码逻辑:
tcp_try_rmem_schedule内部会调用__sk_mem_raise_allocated,这里面会检查全局变量tcp_memory_allocated。如果超标,直接返回失败。

浙公网安备 33010602011771号