网络协议栈:e1000 之上能跑 FTP/SMB/SSH

内核网络栈是真实存在的代码(src/kernel/net/),不是规划。它从 e1000 网卡驱动起,提供句柄化 TCP socket,再往上实现 FTP 客户端/服务端、TinySMB 服务端,以及用户态的 SSH 服务端。本页逐项拆解,并重点讲清协作式单线程带来的限制。

概览:协议族一览

网络栈分层很浅:一块真实驱动的网卡,一道句柄化的 TCP socket,三个跑在它上面的应用协议(FTP 双向、SMB 服务端、SSH 用户态)。下面这张图是架构,不是宣传。

  1. e1000 网卡驱动(QEMU 默认)
  2. TCP socket 句柄化 listen/accept/recv/send
  3. FTP 客户端 + 服务端
  4. SMB TinySMB 服务端
  5. SSH SSHD.TNCR 用户态
  • e1000.c / e1000.h

    e1000 网卡驱动。QEMU 默认的虚拟网卡就是这个型号,所以开箱即可联调。

  • net.c / net.h

    网络栈核心:地址、收发、连接管理都在这里。

  • ftp.c / ftp.h

    FTP:既是客户端也是服务端。

  • smb.c / smb.h

    SMB:内部叫 TinySMB,是服务端实现。

  • sock.c / sock.h

    TCP socket:句柄化接口,带 timeout_ms / poll_recv / readable / local_ip。

e1000 网卡驱动

e1000.c 实现了 QEMU 默认虚拟网卡的驱动。内核启动时打印一行真实的网卡配置,直接在下面这行日志里核对 MAC / IP / 网关。

  • MAC 52:54:00:12:34:56

    QEMU 给 e1000 分配的默认 MAC 地址。

  • IP 10.0.2.15/24

    QEMU user-mode 网络默认就是 10.0.2.0/24 这一段。

  • GW 10.0.2.2

    同一网段的默认网关,出网流量经它转发。

TCP socket API(句柄化)

用户程序通过 tinyos_api_t 函数指针表拿到网络能力。socket 这一族是「句柄化」设计:listen 拿到监听句柄 lh,accept 拿到连接句柄 h,之后 recv / send / close 都围绕 h 转。每个会阻塞的调用都带 timeout_ms,避免把单线程内核卡死。

句柄化意味着什么

连接不是隐式的全局状态,而是用一个整数句柄 h 显式传递。这样单个服务进程可以同时持有一堆连接、按需要轮询(sock_poll_recv / sock_readable),而不会在某一连接上死等。配合 timeout_ms,单线程也能“看起来像”在并发处理。

FTP:客户端 + 服务端

net 命令同时提供 FTP 客户端传输(net get / net put)和服务端(net start ftp)。服务端默认监听 21,PASV 数据端口是 21000。

启动 FTP 服务端

客户端传输

⚠️ 为什么启动器要同时转发控制端口和数据端口

FTP 用两条连接:21 是控制通道(命令/响应),真正传文件走的是 PASV 模式下的另一条数据连接(本栈默认 21000)。客户端连进来时,数据端口也必须从宿主机可达,否则传文件会卡在“等数据连接”。所以启动器(start.bat / build.ps1)要同时把 21(或 2222→22 之外)和 21000 这两类端口都做转发,缺一个 FTP 就只能“登录成功、传不了文件”。

SMB(TinySMB)服务端

smb.c 在内核里实现了一个叫 TinySMB 的服务端,默认监听 445。客户端侧同样有 smb_get / smb_put 两个传输函数(注意比 FTP 多一个 share 参数)。

SSH:SSHD.TNCR 用户态服务

SSH 不是内核内建,而是一个用户态程序 /bin/SSHD.TNCR(114500 字节)。它的运行参数来自 netconf 配置,对外通过端口转发 2222→22 暴露给宿主机。

  • /bin/SSHD.TNCR

    114500 字节的用户态 SSH 服务端,独立的进程。

  • netconf

    SSHD 的运行配置来源(网络配置 / 账号等)。

  • 2222 → 22

    宿主机的 2222 转发到 TinyOS 的 22,使外部 SSH 客户端能连进来。

端口转发对照表

QEMU 把宿主机端口转发进 TinyOS。下面来自 start.bat 与 build.ps1 里真实写的规则。注意 FTP 占了两条(控制 21 + 数据 21000)。

宿主端口 ↔ TinyOS 端口(来自 start.bat / build.ps1)
宿主端口 TinyOS 端口 用途
2222 22 SSH(SSHD.TNCR)。
21 21 FTP 控制通道(build.ps1 中的规则)。
21000 21000 FTP PASV 数据端口(start.bat 与 build.ps1 都转发)。

start.bat 里显式转发的是 2222 和 21000;build.ps1 的 $net 定义里额外包含 21→21。无论哪份脚本,FTP 的“控制 + 数据”两条端口都必须同时可达,否则传文件会失败。

协作式单线程的限制(重点)

内核是协作式单线程:net_poll_all() 一次只处理一个连接。这条约束直接决定了服务端必须怎么写,是理解本栈最需要记住的一点。

⚠️ 不能做 keep-alive 长连接

因为一次只服务一个连接,如果某个连接保持打开、等服务端“等下一个请求”,它会一直占着 net_poll_all() 的档期。结果:其它连接饿死、整体超时。所以服务端必须每请求独立建连,并在响应末尾使用 Connection: close —— 处理完立刻关,把档期让给下一个连接。

  • net_poll_all()

    一次只处理一个连接;没有真正的并发调度。

  • 每请求独立建连

    服务端不要复用旧连接,处理完就用 Connection: close 关掉。

  • CPU 负载怎么算

    用 g_sys_idle_ticks 占空比反推:pit.c 在 pit_sleep 里累加空闲节拍,空闲越多负载越低。

  • local_kbhit() / local_getc()

    非阻塞控制台输入,是服务主循环的“逃生舱”——可以中途按键退出,而不阻塞在网络轮询上。

示例程序:/bin/net_demo.TNCR

仓库里带一个最小的网络示例程序 /bin/net_demo.TNCR,体积只有 467 字节,演示如何调用上面的 socket / FTP / SMB API。它的源码打包进了 romfs,可直接在 tinysh 里运行。