上一章(四十二)修了一堆 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;                     // 又推进一次!
}

问题一:seqsnd_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 指令(如 vzeroallvmovdqu),而内核的 enable_sse() 只开启了 SSE,没有开启 AVX。

AVX 需要:

  1. CR4.OSXSAVE(bit 18)置 1
  2. 执行 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 解析。