Linux内核学习轨迹第八部:TCP协议实现深度拆解(第五节)
5. 传输层:TCP协议实现深度拆解
5.1 TCP的核心设计目标与协议特性
- 面向连接:通信双方必须先通过三次握手建立连接,才能传输数据;数据传输完成后,通过四次挥手断开连接,释放内核资源。内核会为每个TCP连接维护独立的连接状态、缓冲区、定时器、统计信息,实现端到端的独立通信。
- 可靠传输:通过序列号、确认应答、超时重传、校验和机制,保证数据100%完整交付,不会出现丢失、重复、乱序。哪怕IP层出现丢包、错包、乱序,TCP也能自动恢复,对上层应用完全透明。
- 有序字节流:TCP把上层应用的数据看做无边界的字节流,不保留消息边界,保证接收端收到的字节顺序,和发送端写入的顺序完全一致。上层应用无需处理乱序问题,只需按字节流读取即可。
- 流量控制:通过滑动窗口机制,保证发送端的发送速率不超过接收端的处理能力,避免发送端发送过快,导致接收端缓冲区溢出、数据包被丢弃。
- 拥塞控制:通过拥塞控制算法,实时探测网络的带宽、延迟、丢包情况,动态调整发送速率,避免发送过多数据包导致网络拥塞,最大化利用网络带宽,同时保证网络的公平性。
5.2 TCP核心数据结构深度拆解
// TCP传输控制块,继承自struct inet_sock → struct sock
struct tcp_sock {
// ===================== 1. 继承的基础结构 =====================
struct inet_sock inet; // IPv4专用结构,包含源目IP、端口等信息
// ===================== 2. 连接状态核心字段 =====================
u8 tcp_state; // TCP连接状态,对应11种标准状态(ESTABLISHED/TIME_WAIT等)
u8 repair; // 连接修复状态,用于TCP连接迁移
__be16 sport; // 源端口
__be16 dport; // 目的端口
u32 flags; // TCP连接全局标志位,如TSO开启、SACK支持等
// ===================== 3. 序列号核心字段(可靠传输的基石) =====================
u32 snd_nxt; // 下一个要发送的序列号
u32 snd_una; // 已发送但未收到ACK的最小序列号(发送窗口左边界)
u32 snd_wnd; // 对端通告的发送窗口大小(字节)
u32 rcv_nxt; // 下一个期望接收的序列号(接收窗口左边界)
u32 rcv_wnd; // 本机通告的接收窗口大小(字节)
u32 write_seq; // 应用层写入的最高序列号
u32 acked_seq; // 已收到ACK确认的最高序列号
u32 fin_seq; // FIN包的序列号,用于连接关闭
// ===================== 4. 窗口与流量控制字段 =====================
u32 snd_wl1; // 上次窗口更新的序列号
u32 snd_wl2; // 上次窗口更新的ACK序列号
u32 mss_cache; // 缓存的MSS(最大分段大小),避免重复计算
u16 advmss; // 向对端通告的MSS
u8 window_clamp; // 接收窗口的最大上限,用于窗口缩放
u32 rcv_wup; // 上次接收窗口更新的序列号
__u32 rcv_wnd_space; // 接收缓冲区的剩余可用空间
// ===================== 5. 拥塞控制核心字段 =====================
u32 snd_cwnd; // 拥塞窗口大小(单位:MSS),核心控制发送速率
u32 snd_ssthresh; // 慢启动阈值,区分慢启动和拥塞避免阶段
u32 prior_cwnd; // 拥塞前的拥塞窗口,用于丢包后恢复
u32 prr_delivered; // 比例降速恢复(PRR)的已交付数据包数
u32 prr_out; // PRR的已发送数据包数
const struct tcp_congestion_ops *ca_ops; // 拥塞控制算法操作集(BBR/CUBIC等)
u32 ca_priv[16]; // 拥塞控制算法的私有数据区
// ===================== 6. 重传机制核心字段 =====================
u32 retrans_out; // 正在重传的数据包数量
u32 lost_out; // 标记为丢失的数据包数量
u32 sacked_out; // 被SACK标记的数据包数量
u8 dupacks; // 重复ACK的数量,用于快速重传
u32 rto; // 当前的RTO(重传超时时间),单位毫秒
u32 srtt; // 平滑后的RTT(往返时间),单位微秒
u32 mdev; // RTT的平均偏差,用于RTO计算
u32 mdev_max; // 最大RTT偏差
u32 rttvar; // RTT方差,RTO计算的核心参数
// ===================== 7. 定时器核心字段 =====================
struct timer_list retrans_timer; // 重传定时器,超时触发重传
struct timer_list delack_timer; // 延迟ACK定时器
struct timer_list timewait_timer; // TIME_WAIT状态定时器
unsigned long timeout; // 超时时间戳
int icsk_retransmits; // 重传次数计数
int icsk_syn_retries; // SYN包重传次数
// ===================== 8. 队列核心字段 =====================
struct sk_buff_head out_of_order_queue; // 乱序接收队列,存储乱序到达的数据包
struct sk_buff_head write_queue; // 发送队列,待发送的数据包
struct sk_buff_head retransmit_queue; // 重传队列,已发送未收到ACK的数据包
};
- 序列号四元组:snd_una、snd_nxt、rcv_nxt、rcv_wnd是TCP可靠传输的核心,定义了发送窗口和接收窗口的边界,所有的发送、接收、确认逻辑都基于这四个字段展开。
- 拥塞窗口snd_cwnd:TCP发送速率的核心控制字段,单位是MSS。TCP的实际发送窗口 = min(对端通告的接收窗口snd_wnd, 拥塞窗口snd_cwnd),也就是「流量控制」和「拥塞控制」的最小值,保证既不超过接收端的处理能力,也不超过网络的承载能力。
- RTT与RTO:srtt是平滑后的往返时间,rto是重传超时时间。Linux内核通过srtt和RTT方差动态计算RTO,最小RTO为200ms,最大为120s,超时后会触发重传,同时指数退避延长RTO。
- 三大核心队列:
- write_queue:应用层写入、等待发送的数据包队列;
- retransmit_queue:已发送但未收到ACK的数据包队列,用于超时重传;
- out_of_order_queue:乱序到达的数据包队列,TCP会等待缺失的数据包到达后,再按序交给上层应用。
5.3 TCP连接生命周期管理
5.3.1 连接建立:三次握手的内核全流程
1. 服务端监听准备
1.服务端调用socket()创建TCP监听Socket,内核初始化tcp_sock结构,状态设置为TCP_CLOSE;
2.调用bind()绑定IP和端口,内核校验端口权限、端口占用情况,更新tcp_sock的源端口、IP;
3.调用listen()将Socket状态设置为TCP_LISTEN,初始化两个核心队列:
- 半连接队列(SYN队列):存储已收到客户端SYN、已回复SYN+ACK、等待客户端ACK的连接,状态为TCP_SYN_RECV;
- 全连接队列(ACCEPT队列):存储已完成三次握手、等待服务端accept()取出的连接,状态为TCP_ESTABLISHED;
4.服务端调用accept()阻塞等待,等待全连接队列中有新的连接。
2. 三次握手完整内核流程
客户端 → 发送SYN包(ISN=c,SYN标志位=1) → 服务端
客户端 ← 回复SYN+ACK包(ISN=s,ACK=c+1,SYN+ACK标志位=1) ← 服务端
客户端 → 发送ACK包(ACK=s+1,ACK标志位=1) → 服务端
1.第一次握手:客户端SYN → 服务端
- 客户端调用connect(),内核创建tcp_sock结构,随机生成初始序列号ISN,封装SYN包发送,状态设置为TCP_SYN_SENT;
- 服务端网卡收到SYN包,经过协议栈处理后,进入TCP层tcp_v4_do_rcv()函数;
- 服务端校验SYN包合法性,检查监听端口、半连接队列是否已满,若队列满且未开启syncookies,直接丢弃SYN包;
- 服务端随机生成自己的初始序列号ISN,分配struct request_sock请求结构,放入半连接队列,回复SYN+ACK包,状态设置为TCP_SYN_RECV;
- 开启重传定时器,若超时未收到客户端ACK,会重传SYN+ACK包,重传次数由tcp_synack_retries控制(默认5次)。
2.第二次握手:服务端SYN+ACK → 客户端
- 客户端收到SYN+ACK包,校验ACK序列号是否正确,更新自己的tcp_sock序列号、窗口信息;
- 客户端状态从TCP_SYN_SENT更新为TCP_ESTABLISHED,回复第三次握手的ACK包;
- 客户端Socket已完成连接建立,可调用send()发送数据。
3.第三次握手:客户端ACK → 服务端
- 服务端收到ACK包,校验序列号是否正确,从半连接队列中取出对应的request_sock结构;
- 服务端创建新的tcp_sock结构,初始化序列号、窗口、MSS等信息,状态设置为TCP_ESTABLISHED;
- 把新的连接放入全连接队列,唤醒阻塞在accept()的服务端进程;
- 服务端accept()取出全连接队列中的连接,返回新的文件描述符,连接正式建立完成,可进行数据传输。
生产环境高频问题:连接被拒绝
- 全连接队列最大长度 = min(net.core.somaxconn, listen()的backlog参数);
- 半连接队列最大长度 = max(net.ipv4.tcp_max_syn_backlog, 表项上限);
解决方案:
- 调大net.core.somaxconn到4096,同时应用层listen()的backlog参数同步调大;
- 调大net.ipv4.tcp_max_syn_backlog到8192;
- 开启net.ipv4.tcp_syncookies = 1,半连接队列满时,用syncookies机制处理SYN包,避免连接被丢弃;
- 优化服务端accept()处理逻辑,避免accept线程阻塞,及时取出全连接队列的连接。
5.3.2 连接断开:四次挥手的内核全流程
主动关闭方 → 发送FIN包(FIN标志位=1) → 被动关闭方
主动关闭方 ← 回复ACK包(ACK标志位=1) ← 被动关闭方
主动关闭方 ← 发送FIN包(FIN标志位=1) ← 被动关闭方
主动关闭方 → 回复ACK包(ACK标志位=1) → 被动关闭方
1.第一次挥手:主动关闭方发送FIN
- 主动关闭方调用close(),内核发送FIN包,状态从TCP_ESTABLISHED更新为TCP_FIN_WAIT1;
- 主动关闭方关闭自己的发送通道,不能再向被动关闭方写入数据,但仍可以接收数据。
2.第二次挥手:被动关闭方回复ACK
- 被动关闭方收到FIN包,内核回复ACK包,状态从TCP_ESTABLISHED更新为TCP_CLOSE_WAIT;
- 被动关闭方的接收通道关闭,read()会返回EOF,通知应用层连接即将关闭;
- 主动关闭方收到ACK包,状态从TCP_FIN_WAIT1更新为TCP_FIN_WAIT2,等待被动关闭方的FIN包。
3.三次挥手:被动关闭方发送FIN
- 被动关闭方调用close(),内核发送FIN包,状态从TCP_CLOSE_WAIT更新为TCP_LAST_ACK;
- 被动关闭方关闭自己的发送通道,不能再向主动关闭方写入数据。
4.第四次挥手:主动关闭方回复ACK
- 主动关闭方收到FIN包,内核回复ACK包,状态从TCP_FIN_WAIT2更新为TCP_TIME_WAIT;
- 被动关闭方收到ACK包,状态从TCP_LAST_ACK更新为TCP_CLOSE,连接完全关闭,释放内核资源;
- 主动关闭方进入TIME_WAIT状态,等待2MSL(最长报文寿命,默认60s)后,状态更新为TCP_CLOSE,释放内核资源。
生产环境两大高频问题
1. TIME_WAIT状态连接堆积
- 客户端场景:开启net.ipv4.tcp_tw_reuse = 1,允许将TIME_WAIT状态的端口用于新的TCP连接,这是最安全的优化方式;
- 架构优化:短连接改长连接复用(如HTTP Keep-Alive、数据库连接池),从根源减少TIME_WAIT的产生;
- 调大本地端口范围:net.ipv4.ip_local_port_range = 1024 65535,增加可用端口数量;
- 注意:Linux 4.12+内核已移除tcp_tw_recycle参数,该参数在NAT场景下会导致大量连接失败,禁止使用。
2. CLOSE_WAIT状态连接堆积
- 排查应用层代码,确保所有Socket的异常分支、EOF场景都有对应的close()逻辑;
- 给Socket设置SO_KEEPALIVE保活机制,检测到对端断开后自动关闭连接;
- 监控进程的CLOSE_WAIT连接数,超过阈值触发告警,及时定位代码问题。
5.4 TCP可靠传输核心机制
5.4.1 序列号与确认应答机制
- 核心规则:
- 发送端发送的每个TCP包,都包含本次数据的起始序列号和数据长度;
- 接收端收到数据后,回复ACK包,ACK序列号 = 已成功收到的最高序列号 + 1,代表这个序列号之前的所有字节都已成功接收;
- 发送端收到ACK后,就可以释放对应序列号的数据包,无需再保留重传副本;
- 若发送端超时未收到ACK,会触发重传机制,重新发送对应的数据包。
- 核心优势:通过累计确认机制,哪怕ACK包丢失,只要后续的ACK包到达,发送端就能确认之前的数据都已收到,无需重传,提升传输效率。
5.4.2 滑动窗口与流量控制
- 接收端在ACK包中,通过Window字段,通告自己当前的接收缓冲区可用空间大小,也就是接收窗口rcv_wnd;
- 发送端的发送窗口上限,就是接收端通告的接收窗口大小,发送端已发送未收到ACK的数据量,不能超过这个窗口大小;
- 随着接收端应用层读取数据,接收缓冲区可用空间增加,接收窗口会向右滑动,通告更大的窗口大小;
- 发送端收到新的窗口通告后,发送窗口也向右滑动,发送更多的数据。
- 糊涂窗口综合征:当接收端缓冲区只剩少量空间时,会通告很小的窗口,发送端发送小数据包,导致网络中充斥大量小包,带宽利用率极低。Linux内核通过Clark算法解决:接收端只有当可用空间达到MSS的一半时,才会通告非零的窗口;发送端只有当数据大小达到MSS,或者窗口足够大时,才会发送数据,避免小包发送。
- 零窗口处理:当接收端缓冲区满时,会通告零窗口,发送端停止发送数据,开启持续定时器,定期发送窗口探测包,直到接收端通告新的非零窗口,恢复发送。
5.5 TCP拥塞控制深度拆解
6.5.1 拥塞控制的核心框架
- 慢启动阶段:连接建立后,拥塞窗口cwnd从初始值(默认10个MSS)开始,每收到一个ACK,cwnd翻倍,呈指数增长,快速探测网络带宽;
- 拥塞避免阶段:当cwnd达到慢启动阈值ssthresh后,进入拥塞避免阶段,cwnd线性增长(每个RTT增加1个MSS),缓慢增加发送速率,避免网络拥塞;
- 拥塞触发阶段:当检测到网络丢包(超时重传、3个重复ACK),判定网络出现拥塞,降低慢启动阈值,减小拥塞窗口,降低发送速率;
- 快速恢复阶段:快速重传触发后,进入快速恢复阶段,通过PRR(比例降速恢复)算法调整cwnd,避免拥塞窗口降得过低,快速恢复发送速率。
5.5.2 主流拥塞控制算法对比与选型
|
特性 |
CUBIC(内核默认) |
BBR |
|
核心原理 |
基于三次函数的拥塞窗口增长,以丢包作为拥塞信号,丢包时降低窗口 |
基于带宽和最小RTT探测,不以丢包作为拥塞信号,以网络的最大带宽和最小延迟作为发送速率的依据 |
|
核心优势 |
高带宽长延迟网络中表现稳定,带宽利用率高,对不同网络环境的兼容性好,公平性强 |
不依赖丢包调整速率,在有随机丢包的广域网、跨地域网络中,带宽利用率远高于CUBIC,重传率更低,延迟更稳定 |
|
劣势 |
把随机丢包判定为拥塞,在有丢包的无线网络、跨地域网络中,会过度降低发送速率,带宽利用率低 |
在内网、低延迟无丢包的网络中,表现不如CUBIC;多流场景下的公平性略差于CUBIC;需要配合fq_codel排队规则使用 |
|
最佳适用场景 |
内网IDC环境、局域网、数据中心内部通信、无随机丢包的稳定网络 |
广域网、跨地域网络、CDN、大文件传输、有随机丢包的无线网络、跨境通信 |
# 查看内核支持的拥塞控制算法
cat /proc/sys/net/ipv4/tcp_available_congestion_control
# 临时设置默认拥塞控制算法为BBR
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 永久配置,写入/etc/sysctl.conf
net.ipv4.tcp_congestion_control = bbr
# 配合BBR使用,开启FQ排队规则
tc qdisc replace dev eth0 root fq
5.6 工程实践:TCP生产环境调优与问题排查
5.6.1 生产环境核心调优参数
# ===================== 连接管理参数 =====================
# 全连接队列最大长度
net.core.somaxconn = 4096
# 半连接队列最大长度
net.ipv4.tcp_max_syn_backlog = 8192
# 开启syncookies,应对SYN洪水攻击
net.ipv4.tcp_syncookies = 1
# SYN包重传次数
net.ipv4.tcp_syn_retries = 3
# SYN+ACK包重传次数
net.ipv4.tcp_synack_retries = 2
# 允许TIME_WAIT端口复用
net.ipv4.tcp_tw_reuse = 1
# TIME_WAIT超时时间,默认60s
net.ipv4.tcp_fin_timeout = 30
# 系统最大TIME_WAIT连接数
net.ipv4.tcp_max_tw_buckets = 262144
# ===================== 缓冲区与窗口参数 =====================
# Socket接收缓冲区默认值/最小值/最大值
net.core.rmem_default = 262144
net.core.rmem_max = 16777216
# Socket发送缓冲区默认值/最小值/最大值
net.core.wmem_default = 262144
net.core.wmem_max = 16777216
# TCP接收缓冲区:最小值/默认值/最大值
net.ipv4.tcp_rmem = 4096 262144 16777216
# TCP发送缓冲区:最小值/默认值/最大值
net.ipv4.tcp_wmem = 4096 262144 16777216
# 开启TCP窗口缩放,支持大带宽长延迟网络
net.ipv4.tcp_window_scaling = 1
# ===================== 可靠传输与重传参数 =====================
# 开启SACK选择性确认,提升丢包重传效率
net.ipv4.tcp_sack = 1
# 开启DSACK,检测重复包和虚假重传
net.ipv4.tcp_dsack = 1
# 开启FACK,前向重传,提升重传效率
net.ipv4.tcp_fack = 1
# TCP数据重传最大次数
net.ipv4.tcp_retries2 = 8
# 最小RTO,默认200ms
net.ipv4.tcp_min_rto = 200
# ===================== 拥塞控制参数 =====================
# 默认拥塞控制算法
net.ipv4.tcp_congestion_control = bbr
# 开启TCP时间戳,提升RTT计算精度
net.ipv4.tcp_timestamps = 1
# 开启ECN显式拥塞通知
net.ipv4.tcp_ecn = 1
# ===================== 保活机制参数 =====================
# TCP保活探测时间(连接空闲2小时后开始探测)
net.ipv4.tcp_keepalive_time = 7200
# 保活探测间隔
net.ipv4.tcp_keepalive_intvl = 75
# 保活探测失败次数,超过后断开连接
net.ipv4.tcp_keepalive_probes = 9
5.6.2 高频问题标准化排查流程
1. TCP重传率过高
- 确认丢包位置:用mtr/traceroute检测链路的丢包率,定位是内网、运营商、还是对端网络的丢包;
- 检查链路质量:用ping -f检测链路的延迟抖动、丢包情况,确认是否有随机丢包;
- 检查拥塞控制算法:广域网场景切换到BBR算法,内网场景使用CUBIC;
- 检查网卡与驱动:用ethtool -S eth0查看网卡是否有丢包、错误,是否有协商错误;
- 检查防火墙/TC规则:确认是否有限速、丢包规则导致的丢包;
- 优化参数:开启SACK/DSACK,调大TCP发送/接收缓冲区,优化MSS配置。
2. TCP连接超时
- 网络连通性检查:用ping/telnet检查服务端IP、端口是否可达,确认是否有防火墙拦截;
- 服务端监听检查:用ss -tulnp检查服务端是否在正确的IP、端口上监听;
- 连接队列检查:用netstat -s查看是否有监听队列溢出,调大somaxconn和backlog参数;
- 路由检查:用ip route检查客户端和服务端的路由是否正确,是否有路由不可达;
- 安全组/防火墙检查:确认服务端iptables、云厂商安全组是否放行了对应端口的访问;
- 内核参数检查:确认tcp_syn_retries、tcp_synack_retries是否设置过大,导致超时时间过长。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)