网络协议栈:e1000 之上能跑 FTP/SMB/SSH
内核网络栈是真实存在的代码(src/kernel/net/),不是规划。它从 e1000 网卡驱动起,提供句柄化 TCP socket,再往上实现 FTP 客户端/服务端、TinySMB 服务端,以及用户态的 SSH 服务端。本页逐项拆解,并重点讲清协作式单线程带来的限制。
概览:协议族一览
网络栈分层很浅:一块真实驱动的网卡,一道句柄化的 TCP socket,三个跑在它上面的应用协议(FTP 双向、SMB 服务端、SSH 用户态)。下面这张图是架构,不是宣传。
- e1000 网卡驱动(QEMU 默认)
- TCP socket 句柄化 listen/accept/recv/send
- FTP 客户端 + 服务端
- SMB TinySMB 服务端
- 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 / 网关。
[net ] e1000 NIC: MAC 52:54:00:12:34:56 IP 10.0.2.15/24 GW 10.0.2.2
-
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,避免把单线程内核卡死。
// TCP socket(句柄化)
sock_listen(port)
sock_accept(lh, timeout_ms)
sock_recv(h, buf, max, timeout_ms)
sock_poll_recv(h, buf, max)
sock_send(h, buf, n)
sock_close(h)
sock_closed(h)
sock_readable(h)
sock_local_ip()
// FTP / SMB 客户端
ftp_get(host, port, user, pass, remote, local)
ftp_put(host, port, user, pass, local, remote)
smb_get(host, port, share, user, pass, remote, local)
smb_put(host, port, share, user, pass, local, remote)
// 辅助
net_info()
itoa(v, buf)
// 非阻塞控制台输入(服务主循环的“逃生舱”)
local_kbhit()
local_getc()
句柄化意味着什么
连接不是隐式的全局状态,而是用一个整数句柄 h 显式传递。这样单个服务进程可以同时持有一堆连接、按需要轮询(sock_poll_recv / sock_readable),而不会在某一连接上死等。配合 timeout_ms,单线程也能“看起来像”在并发处理。
FTP:客户端 + 服务端
net 命令同时提供 FTP 客户端传输(net get / net put)和服务端(net start ftp)。服务端默认监听 21,PASV 数据端口是 21000。
启动 FTP 服务端
net start ftp [port] # 默认 21;PASV 数据端口 21000
# 提示:launcher must forward both
客户端传输
net get ftp <host> <port> <user> <pass> <local> <remote>
net put ftp <host> <port> <user> <pass> <local> <remote>
⚠️ 为什么启动器要同时转发控制端口和数据端口
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 参数)。
# 启动 TinySMB 服务端(默认 445)
net start smb [port]
# 客户端传输(多一个 share 参数)
net get smb <host> <port> <share> <user> <pass> <local> <remote>
net put smb <host> <port> <share> <user> <pass> <local> <remote>
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 端口 | 用途 |
|---|---|---|
| 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 里运行。
tinysh> /bin/net_demo.TNCR # 467 bytes,演示网络 API 用法