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

image

对于接收端如何更新发生窗口

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。这说明这个包和上次更新窗口的包是同一个包(或者是同一个包的重传)。

  • 子条件: 在这种情况下,一般是不更新的,除非满足以下特例:

    1. nwin > tp->snd_wnd: 对方给的窗口变大了。永远不要拒绝“免费升舱”。即使是重传包,如果它宣称窗口变大了,我们通常会接受。

    2. !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。如果超标,直接返回失败。

 

posted @ 2025-12-11 11:44  codestacklinuxer  阅读(0)  评论(0)    收藏  举报