从零写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(四十四):UDP Socket + DNS 解析

上一章(四十三)把 TCP 重传做好了。这一章加两个新功能:UDP socket 用户空间接口 和 DNS 解析 syscall,让用户程序可以用域名来连接服务器。 目标 内核里其实早就有 udp_send 和 dns_resolve 这两个函数了,但用户程序用不到,因为没有对应的 syscall。这章要做的就是把这两个内核能力"打通"到用户空间: socket(AF_INET, SOCK_DGRAM, 0) → 分配一个 UDP socket,返回 fd 200+ sendto / recvfrom → 通过 UDP socket 收发数据 SYS_DNS_RESOLVE(自定义 syscall 500)→ 通过域名查 IP UDP Socket 设计 UDP 比 TCP 简单很多:无连接、无状态机、无重传。核心是一个接收队列:内核收到 UDP 包时,把它放进对应 socket 的队列,用户程序再来取。 typedef struct { uint32_t src_ip; uint16_t src_port; uint16_t len; uint8_t data[512]; // 每个包最多 512 字节 } udp_pkt_entry_t; typedef struct { int used; uint16_t local_port; udp_pkt_entry_t queue[4]; // 最多暂存 4 个包 uint32_t qhead, qtail; } udp_sock_t; 队列用无限增长的 qhead/qtail 计数(不是环形下标),访问时用 % UDP_PKT_MAX,满了就丢包: ...

June 2, 2026 · 3 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