内核 3.10, 接《tcp 的发送端一个小包就能打破对端的 delay_ack 么?》
我们继续来分析这个没满 mss 的小包,
可以看到, 由于受到 syn ack 这端是发包端, 所以该发送链路协商的 mss 为 1460, 而接收的时候, mss 只能为 1412.
我们来找下规律, 看下小包发送有没有其他时间规律:
可以看到, 小包就是下面那排小点, 其中总长度为 490, 很明显的一排.
图形还是比较好找规律, 可以大概看出, 小包的发送间隔几乎是固定的, 间隔为 400ms. 凭着对 tcp 的了解, 协议栈肯定不会有固定 400ms 发送一个固定大小的小包的
行为, 所以这个应该是用户态行为.
用户态如何制造这种情况, 我们再来看两个时间间隔内的报文总长度:
对应的 seq 分别为: 1052063,1576351,2100639,2624927,3149215,3673503,4197791,4722079,5246367.
可以算出, 两个小包之间的 seq 相差刚好为 524288 字节, 也就是 512k, 而从下图的报文时间分布来看, 发包的时候, 是一种 burst 现象, 也就是有包赶紧发, 有的时间段是没有包的.
所以得出这样的结论: 每 400ms, 用户态进程发送 512k 的报文, 其中最后一个报文, 是不满 mss 的, 这个不满 mss 的报文, 因为某种原因,
等待了一会, 发现后面没有报文继续发送, 所以还是发送出去了, 前面等待是因为不满 mss, 等待超时之后, 只能发送出去, 发送出去之后, 因为没有新的数据要下发, 所以形成了一个明显的前后间隔.
那什么样的情况, 不满 mss 的报文需要等待呢? 熟悉 tcp 的兄弟肯定会想起来, 这个就像 nagle 啊.
我们来看这样一个流程, 在一个 establish 的链路中, 在收到 ack 之后, 会先调用 tcp_ack 处理这个 ack, 尽可能地清理到之前因为未收到 ack 而保存在在发送队列中的 skb,
- void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
- const struct tcphdr *th, unsigned int len)
- {
- ...
- if (len <= tcp_header_len) {
- /* Bulk data transfer: sender */
- if (len == tcp_header_len) {
- /* Predicted packet is in Windows by definition.
- * seq == rcv_nxt and rcv_wup <= rcv_nxt.
- * Hence, check seq<=rcv_wup reduces to:
- */
- if (tcp_header_len ==
- (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
- tp->rcv_nxt == tp->rcv_wup)
- tcp_store_ts_recent(tp);
- /* We know that such packets are checksummed
- * on entry.
- */
- tcp_ack(sk, skb, 0);// 入向 ack 处理
- __kfree_skb(skb);
tcp_data_snd_check(sk);--------------- 检查是不是有数据需要发送, 收到 ack 是触发数据发送的三大原因之一, 其余两个为用户调用发送以及定时器超时后发送
- return;
- }
处理完 ack 之后, 也就是清理了那些 ack 的 skb 之后, 需要再检查是否有数据发送, 因为一个有效的 ack 除了清理我们保存的 skb 外, 也就是调用 tcp_clean_rtx_queue, 还可能为我们提供了新的窗口信息:
- static inline void tcp_data_snd_check(struct sock *sk)
- {
- tcp_push_pending_frames(sk);
- ...
- }
- static inline void tcp_push_pending_frames(struct sock *sk)
- {
- if (tcp_send_head(sk)) {
- struct tcp_sock *tp = tcp_sk(sk);
- __tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);
- }
- }
- void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
- int nonagle)
- {
- .......
- if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
- sk_gfp_atomic(sk, GFP_ATOMIC)))
tcp_check_probe_timer(sk);--------------------------tcp_write_xmit 返回 1, 则设置 timer
}
最后进入到熟悉的 tcp_write_xmit 函数,
- static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
- int push_one, gfp_t gfp)
- {
- .....
if (tso_segs == 1) {------ 走的这个流程
if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
(tcp_skb_is_last(sk, skb) ?----------------- 确实是最后一个报文, 因为其他报文因为 nginx 限速没有下来
- nonagle : TCP_NAGLE_PUSH))))
- break;
- } else {
- if (!push_one &&
- tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
- max_segs))
- break;
- }
- ....
return (push_one == 2) || (!tp->packets_out && tcp_send_head(sk));-------push_one 为 0, 且 tp->packet_out 为 0,tcp_send_head 不为 NULL
}
break 出去之后, 并没有发包, 而是等待, 因为
return (push_one == 2) || (!tp->packets_out && tcp_send_head(sk)); 返回 1, 主要是 tp->packets_out 为 0, 而 tcp_send_head(sk) 非 NULL.
这样就进入了:
- static inline void tcp_check_probe_timer(struct sock *sk)
- {
- const struct tcp_sock *tp = tcp_sk(sk);
- const struct inet_connection_sock *icsk = inet_csk(sk);
- if (!tp->packets_out && !icsk->icsk_pending)
- inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
- icsk->icsk_rto, TCP_RTO_MAX);
- }
主要就是设置定时器, ICSK_TIME_PROBE0, 对应的超时时间为 rto,
- static inline u32 __tcp_set_rto(const struct tcp_sock *tp)
- {
- return usecs_to_jiffies((tp->srtt_us>> 3) + tp->rttvar_us);
- }
tp->rttvar_us 其实不能小于 200ms, 由于 tp->srtt_us 存储的是平滑 rtt 的 8 倍 (为了不进行 float 计算, 因为平滑的时候, 是取的 7/8 的老 rtt+1/8 新 rtt), 所以需要 >>3, 按照当前流来说,
在设置 rto 之前, 前面都是有多个 40ms 的 delay_ack 作为 rtt, 所以最终的定时器大概就是在 240ms 左右. 和 ttl 图形是吻合的. 当然, 由于第一个小包之前的 rtt 都不大, 平滑之后大概 20ms 左右,
所以第一个小包离收到 ack 之后的 rto 大概在 220ms, 也是符合图形的.
来源: http://www.bubuko.com/infodetail-2967610.html