锁与并发
ax-net 的并发模型是:协议核心串行推进,设备收发、控制面查询和用户 socket 调用通过短锁、队列、原子状态与 waker 解耦。smoltcp 的 Interface 和 SocketSet 不是多线程并发对象,因此 PollOwnership 保证任一时刻只有一个完整 poll 推进者:通常是 net-poll worker;UDP drop 的同步 egress flush 可以在调用线程取得 required ownership。设备 worker 从不成为协议栈驱动者。
本文按锁所在层级说明每个同步对象负责的状态、实际源码位置、常见获取路径和不应跨越的边界。代码片段只保留与锁边界相关的部分,完整实现以链接源码为准。
1. 总体并发模型
总体并发模型把应用线程、网络轮询 Worker、设备收发 Worker 和 IRQ action 映射到各自允许访问的共享资源。SERVICE、SocketSet、控制面读写锁与设备队列具有固定方向,图中的虚线唤醒路径表示 IRQ 不会进入普通设备锁或协议核心。
图中的箭头表示常见访问方向,不表示所有对象都在同一个调用栈中嵌套。关键边界是:
- 普通应用路径修改 socket 状态并
request_poll();只有 UDP drop 的内部flush_egress()会先获取 required ownership 再执行完整Service::poll()。 - 设备 worker 只在设备和 Router queue 之间搬运 packet,不反向进入
SERVICE或SOCKET_SET。 - IRQ action 独立拥有从 driver 分离出的
EthernetIrqHandler,只确认/汇总事件并 wake,不获取正常收发的 driver lock,也不进入 smoltcp、Router 或 socket set。 - 控制面查询返回快照,不持锁暴露内部对象引用。
这些边界把图中的访问方向转化为可审查规则:协议核心、设备 Worker 和 IRQ 之间只能通过请求或队列交接。下一节从最上层的服务锁开始说明串行协议状态如何被保护。
2. 协议核心锁
协议核心由 lib.rs 中的全局 SERVICE、SOCKET_SET 和轮询原子状态共同组织,任一时刻只有一个所有权持有者可以推进 smoltcp。外层服务锁负责调度状态,内层 SocketSet 锁负责 socket 集合,两者必须保持固定嵌套方向。
// lib.rs:113-123
static LISTEN_TABLE: LazyLock<ListenTable> = LazyLock::new(ListenTable::new);
static SOCKET_SET: LazyLock<SocketSetWrapper> = LazyLock::new(SocketSetWrapper::new);
static SERVICE: Once<Mutex<Service>> = Once::new();
static NET_CONTROL: Once<Arc<NetControl>> = Once::new();
static POLL_OWNER: AtomicU8 = AtomicU8::new(POLL_OWNER_FREE);
static NET_POLL_REQUESTED: AtomicBool = AtomicBool::new(false);
static NET_POLL_WAKE: WaitQueue = WaitQueue::new();
全局单例代码显示服务、SocketSet、监听表和控制面分别拥有独立同步对象,初始化后不会替换所有者。服务锁位于协议推进最外层,下面说明它保护的状态 和进入条件。
2.1 服务锁
SERVICE: Mutex<Service> 是协议核心最外层锁,保护 Service 内的 smoltcp Interface、Router、DHCP client/server 和 orphan reaper 状态。完整 poll 通过取得 PollOwnership 后的 poll_until_idle(ownership) 进入:
while get_service().poll(&mut SOCKET_SET.inner.lock()) {}
这行代码定义了主锁顺序:SERVICE -> SOCKET_SET.inner -> Service::poll()。因此任何已经持有 SOCKET_SET.inner 的路径都不能反向获取 SERVICE。
Service::poll() 的主体在 service.rs。它在同一轮 poll 内处理 Router RX、DHCP event、DHCP server reply、smoltcp poll、DHCP 定时器、orphan reaper 和 Router TX dispatch:
// service.rs:737-785, 摘要
pub fn poll(&mut self, sockets: &mut SocketSet) -> bool {
router_rx_pending = self.router.poll(timestamp, sockets, |interface_id, packet| {
if let Some(event) = state.process_packet(interface_id, packet, timestamp) {
dhcp_events.push(event);
}
});
for event in dhcp_events {
self.handle_dhcp_event(event);
}
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
|| socket_state_changed
|| router_rx_pending
}
SERVICE 是必要的全局串行点,因为 smoltcp Interface、Router 的 smoltcp-facing buffers 和 DHCP 状态必须作为一个协议核心一起推进。它不应包围用户态阻塞 I/O、设备驱动等待或长时间 sleep。
2.2 SocketSet 锁
SOCKET_SET.inner 定义在 wrapper.rs,保护全局 smoltcp SocketSet 以及 handle 查找、插入和移除。调用方应在锁内只完成短状态操作,把用户复制、等待和可能触发回调的工作移到临界区之外。
// wrapper.rs:44-50
pub(crate) struct SocketSetWrapper<'a> {
pub inner: Mutex<SocketSet<'a>>,
udp_binds: Mutex<HashMap<u16, Vec<UdpBoundEntry>>>,
}
socket API 经常只需要 SOCKET_SET.inner,例如 with_socket_mut() 在 wrapper.rs 只短暂进入某个 smoltcp socket:
// wrapper.rs:68-75
pub fn with_socket_mut<T: AnySocket<'a>, R, F>(&self, handle: SocketHandle, f: F) -> R
where
F: FnOnce(&mut T) -> R,
{
let mut set = self.inner.lock();
let socket = set.get_mut(handle);
f(socket)
}
SOCKET_SET.inner 保护的是 smoltcp socket 内部状态,不保护 TCP/UDP public bind side table,也不保护控制面接口 registry。这样做可以避免所有 POSIX 语义都挤进一个全局 socket set 锁。
2.3 轮询原子量
poll_until_idle() 使用 PollOwnership 的单原子状态防止并发推进,并消费 NET_POLL_REQUESTED 合并 poll 过程中新到的请求:
fn poll_until_idle(ownership: PollOwnership) -> bool {
if !ownership.try_acquire() {
return false;
}
// poll Service/SocketSet,解锁后 drain deferred PollSet wakes。
// owner guard Drop 以 Release 释放所有权。
true
}
这些原子量不是数据结构锁。Opportunistic 申请失败即可返回,供常规 worker 使用;Required 等待 owner 释放,供 UDP close 保 证 egress 交付。两者都不能替代 SERVICE/SOCKET_SET 对具体状态的保护。
3. 控制面路由锁
控制面状态定义在 service.rs。NetControl.state 保护接口 registry 和 DNS registry,routes 指向共享路由表:
// service.rs:99-107
struct ControlState {
interfaces: Vec<NetInterface>,
dns: Vec<DnsServerEntry>,
}
pub struct NetControl {
state: RwLock<ControlState>,
pub(crate) routes: SharedRouteTable,
}
路由表共享类型定义在 router.rs,Router 和控制面持有同一份 SharedRouteTable:
// router.rs:417-425
pub(crate) type SharedRouteTable = Arc<RwLock<RouteTable>>;
pub struct Router {
rx_buffer: PacketBuffer,
tx_buffer: PacketBuffer,
queues: Arc<RouterQueues>,
devices: Vec<Arc<DeviceHandle>>,
table: SharedRouteTable,
}
结构代码显示接口/DNS registry 与共享路由表采用分离锁,使普通快照查询不需要进入 SERVICE。控制面状态锁负责第一部分,路由锁在下一小节单独说明。
3.1 控制面状态锁
NetControl.state 是读多写少锁。接口查询、DNS 查询和本地地址绑定推导只持读锁并返回快照,例如 interfaces() 在 service.rs:
// service.rs:126-130
pub fn interfaces(&self) -> Vec<InterfaceInfo> {
let state = self.state.read();
state.interfaces.iter().map(NetInterface::to_info).collect()
}
运行期 DHCP 或静态设备注册会写入接口/DNS 状态。DHCP commit 的关键更新在 service.rs:
// service.rs:254-279, 摘要
let mut state = self.state.write();
if let Some(interface) = state
.interfaces
.iter_mut()
.find(|interface| interface.id == update.interface_id)
{
interface.ipv4 = update.ipv4;
interface.gateway = update.gateway;
}
state.dns.retain(|entry| {
entry.interface_id != update.interface_id || entry.source != update.dns_source
});
self.routes
.write()
.replace_ipv4_rules_for_interface(update.interface_id, routes);
这里写锁范围只覆盖接口和 DNS registry 的更新;路由表用独立 SharedRouteTable 锁。控制面查询路径不进入设备锁,也不需要获取 SERVICE。
3.2 共享路由锁
SharedRouteTable 是 route lookup 和 TX dispatch 的共享边界。socket connect/send 通过控制面查询 route;Router dispatch 在 router.rs 直接读路由表并根据 smoltcp 已选择的源地址决定出接口:
// router.rs:672-695, 摘要
let routes = self.table.read();
let Some(route) = routes.select_route_for_source(&dst_addr, &src_addr) else {
warn!("No route found for source {} destination {}", src_addr, dst_addr);
continue;
};
let dev = &self.devices[route.dev];
if dev.interface_id == InterfaceId::LOOPBACK {
poll_next |= inject_loopback_rx_direct(
&mut self.rx_buffer,
dst_addr,
packet.into_inner(),
sockets,
);
} else {
poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner());
}
因此 SharedRouteTable 是 TX 热路径锁,但只做规则查找,不访问 driver,不访问 socket payload。接口配置或 DHCP 更新通过写锁替换某接口的 IPv4 路由规则。
4. Socket 层锁
Socket 层锁分为三类:全局 smoltcp socket set、协议 public side table、单 socket 局部状态。它们不是 SERVICE 的重复,而是为了让不同语义有不同粒度。
4.1 TCP 公共状态
TCP socket 的 public 状态不完全等同于 smoltcp TCP 状态。TcpSocket 在 tcp.rs 中用 StateLock、endpoint mutex 和原子 option 保存 POSIX 可见状态:
// tcp.rs:82-92
pub struct TcpSocket {
state: StateLock,
handle: SocketHandle,
bound_endpoint: Mutex<IpListenEndpoint>,
peer_endpoint: Mutex<Option<IpEndpoint>>,
bound_registered: AtomicBool,
StateLock 在 state.rs 用 AtomicU8 做 public state CAS gate:
// state.rs:47-65
pub struct StateLock(AtomicU8);
impl StateLock {
pub fn get(&self) -> State {
self.0
.load(Ordering::Acquire)
.try_into()
.expect("invalid state")
}
}
TCP 端口占用表在 tcp.rs:
// tcp.rs:953-970
static TCP_BOUND_PORTS: LazyLock<Mutex<HashMap<u16, Vec<TcpBoundEntry>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
struct TcpBoundEntry {
addr: Option<IpAddress>,
reuse_port: bool,
}
它只记录 public bind ownership,避免每次 ephemeral port 或 bind 冲突检查都扫描整个 SocketSet。listen table 按端口懒创建 bucket,每个 bucket 仍是独立短锁,定义在 listen_table.rs:
// listen_table.rs:108-112
type ListenTableEntry = Arc<Mutex<Vec<ListenTableEntryInner>>>;
pub struct ListenTable {
tcp: Mutex<HashMap<u16, ListenTableEntry>>,
}
SYN snoop 在 Router RX 阶段进入对应 bucket,并在已经持有 SOCKET_SET.inner 的 poll 上下文里创建 child socket,见 listen_table.rs:
// listen_table.rs:274-315, 摘要
let Some(entries) = self.listen_entry(dst.port) else {
return;
};
let mut table = entries.lock();
if let Some(entry) = table
.iter_mut()
.find(|entry| entry.can_accept_endpoint(dst))
{
if entry.syn_queue.len() >= entry.backlog {
return;
}
let handle = sockets.add(socket);
entry.syn_queue.push_back(AcceptedTcp {
handle,
local_endpoint: dst,
remote_endpoint: src,
});
entry.accept_poll.wake();
}
对应的 accept 路径在 tcp.rs,顺序是先进入 SOCKET_SET.inner,再进入 LISTEN_TABLE bucket:
// tcp.rs:522-528
let bound_endpoint = self.bound_endpoint()?;
self.general.recv_poller(self, || {
request_poll();
let accepted = {
let mut sockets = SOCKET_SET.inner.lock();
LISTEN_TABLE.accept(bound_endpoint, &mut sockets)?
};
TCP 代码展示 public state、端口表和监听 bucket 的获取方向,accept queue 不直接暴露 smoltcp child。UDP 使用独立 bind side table 和局部 endpoint 锁,不能复用 TCP 的 stream 生命周期。
4.2 UDP 绑定状态
UDP 的 local/peer endpoint、cork 与 option 状态定义在 udp.rs,而 wildcard 冲突和 reuseport 组由 SocketSetWrapper 的 bind side table 仲裁。局部锁与全局表必须按统一顺序更新,确保 bind 失败或 drop 时不会留下幽灵占用。
// udp.rs:76-88
pub struct UdpSocket {
handle: SocketHandle,
bind_lock: Mutex<()>,
local_addr: Mutex<Option<IpEndpoint>>,
peer_addr: Mutex<Option<(IpEndpoint, IpAddress)>>,
general: GeneralOptions,
tos_keys: Mutex<Vec<EgressIpTosKey>>,
cork: Mutex<Option<CorkState>>,
}
UDP public bind side table 放在 SocketSetWrapper.udp_binds。bind_lock 把 local 检查、smoltcp bind、side-table 登记和回滚串成一个事务;local_addr/peer_addr 使用 sleepable Mutex