运行时流程
本文描述 ax-net 从初始化到运行期收发包、socket 阻塞等待、DHCP/DNS 和本地 transport 的关键流程。流程以源码中的真实边界为准:应用线程只修改 socket 状态并请求 poll,设备 worker 只搬运 packet,专用 net-poll worker 独占推进 smoltcp Interface 和全局 SocketSet。
核心流程涉及:
| 流程 | 关键源码 |
|---|---|
| 初始化 | lib.rs init_network() |
| poll 主循环 | lib.rs net_poll_worker() / poll_until_idle() |
| 协议推进 | service.rs Service::poll() |
| RX/TX dispatch | router.rs Router::poll() / Router::dispatch() |
| socket wait | general.rs send_poller_with() / recv_poller_with() |
| TCP passive open | listen_table.rs / router.rs snoop_tcp_packet() |
| DHCP/DNS | service.rs, lib.rs |
总体时序
运行期只有一个协议核心 owner:net-poll worker。socket 调用者和设备 worker 都通过轻量唤醒把工作交给它。
初始化阶段
初始化阶段构造控制面、单协议核心和多设备数据面。init_network() 是一次性入口,重复调用会 panic。
配置校验
init_network() 首先校验 NetworkConfig:
- 接口名不能是保留名
lo。 dhcp = true不能同时配置static_ip。- 静态 IP 不能是 unspecified。
- 静态 prefix 不能大于 32。
- DNS server 不能是 unspecified。
- 每个显式
InterfaceConfig必须匹配一个设备。 - 一个设备不能被多个 config 同时匹配。
- 接口名不能冲突。
网关 0.0.0.0 是有效配置,表示不安装默认路由。
设备与控制面构建
接口 ID 约定:
InterfaceId(1)固定为lo。- Ethernet 接口从
InterfaceId(2)开始按设备发现顺序分配。 InterfaceId(0)是 Router TX 内部占位符,不出现在 public API。
DHCP Bootstrap
如果存在 DHCP 接口,初始化末尾调用 wait_for_dhcp_bootstrap():
fn wait_for_dhcp_bootstrap() {
for _ in 0..DHCP_BOOTSTRAP_ATTEMPTS {
request_poll();
if get_service().dhcp_configured() {
return;
}
ax_task::sleep(DHCP_BOOTSTRAP_POLL_INTERVAL);
}
warn!("DHCP bootstrap timed out");
}
bootstrap 等待只要求任一 DHCP 接口成功获得地址。这样一个断开的 DHCP 网口不会阻塞整个系统启动;未配置完成的接口后续仍由 net-poll worker 继续推进。
Poll 主循环
poll 主循环由 net-poll 线程执行。它合并 socket、设备和 timer 唤醒,并通过 CAS 防止重入。
request_poll
pub fn request_poll() {
publish_poll_request(&NET_POLL_REQUESTED, || {
NET_POLL_WAKE.notify_one(true);
});
}
fn publish_poll_request(requested: &AtomicBool, wake: impl FnOnce()) {
if !requested.swap(true, Ordering::AcqRel) {
wake();
}
}
request_poll() 不执行协议栈,只设置标志并唤醒 worker。关键点:publish_poll_request() 用 swap 检测 false→true 转换,仅在该转换发生时才调用 notify_one(),避免已经在 pending 状态下重复 wake 唤醒造成的额外调度开销。socket 热路径、设备 RX/TX worker、DHCP/DNS 查询都使用这个入口。
poll_until_idle
fn poll_until_idle() {
POLL_AGAIN.store(true, Ordering::Release);
loop {
if POLLING_INTERFACES
.compare_exchange(false, true, Ordering::Acquire, Ordering::Acquire)
.is_err()
{
return;
}
while POLL_AGAIN.swap(false, Ordering::AcqRel) {
while get_service().poll(&mut SOCKET_SET.inner.lock()) {}
}
POLLING_INTERFACES.store(false, Ordering::Release);
if !POLL_AGAIN.load(Ordering::Acquire) {
return;
}
}
}
poll_until_idle() 内联 poll 调用的锁顺序是:
SERVICE -> SOCKET_SET.inner -> Service::poll()
poll_until_idle() 在仍有工作时批量推进,不主动 yield。它是专用 worker,不需要在每个 packet 后让出给应用线程。
defer_poll_wake
smoltcp 在 Interface::poll() 中调用 socket waker 时,SocketSet 仍被持有。此时如果 waker 直接调用 PollSet::wake(),可能在 PollSet 内部触发 re-registration 等需要 socket 锁的操作,导致潜在重入或死锁。因此 ax-net 使用延迟唤醒机制:
// lib.rs
pub(crate) fn defer_poll_wake(poll: Arc<PollSet>, ready: IoEvents) {
DEFERRED_POLL_WAKES.lock().push((poll, ready));
if !DEFERRED_POLL_WAKE_PENDING.swap(true, Ordering::AcqRel) {
NET_POLL_WAKE.notify_one(true);
}
}
DeferPollWake 实现 Wake trait,被所有需要安全唤醒的 smoltcp socket 路径使用。每次 poll_until_idle() 结束前,net-poll worker 调用 drain_deferred_poll_wakes() 批量执行延迟的 PollSet 唤醒——此时 SocketSet 已解锁。
wake_net_task_irq / NET_IRQ_NOTIFY
IRQ 上下文不能直接进入协议栈。设备通过 wake_net_task_irq() 设置 NET_IRQ_NOTIFY,通知 net-poll worker 有新的设备事件需要处理。该函数只做原子置位 + wait-queue wake,不涉及任何锁:
pub(crate) fn wake_net_task_irq() {
NET_IRQ_NOTIFY.notify();
request_poll();
}
Service::poll
Service::poll() 是协议核心的单轮调度。下面是保留关键顺序的简化示意:
pub fn poll(&mut self, sockets: &mut SocketSet) -> bool {
let timestamp = now();
let mut dhcp_events = Vec::new();
let mut dhcp_server_replies = Vec::new();
self.router.poll(timestamp, sockets, |interface_id, packet| {
// DHCP client/server snoop
});
for event in dhcp_events {
self.handle_dhcp_event(event);
}
let mut dhcp_server_sent = false;
for (dev, reply) in dhcp_server_replies {
dhcp_server_sent |= self.router.send_on_device(
dev,
IpAddress::Ipv4(Ipv4Address::BROADCAST),
&reply,
timestamp,
);
}
let socket_state_changed =
self.iface.poll(timestamp, &mut self.router, sockets) == PollResult::SocketStateChanged;
let dhcp_poll_next = self.poll_dhcp(timestamp);
crate::orphan::reap_orphans(timestamp, sockets);
self.router.dispatch(timestamp, sockets)
|| dhcp_poll_next
|| dhcp_server_sent
|| socket_state_changed
}
顺序约束:
Router::poll()在 smoltcp 前执行,让新 RX packet 进入rx_buffer。- DHCP snoop 在 smoltcp 消费 packet 前执行,保留 ingress
InterfaceId。 - DHCP ACK/NAK 先生成
NetworkStateUpdate,再提交到 smoltcp address list、控制面和 route table。 Router::dispatch()在 smoltcp 后执行,把本轮生成的 TX packet 送出。
数据面流程
数据面由 RX path、TX path 和 loopback fast path 组成。真实设备通过 worker 和有界队列与协议核心解耦。
RX Path
数据结构转换:
driver RX buffer
-> device_rx_worker local PacketBuffer
-> shared BoundedPacketQueue<RxPacket>
-> Router.rx_buffer
-> RxToken -> smoltcp Interface::poll()
RxPacket 保存 ingress InterfaceId,用于 DHCP 分发和诊断。队列满时丢包并记录 warning,避免网络热路径无界增长。
TX Path
dispatch 规则:
- IPv4 limited broadcast 发往所有非 loopback 设备。
- IPv4/IPv6 单播按
(dst, src)查select_route_for_source()。 - 源地址必须与 route rule 的 source 一致,避免多宿主环境下从错误接口发包。
- loopback 目的地直接写入
Router.rx_buffer。 - 普通设备 TX 进入 per-device
tx_queue,由 TX worker 调用Device::send()。
Loopback Fast Path
loopback 普通 TX 不进入设备队列:
Router.tx_buffer
-> Router::dispatch()
-> inject_loopback_rx_direct()
-> Router.rx_buffer
-> next Service::poll() / same idle loop
inject_loopback_rx_direct() 在写入 RX buffer 前调用 snoop_tcp_packet(),因此 loopback TCP SYN 可以在同一轮 poll 中预创建 accept child socket。
ARP / Neighbor
Ethernet TX 需要把 IP packet 发送到 next-hop MAC:
Device::send(next_hop, ip_packet)
-> neighbor cache hit: encapsulate Ethernet frame + transmit
-> miss with pending ARP: queue packet in pending_packets
-> miss without pending ARP: send ARP request + queue packet
入站 ARP reply 或 gratuitous ARP 会更新 neighbor 表,并释放等待该 next hop 的 pending packet。neighbor TTL 为 300 秒,ARP retry 间隔为 1 秒。
Socket 流程
socket 流程只修改 socket 状态并注册 waker;协议状态机推进交给 net-poll worker。
TCP Connect
TcpSocket::connect(remote)
-> choose/bind local endpoint
-> control plane route/source decision
-> smoltcp tcp::Socket::connect()
-> state = Connecting
-> request_poll()
-> poll_io waits for OUT or error
连接完成由 smoltcp 在后续 Interface::poll() 中推进。Pollable::register() 同时注册 smoltcp send/recv waker 和设备 readiness waker。
TCP Listen / Accept
TcpSocket::listen(backlog)
-> register endpoint in LISTEN_TABLE
-> state = Listening
incoming SYN
-> Router::poll()
-> snoop_tcp_packet()
-> LISTEN_TABLE.incoming_tcp_packet()
-> create child smoltcp TCP socket
-> enqueue AcceptedTcp
-> smoltcp consumes SYN and advances child state
TcpSocket::accept()
-> LISTEN_TABLE.accept()
-> return first acceptable child
-> construct connected TcpSocket
accept readiness 由 ListenTableEntryInner.accept_poll 维护。pending child 的 recv/send readiness 会唤醒 listener 的 accept waiters。
TCP/UDP Send-Recv
通用阻塞逻辑来自 GeneralOptions:
pub fn send_poller_with<P: Pollable, F: FnMut() -> AxResult<T>, T>(
&self,
pollable: &P,
extra_nonblocking: bool,
f: F,
) -> AxResult<T> {
block_on(timeout(
self.send_timeout(),
poll_io(pollable, IoEvents::OUT, self.nonblocking() || extra_nonblocking, f),
))?
}
poll_io() 流程:
- 先执行一次操作闭包。
- 成功则返回。
WouldBlock且 nonblocking/MSG_DONTWAIT则立即返回。- 否则注册 waker 并挂起。
- 被 socket readiness、设备 readiness 或 timeout 唤醒后重试。
UDP connected socket 在 recv 时过滤 peer;MSG_MORE 会把多次 send 合并为一个 datagram,并固定第一次 send 的 remote/source。
Raw Socket
raw socket 处理 IP 层 packet:
- send 时按 remote 选择 source,或使用显式绑定地址。
- loopback ICMP 走本地快速路径。
- connected raw socket 使用 peer filter。
deferred_rx保存被 peer filter 暂存的 wire packet,保证MSG_PEEK和后续 recv 不破坏 packet 格式。
控制协议流程
控制协议不是独立线程,它们挂在 Service::poll() 中运行。
DHCP Client
DHCP 状态机:
Discovering --Offer--> Requesting --ACK--> Bound
^ | | |
| | +--NAK/reset------+
+--retry---+--timeout/retry-------------+
入站 packet 路径:
Router::poll()
-> snoop(interface_id, packet)
-> DhcpState::process_packet(interface_id, packet, timestamp)
-> DhcpEvent::Configured / Deconfigured
-> Service::handle_dhcp_event()
-> commit_network_state()
提交内容:
- smoltcp
InterfaceIP address list。 NetControl.state.interfaces的 IPv4/gateway。- DNS registry。
- route table 中该接口的 IPv4 rules。