上一章(四十二)修了一堆 busybox 相关的 bug,让 ls/exec/wait 都能正常工作,wget_test 也能跑通 HTTP。但那时的 TCP 实现有个隐患:一旦丢包,传输就会永远卡住。这一章把重传机制做完整。
原代码的问题
ch42 的 tcp_send_raw 函数身兼数职:构造 TCP 包、发送、存入发送缓冲区、推进 snd_nxt。
static void tcp_send_raw(tcp_sock_t *s, uint8_t flags, const void *data, uint16_t dlen) {
...发包...
if (dlen) {
...存 sbuf...
s->snd_nxt += dlen;
}
if (flags & (TCP_SYN | TCP_FIN)) s->seq++; // 推进 seq
if (dlen) s->seq += dlen; // 又推进一次!
}
问题一:seq 和 snd_nxt 是两个字段,但都在追踪"下一个要发的序号",语义重复,而且 seq 被推进了两次(SYN/FIN 一次、数据一次)。
问题二:重传时的代码非常丑陋:
uint32_t saved_seq = s->seq;
uint32_t saved_nxt = s->snd_nxt;
s->seq = s->snd_una;
s->snd_nxt = s->snd_una;
tcp_send_raw(s, TCP_PSH | TCP_ACK, rtbuf, chunk);
s->snd_nxt = saved_nxt; // 手动恢复
s->seq = saved_seq;
这种"临时改状态再恢复"的方式容易出错,而且没有指数退避。
重构思路
把发包逻辑分成两层:
tcp_send_seg(s, seq, flags, data, dlen):底层,只负责构造和发包,seq 由调用方指定,不改任何状态。tcp_send_raw(s, flags, data, dlen):高层,调用tcp_send_seg,然后存 sbuf、推进snd_nxt、启动计时器。
这样重传就变得干净:直接调用 tcp_send_seg(s, s->snd_una, ...) 就行,不需要保存/恢复状态。
同时删掉了多余的 seq 字段,统一用 snd_una/snd_nxt:
| 字段 | 含义 |
|---|---|
snd_una |
已发送但未被 ACK 的起始序号 |
snd_nxt |
下一个新数据的序号 |
ack |
期望收到的对端序号(填入 ACK 字段) |
核心实现
tcp_send_seg:底层发包
static void tcp_send_seg(tcp_sock_t *s, uint32_t seq, uint8_t flags,
const void *data, uint16_t dlen) {
static uint8_t seg[2048];
tcp_hdr_t *tcp = (tcp_hdr_t *)seg;
tcp->src_port = htons(s->local_port);
tcp->dst_port = htons(s->remote_port);
tcp->seq = htonl(seq);
tcp->ack_seq = (flags & TCP_ACK) ? htonl(s->ack) : 0;
tcp->data_off = 0x50;
tcp->flags = flags;
tcp->window = htons(4096);
tcp->checksum = 0;
tcp->urgent = 0;
if (dlen) memcpy_s(seg + sizeof(tcp_hdr_t), data, dlen);
uint16_t tlen = sizeof(tcp_hdr_t) + dlen;
tcp->checksum = tcp_checksum(my_ip, s->remote_ip, seg, tlen);
ip_send(s->remote_ip, IP_PROTO_TCP, seg, tlen);
}
tcp_send_raw:发新数据
static void tcp_send_raw(tcp_sock_t *s, uint8_t flags, const void *data, uint16_t dlen) {
tcp_send_seg(s, s->snd_nxt, flags, data, dlen);
if (dlen) {
for (uint16_t i = 0; i < dlen; i++)
s->sbuf[(s->snd_nxt + i) % TCP_SBUF_SIZE] = ((const uint8_t *)data)[i];
s->snd_nxt += dlen;
s->rto_backoff = 0;
s->rto_deadline = pit_get_ticks() + TCP_RTO_BASE;
}
if (flags & (TCP_SYN | TCP_FIN)) s->snd_nxt++;
}
注意:SYN 和 FIN 各占 1 个序号,但没有 payload,所以只推进 snd_nxt 不存 sbuf。
tcp_retransmit_check:指数退避重传
#define TCP_RTO_BASE 200 /* 初始超时 ~200ms */
#define TCP_RTO_MAX 3200 /* 最大超时 ~3.2s */
static void tcp_retransmit_check(void) {
uint64_t now = pit_get_ticks();
for (int i = 0; i < TCP_MAX_SOCKS; i++) {
tcp_sock_t *s = &tcp_socks[i];
if (!s->used || s->state != TCP_ESTABLISHED) continue;
if (!s->rto_deadline || now < s->rto_deadline) continue;
uint32_t unacked = s->snd_nxt - s->snd_una;
if (unacked == 0) { s->rto_deadline = 0; continue; }
s->rto_backoff++;
uint32_t rto = TCP_RTO_BASE << s->rto_backoff;
if (rto > TCP_RTO_MAX) rto = TCP_RTO_MAX;
serial_print("[tcp] RTO retransmit unacked=");
serial_print_hex(unacked);
serial_print(" backoff=");
serial_print_hex(s->rto_backoff);
serial_print("\r\n");
uint16_t chunk = (unacked > 1460) ? 1460 : (uint16_t)unacked;
static uint8_t rtbuf[1460];
for (uint16_t j = 0; j < chunk; j++)
rtbuf[j] = s->sbuf[(s->snd_una + j) % TCP_SBUF_SIZE];
tcp_send_seg(s, s->snd_una, TCP_PSH | TCP_ACK, rtbuf, chunk);
s->rto_deadline = now + rto;
}
}
超时时间从 200ms 起,每次重传翻倍:200 → 400 → 800 → 1600 → 3200ms(上限)。
SYN 重传
原来 tcp_connect 发一次 SYN 等 500ms,超时就失败。现在改成循环重试:
#define TCP_SYN_RETRIES 5
int tcp_connect(int s, uint32_t ip, uint16_t port) {
...初始化 sock...
uint32_t rto = TCP_RTO_BASE;
for (int attempt = 0; attempt < TCP_SYN_RETRIES; attempt++) {
serial_print("[tcp] SYN");
if (attempt) { serial_print(" retry="); serial_print_hex(attempt); }
serial_print("\r\n");
tcp_send_seg(sock, sock->syn_seq, TCP_SYN, 0, 0);
uint64_t deadline = pit_get_ticks() + rto;
while (pit_get_ticks() < deadline) {
sti; net_poll(); cli;
if (sock->state == TCP_ESTABLISHED) return 0;
if (sock->state == TCP_CLOSED) return -1;
}
rto = (rto * 2 > TCP_RTO_MAX) ? TCP_RTO_MAX : rto * 2;
}
sock->state = TCP_CLOSED;
return -1;
}
SYN 用 tcp_send_seg 而非 tcp_send_raw,因为 SYN 重传需要用相同的 syn_seq(不能推进 snd_nxt)。
ACK 处理:部分确认时重置退避
if ((int32_t)(ack_val - s->snd_una) > 0) {
s->snd_una = ack_val;
if (s->snd_una == s->snd_nxt) {
s->rto_deadline = 0; // 全部确认,停止计时
} else {
s->rto_backoff = 0; // 部分确认,重置退避(网络在恢复)
s->rto_deadline = pit_get_ticks() + TCP_RTO_BASE;
}
}
验证重传
用一个简单的内核注入法:在 tcp_send_seg 里人工丢掉第一个 PSH 数据包,然后跑 wget_test:
[tcp] SYN
[tcp] ESTABLISHED
connected!
[tcp] TEST drop pkt seq=0x12345679 ← 第一个包被丢弃
request sent
[tcp] RTO retransmit unacked=0x27 backoff=0x1 ← 200ms后重传,退避=1
...
HTTP/1.0 200 OK
...
DONE ← 传输成功!
从 [tcp] TEST drop pkt 到 [tcp] RTO retransmit 中间等了约 200ms(TCP_RTO_BASE),backoff=1 表示这是第一次重传。
和 ch42 的差异
| 功能 | ch42 | ch43 |
|---|---|---|
| 发包/状态分离 | 否(tcp_send_raw 身兼数职) |
是(tcp_send_seg + tcp_send_raw) |
| 重传 | 有但逻辑混乱 | 干净,直接用 tcp_send_seg |
| 指数退避 | 固定 RTO*2 |
TCP_RTO_BASE << backoff,上限 3200ms |
| SYN 重传 | 无 | 最多 5 次,指数退避 |
冗余 seq 字段 |
有 | 删除,统一用 snd_nxt |
Bug 修复:busybox AVX 指令崩溃
这章还有一个 bug:运行 ls 时内核崩溃:
[EXCEPTION] Invalid Opcode (int=0x6 rip=0x6915a1)
int=6 是 #UD(Invalid Opcode),RIP 在用户空间,说明是 busybox 执行了内核不支持的指令。
原因:busybox 预编译二进制包含 AVX 指令(如 vzeroall、vmovdqu),而内核的 enable_sse() 只开启了 SSE,没有开启 AVX。
AVX 需要:
- CR4.OSXSAVE(bit 18)置 1
- 执行
xsetbv XCR0=7(开启 x87 + SSE + AVX 状态保存)
坑:直接加 xsetbv 会让内核完全无法启动——因为 xsetbv 指令本身在不支持 XSAVE 特性的 CPU 上也会触发 #UD。QEMU 默认 CPU 不一定暴露 XSAVE。
修复:先用 CPUID 检查再执行:
uint32_t ecx = 0;
__asm__ volatile("mov $1, %%eax; cpuid" : "=c"(ecx) :: "eax", "ebx", "edx");
if (ecx & (1u << 26)) { // CPUID leaf 1, ECX[26] = XSAVE 支持
cr4 |= (1ULL << 18); // OSXSAVE
__asm__ volatile("mov %0, %%cr4" :: "r"(cr4));
__asm__ volatile("xor %%ecx, %%ecx; mov $7, %%eax; xor %%edx, %%edx; xsetbv"
::: "eax", "ecx", "edx");
} else {
__asm__ volatile("mov %0, %%cr4" :: "r"(cr4));
}
另外,SMP 的 AP 核心(ap_main())也要做同样的初始化,否则进程被调度到 AP 时同样会崩溃。
小结
TCP 重传的核心思路:发出去的数据先存到发送缓冲区,直到收到 ACK 才丢弃;超时未确认就重发,每次超时加倍。
实现时最容易犯的错是把"发新数据"和"重传"混在一起用同一个函数,导致状态被意外修改。把底层发包(tcp_send_seg)和状态管理(tcp_send_raw)分开,代码就清晰很多。
下一章做 UDP socket 和 DNS 解析。