TCP的核心系列 — 重传队列的更新和时延的采样(二)

本文详细解析了TCP协议中rtt采样条件、重复ACK处理、重传数据包的ACK处理及对不同状态的rtt采样规则。包括对skb记分牌的理解、释放段操作、RenoACK处理、rtt采样条件、icsk_ca_ops调用细节以及重置超时重传定时器的过程。

在tcp_clean_rtx_queue()中,并非对每个ACK都进行时延采样。是否进行时延采样,跟这个ACK是否为

重复的ACK、这个ACK是否确认了重传包,以及是否使用时间戳选项都有关系。

 

本文主要内容:tcp_clean_rtx_queue()的一些细节,时延采样的条件。

内核版本:3.2.12

Author:zhangskd @ csdn

 

记分牌

 

TCP_SKB_CB(skb)->sacked称为skb的记分牌,用于跟踪skb的状态。

以下是skb记分牌可能的取值:

#define TCPCB_SACKED_ACKED 0x01 /* skb被SACKed */
#define TCPCB_SACKED_RETRANS 0x02 /* skb retransmitted */
#define TCPCB_LOST 0x04 /* skb判断为LOST */
#define TCPCB_TAGBITS 0x07 /* All tag bits */
#define TCPCB_EVER_RETRANS 0x80 /* Ever retransmitted frame */
#define TCPCB_RETRANS (TCPCB_SACKED_RETRANS | TCPCB_EVER_RETRANS)

 

TCPCB_SACKED_RETRANS和TCPCB_EVER_RETRANS有什么区别呢?

当一个skb被重传了,它的记分牌就会添加TCPCB_RETRANS标志。

当重传的skb被sack确认或者ack累积确认时,就会清除TCPCB_SACKED_RETRANS标志,

但是这个时候TCPCB_EVER_RETRANS还保留,表示这个skb曾经被重传过。

TCPCB_EVER_RETRANS在更新RTT中发挥作用,如果skb曾经重传过,那么就不根据它来计算RTT

采样值,更新srtt和rto等。

 

释放段

 

从发送队列sk->sk_write_queue上移除skb,并非从物理内存中释放skb。

static inline void tcp_unlink_write_queue(struct sk_buff *skb, struct sock *sk)
{
    __skb_unlink(skb, &sk->sk_write_queue);
}

static inline void __skb_unlink(struct sk_buff *skb, struct sk_buff_head *list)
{
    struct sk_buff *next, *prev;
    list->qlen--; /* 发送队列长度减1 */
    next = skb->next;
    prev = skb->prev;
    skb->next = skb->prev = NULL;
    next->prev = prev;
    prev->next = next;
}

static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
    sock_set_flag(sk, SOCK_QUEUE_SHRUNK); /* 设置标志,表示有skb释放了*/
    sk->sk_wmem_queue -= skb->truesize; /* 更新发送队列所占内存*/
    sk_mem_uncharge(sk, skb->truesize);
    __kfree_skb(skb); /* 释放skb占用的内存*/
}

 

Reno ACK处理

 

场景:Reno中收到ACK的时候。

这时候会模拟SACK,更新tp->sacked_out和tp->reordering。

/* Account for ACK, ACKing some data in Reno Recovery phase. */
static void tcp_remove_reno_sacks(struct sock *sk, int acked)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (acked > 0) {
        /* One ACK acked hole. The rest eat duplicate ACKs.
         * 收到正常ACK,不在Recovery状态。
         * 收到Cummulative ACK,确认完所有的sacked包。
         */
        if (acked - 1 >= tp->sacked_out)
            tp->sacked_out = 0;
        else /* 收到Partial ACK,确认了部分sack包*/
            tp->sacked_out -= acked - 1;
    }

    /* 检查是否有乱序,有的话更新tp->reordering */
    tcp_check_reno_reordering(sk, acked); 
    tcp_verify_left_out(tp);
}

 

时延采样

 

(1)什么情况下进行RTT采样?

首先,如果是重复的ACK,即没有使snd_una前进的ACK,那么是不会进入RTT更新路径的:

if (flag & FLAG_ACKED)

{

    ...

    tcp_ack_update_rtt(sk, flag, seq_rtt);

    tcp_rearm_rto(sk);

    ...

}

 

其次,有确认新数据的情况下:

1. 没有使用时间戳选项(tcp_ack_no_tstamp)

    确认的数据段中不能包含重传过的段(FLAG_RETRANS_DATA_ACKED)。

    因为我们无法知道是哪个包,原始包还是重传包触发了这个ACK,因此无法确定触发包的

    发送时间。

    如果此ACK确认了多个段,则使用第一个被确认段的发送时间来计算seq_rtt。

 

2. 使用时间戳选项(tcp_ack_saw_tstamp)

    RTTM rule:A TSecr value received in a segment is used to update the averaged

    RTT measurement only if the segment acknowledges some new data, i.e, only if

    it advances the left edge of the send window.

    此时我们知道这个ACK的触发段的确切发送时间为:tp->rx_opt.rcv_tsecr,所以我们计算得到

    的RTT总是正确的,而不用去考虑触发这个ACK的是原始包还是重传包。

 

