从零写OS(四十五):TCP 服务端——bind/listen/accept

前面几章都是做 TCP 客户端——发 SYN、连接、发请求、收响应。这章反过来,让内核能作为 TCP 服务端:监听端口,接受连接,响应请求。 客户端 vs 服务端的区别 客户端:主动发 SYN,等待 SYN-ACK。 服务端:被动等待 SYN,收到后发 SYN-ACK,等 ACK 完成三次握手,然后通过 accept() 把新连接交给应用层。 关键区别在于:服务端有两种 socket 角色—— 角色 职责 监听 socket(listen fd) 绑定端口,等待连接请求 连接 socket(accept fd) 每个客户端连接对应一个,负责实际数据收发 三步接口 bind(fd, &sa, len) // 绑定本地端口 listen(fd, backlog) // 进入监听状态 accept(fd, &sa, &len) // 阻塞,等到有连接,返回新 fd 数据结构 tcp_sock_t 加了几个字段: int is_listener; int accept_queue[8]; // 存已完成三次握手的连接 index uint32_t accept_head, accept_tail; 还新增了两个状态: TCP_LISTEN:监听中,等待 SYN TCP_SYN_RECV:收到 SYN 已回 SYN-ACK,等最终 ACK 核心流程 1. 收到 SYN tcp_handle() 里单独处理 SYN: if ((tcp->flags & TCP_SYN) && !(tcp->flags & TCP_ACK)) { // 找到监听该端口的 socket // 分配新连接 slot ns->state = TCP_SYN_RECV; ns->local_port = dport; ns->remote_port = sport; ns->remote_ip = src_ip; ns->ack = ntohl(tcp->seq) + 1; // 下次要 ACK 的序号 ns->syn_seq = 0xABCD1234; ns->snd_una = ns->snd_nxt = ns->syn_seq; tcp_send_seg(ns, ns->syn_seq, TCP_SYN | TCP_ACK, 0, 0); } 2. 收到最终 ACK(完成三次握手) if (s->state == TCP_SYN_RECV && (tcp->flags & TCP_ACK)) { if (ack_val == s->syn_seq + 1) { s->snd_una = s->snd_nxt = ack_val; s->state = TCP_ESTABLISHED; // 放入监听 socket 的 accept queue ls->accept_queue[ls->accept_tail % 8] = i; ls->accept_tail++; } } 3. accept() 取连接 int tcp_accept(int s, ...) { tcp_sock_t *ls = &tcp_socks[s]; while (ls->accept_head == ls->accept_tail) { sti; net_poll(); cli; // 阻塞等待 } int ni = ls->accept_queue[ls->accept_head++ % 8]; return ni; } 测试 写了一个简单的 HTTP 服务端程序: ...

June 2, 2026 · 2 min · 大飞

从零写OS(四十三):TCP 重传 —— 丢包了也能传完

上一章(四十二)修了一堆 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; 这种"临时改状态再恢复"的方式容易出错,而且没有指数退避。 ...

June 1, 2026 · 4 min · 大飞

从零写OS(三十七):网络栈 —— e1000 驱动 + ARP/IP/TCP + Socket

文件系统能读写了,这一章加网络支持。目标是实现一个最小 TCP/IP 栈,让用户程序能通过 socket 发送 HTTP 请求并收到响应。 验证方式: / # wget_test wget_test start connecting... connected! request sent HTTP/1.0 200 OK ... DONE 架构概览 用户程序(wget_test) ↕ syscall: socket/connect/write/read/close 内核 socket 层(net.c) ↕ tcp_connect / tcp_send / tcp_recv / tcp_close TCP 层(net.c) ↕ 以太网帧收发 ARP 层(net.c) ↕ e1000_send / e1000_recv e1000 网卡驱动(e1000.c) ↕ MMIO + DMA QEMU e1000 虚拟网卡(-netdev user,id=net0 -device e1000,netdev=net0) PCI 枚举 e1000 通过 PCI 总线连接。内核在启动时枚举 PCI 设备: void pci_enumerate(void) { for (bus=0; bus<256; bus++) for (dev=0; dev<32; dev++) for (fn=0; fn<8; fn++) { uint16_t vendor = pci_read16(bus, dev, fn, 0); if (vendor == 0xFFFF) continue; // 记录 vendor/device/bar0 } } e1000 的 vendor=0x8086,device=0x100E。BAR0 是 MMIO 基地址(通常 0xFEBC0000)。 ...

May 22, 2026 · 4 min · 大飞
京ICP备14031575号-3