TCP 滑动窗口机制是 Linux 内核网络协议栈的核心组成部分,主要涉及以下几个关键文件和数据结构:
- 核心数据结构:struct tcp_sock
- 窗口管理函数:tcp_rcv_space_adjust()
- 窗口更新逻辑:tcp_event_data_sent()、tcp_ack()
- 拥塞控制实现:tcp_congestion_control()
下面我们通过分析 Linux 内核源码(基于 5.15 版本)来理解滑动窗口的工作原理:
1. 关键数据结构定义
struct tcp_sock {
/* 拥塞控制相关参数 */
u32 snd_cwnd; /* 发送窗口大小 */
u32 snd_ssthresh; /* 慢启动阈值 */
u32 snd_wnd; /* 接收方通告的窗口大小 */
u32 snd_wl1; /* 最近一次窗口更新时的序列号 */
u32 snd_wl2; /* 最近一次窗口更新时的确认号 */
/* 序列号管理 */
u32 snd_una; /* 未确认的最早序列号 */
u32 snd_nxt; /* 下一个要发送的序列号 */
u32 snd_up; /* 紧急指针位置 */
u32 snd_wnd_clamp; /* 窗口大小上限 */
/* 接收窗口管理 */
u32 rcv_wnd; /* 当前接收窗口大小 */
u32 rcv_nxt; /* 期望接收的下一个序列号 */
u32 rcv_wup; /* 窗口更新时的确认号 */
};
2. 接收窗口调整逻辑
当接收到数据时,内核会调用tcp_rcv_space_adjust()函数调整接收窗口:
static void tcp_rcv_space_adjust(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
int window;
/* 计算可用接收缓冲区大小 */
window = sk->sk_rcvbuf - sk_wmem_queued(sk);
/* 应用窗口缩放因子 */
window = min_t(int, window, TCP_MAX_WINDOW);
window >>= tp->rx_opt.wscale;
/* 更新接收窗口大小 */
if (window != tp->rcv_wnd) {
tp->rcv_wnd = window;
/* 设置窗口更新标志 */
tcp_mark_window_update(sk);
}
}
3. 发送窗口计算逻辑
在发送数据前,内核会计算实际可用的发送窗口:
static inline u32 tcp_current_snd_window(const struct tcp_sock *tp)
{
/* 实际发送窗口 = min(拥塞窗口, 接收方通告窗口) */
return min(tp->snd_cwnd, tp->snd_wnd);
}
/* 计算可发送的字节数 */
static inline u32 tcp_send_headroom(const struct tcp_sock *tp)
{
u32 headroom;
/* 窗口右边界 - 下一个要发送的序列号 */
headroom = tcp_wnd_end(tp) - tp->snd_nxt;
/* 如果已经超过窗口大小,返回0 */
if (headroom > tp->snd_wnd)
headroom = 0;
return headroom;
}
4. 窗口滑动逻辑
当收到 ACK 确认时,内核会调用tcp_ack()函数处理确认并滑动窗口:
static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 ack = TCP_SKB_CB(skb)->ack_seq;
u32 old_una = tp->snd_una;
/* 处理ACK确认 */
if (after(ack, tp->snd_una)) {
/* 滑动窗口左边界到最新确认位置 */
tp->snd_una = ack;
/* 更新拥塞窗口(慢启动或拥塞避免) */
tcp_cong_ack(sk, ack, FLAG_SLOWPATH);
/* 检查是否有新的窗口更新 */
if (TCP_SKB_CB(skb)->rx_opt.wscale_ok &&
TCP_SKB_CB(skb)->window_clamp) {
tp->snd_wnd_clamp = TCP_SKB_CB(skb)->window_clamp;
}
/* 更新接收方通告的窗口大小 */
if (tcp_update_windows(tp, skb)) {
/* 窗口已更新,触发发送更多数据 */
tcp_event_data_sent(sk);
}
}
/* 其他ACK处理逻辑... */
return 0;
}
5. 窗口更新触发数据发送
当窗口更新或有新的确认到达时,内核会调用tcp_event_data_sent()尝试发送更多数据:
static void tcp_event_data_sent(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 如果有数据等待发送且窗口允许 */
if (tcp_write_queue_empty(sk) == 0 &&
tcp_send_headroom(tp) > 0) {
/* 尝试发送数据 */
tcp_send_active(sk);
}
}
6. 糊涂窗口综合征避免
Linux 内核通过以下逻辑避免糊涂窗口综合征:
static bool tcp_snd_wnd_test(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
int mss = tcp_current_mss(sk);
/* 窗口太小且缓冲区数据不足一个MSS时,暂不发送 */
if (tp->snd_wnd < mss && sk_wmem_queued(sk) < mss) {
/* 延迟发送,等待更多数据积累 */
return false;
}
return true;
}
滑动窗口核心机制总结
通过分析上述源码,可以总结出 TCP 滑动窗口的核心工作机制:
- 窗口大小动态调整:
- 接收窗口根据可用缓冲区空间动态调整
- 发送窗口取拥塞窗口和接收方通告窗口的最小值
- 窗口滑动触发条件:
- 收到新的 ACK 确认时,窗口左边界滑动到最新确认位置
- 接收方通告窗口更新时,窗口右边界相应调整
- 流量控制实现:
- 接收方通过窗口大小字段告知发送方自身处理能力
- 发送方根据接收方反馈调整发送速率
- 效率优化措施:
- 窗口扩大选项突破 65535 字节限制
- 糊涂窗口综合征避免机制
- 延迟确认和捎带确认减少 ACK 数量
通过这些机制的协同工作,TCP 滑动窗口实现了高效、可靠的网络数据传输。