(2)icsk_ca_ops->pkts_acked()的调用

如果被此ACK确认的数据段中,含有重传过的数据段,那么就设置FLAG_RETRANS_DATA_ACKED,

ca_ops->pkts_acked(sk, pkts_acked, rtt_us)中的rtt_us = -1。

 

当有FLAG_ACKED标志(FLAG_DATA_ACKED | FLAG_SYN_ACKED)时调用。

有FLAG_RETRANS_DATA_ACKED设置,表示此ACK确认的是重传的数据包,这个时候rtt_us为-1。

#define FLAG_NOT_DUP (FLAG_DATA | FLAG_WIN_UPDATE | FLAG_ACKED)

Q: 那么,在什么情况下无法观测到正常的RTT呢?

A: 重传数据包的ACK、Dup ACK、Partical ACK、Cumulative ACK。

 

(3)各个状态下的时延采样

1. Open、CWR

    对每个ACK都进行RTT采样。

2. Disorder

    dup ACK,不进行RTT采样。

    cumulative ACK,进行RTT采样。

3. Recovery

    dup ACK,不进行RTT采样。

    partical ACK

         不启用Timstamp,不进行RTT采样。

         启用Timestamp,进行RTT采样。

    cumulative ACK

         不启用Timstamp,不进行RTT采样。

         启用Timestamp,进行RTT采样。

4. Loss

    partical ACK

         不启用Timstamp,不进行RTT采样。

         启用Timestamp,进行RTT采样。

    cumulative ACK

         不启用Timstamp,不进行RTT采样。

         启用Timestamp,进行RTT采样。

 

(4)重置超时重传定时器

每次更新SRTT和RTO后,都重置超时重传定时器。

/* Restart timer after forward progress on connection.
 * RFC2988 recommends to restart timer to now + rto.
 */
static void tcp_rearm_rto(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);

    if (! tp->packets_out) { /* 如果网络上没有数据,删除超时重传定时器*/
        inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);

    } else { /* 重置超时重传定时器*/
        inet_csk_reset_ximit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    }
}

 

非常好的问题! > ❓ **如何计算时延(Delay/Latency)?** 在计算机网络、音视频通信(如 VoIP、WebRTC)、实时系统中,**“时延”是衡量性能的核心指标之一**。我们来从不同场景出发,详细讲解: --- ## ✅ 简短答案: > 时延(Latency/Delay)是指数据从发送端发出到接收端成功接收到所经历的时间。 > > 常见类型包括: > > - **单向时延(One-way Delay, OWD)** > - **往返时延(Round-trip Delay, RTT)** > > 计算方式取决于协议可用信息,最常用的方法是利用 **RTCP 时间戳对** 或 **ICMP/TCP Echo 报文时间差**。 --- ## 🧩 一、基本概念:四种主要时延成分 总时延 = 传输时延 + 传播时延 + 处理时延 + 排队时延 | 类型 | 说明 | |------|------| | 传输时延 | 数据包长度 / 带宽(比如 1500B / 1Mbps ≈ 12ms) | | 传播时延 | 距离 / 光速(光纤中约 2×10⁸ m/s) | | 处理时延 | 路由器/网卡处理报头、校验等 | | 排队时延 | 包在队列中等待发送的时间(拥塞时显著增加) | 但我们通常说的“网络时延”指的是 **端到端的总延迟**,可以通过报文测量得到。 --- ## 🔍 、根据报文计算时延的几种方法 ### 方法 1:使用 **RTCP SR/RR 时间戳对**(VoIP/WebRTC 场景) 这是 **RTP 流中最标准的时延估算方法**,基于 RFC 3550 RFC 3611。 #### 步骤如下: 1. 发送方在 **RTCP SR(Sender Report)** 中插入两个时间戳: - `NTP_timestamp`:绝对时间(64位,高32位秒,低32位分数) - `RTP_timestamp`:对应的 RTP 时间戳 2. 接收方记录收到 SR 的本地系统时间 `arrival_ntp` 3. 接收方构造 **RR(Receiver Report)**,填写: - `LSR (Last SR)`:SR 的 NTP 时间整数部分 - `DLSR (Delay since Last SR)`:从收到 SR 到发送 RR 的时间(单位:1/65536 秒) #### ✅ 计算 RTT(往返时延)公式: ```text RTT = (Now - LSR) - DLSR ``` 其中: - `Now`:当前时间(接收方发送 RR 的时刻) - `LSR`:对方 SR 的 NTP 时间(已知) - `DLSR`:自己填写的延迟字段 📌 示例代码(C语言): ```c #include <stdint.h> #include <time.h> // 模拟 Now, LSR, DLSR(单位:毫秒) uint32_t now_ms = get_current_ntp_time_ms(); // 当前时间 uint32_t lsr_ms = rr->lsr; // 来自远端的 SR 时间 uint32_t dlsr_ms = (ntohs(rr->dlsr)) * 1000 / 65536; // DLSR 转 ms int32_t rtt_ms = (now_ms - lsr_ms) - dlsr_ms; printf("RTT: %d ms\n", rtt_ms); ``` > ⚠️ 注意:这只能计算 **RTT**,不能直接获得单向延迟(OWD),因为缺乏全局时钟同步。 --- ### 方法 2:使用 **NTP 协议** 精确计算单向延迟(需时间同步) NTP 使用四次握手来估计 OWD 时钟偏差。 ```text Client Server |--[T1]----->| |<---[T2,T3]--| |<--[T4]-----| ``` 四个时间点: - T1:客户端发送请求时间 - T2:服务器收到请求时间 - T3:服务器回复时间 - T4:客户端收到回复时间 #### ✅ 单向延迟(OWD)估算: 假设路径对称,则: ```python delay_one_way = ((T4 - T1) - (T3 - T2)) / 2 ``` > 这是唯一能在无 GPS 时钟下较准确估算 OWD 的方法。 --- ### 方法 3:使用 **ICMP Echo(ping)** 测量 RTT 最简单的工具级方法。 ```bash ping google.com ``` 输出示例: ```text 64 bytes from 142.250.76.142: icmp_seq=1 ttl=113 time=28.3 ms ``` 这里的 `time=28.3 ms` 就是 **RTT(往返时延)** #### 原理: - 发送 ICMP Echo Request 报文,打上发送时间戳 - 收到 Echo Reply 后,用当前时间减去发送时间 → 得到 RTT > ❌ 缺点:无法获取单向延迟;受防火墙限制;不反映应用层真实路径 --- ### 方法 4:使用 **TCP RTT**(通过 socket API) Linux 内核为每个 TCP 连接维护一个 RTT 估计值。 #### 在用户空间读取: ```c #include <sys/socket.h> #include <netinet/tcp.h> struct tcp_info info; socklen_t len = sizeof(info); getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, &len); uint32_t rtt_ms = info.tcpi_rtt / 1000; // 单位是微秒 printf("TCP RTT: %u ms\n", rtt_ms); ``` > ✅ 优点:反映真实业务流路径;自动平滑处理抖动 --- ### 方法 5:应用层主动探测(自定义协议) 你可以设计自己的“心跳包”协议: ```json { "type": "echo_request", "timestamp": 1712345678901 } ``` 对方回: ```json { "type": "echo_response", "request_time": 1712345678901, "response_time": 1712345678930 } ``` 客户端计算: ```python rtt = now() - request_time # 或更准: rtt = (response_time - request_time) + (now() - receive_time) ``` > 可用于 WebSocket、MQTT、自定义 UDP 协议等场景 --- ## 📊 三、不同类型时延对比 | 类型 | 是否可测 | 典型方法 | 应用场景 | |------|---------|----------|----------| | RTT | ✅ 容易测 | ping, RTCP, TCP info | 所有网络通信 | | OWD(单向延迟) | ⚠️ 难测 | NTP、PTP、双星钟同步 | VoIP、金融交易 | | 播放延迟 | ✅ 可推导 | RTP timestamp vs play time | 实时音视频 | | 抖动缓冲延迟 | ✅ 可控 | 自适应 Jitter Buffer | WebRTC 解码前 | --- ## 💡 四、实际建议与注意事项 ### ✅ 如何选择合适的测量方式? | 场景 | 推荐方法 | |------|-----------| | VoIP/QDMA 调试 | RTCP SR/RR + XR 报告 | | 网络连通性测试 | `ping` / `fping` | | TCP 性能监控 | `getsockopt(TCP_INFO)` | | 高精度时间同步 | NTP/PTP | | 自定义协议服务 | 应用层 echo + 时间戳 | ### ⚠️ 注意事项 1. **NTP 时间必须同步**,否则 OWD 不准 2. **网络路径可能不对称**,导致 OWD 估算偏差 3. **无线/WiFi 环境下 RTT 波动大**,需多次采样平均 4. **防火墙可能丢弃 ICMP**,应优先使用应用层探测 --- ## ✅ 总结 > ❓ **如何计算时延?** ✅ **答:** 根据场景不同,主要有以下几种方法: | 方法 | 公式/工具 | 输出 | |------|----------|-------| | RTCP SR/RR | `RTT = (Now - LSR) - DLSR` | RTT(VoIP) | | NTP 四步法 | `OWD = ((T4-T1)-(T3-T2))/2` | 单向延迟 | | ICMP Ping | `time = recv - send` | RTT(通用) | | TCP Info | `tcpi_rtt / 1000` | TCP 层 RTT | | 应用层心跳 | 自定义时间戳差 | 业务级延迟 | 其中,在 **VoIP QDMA 类实时通信系统中,推荐使用 RTCP 时间戳对来计算 RTT**,并结合 RTCP XR 获取更全面的质量评估。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值