锁与并发
ax-net 的并发模型是:协议核心串 行推进,设备收发、控制面查询和用户 socket 调用通过短锁、队列、原子状态与 waker 解耦。smoltcp 的 Interface 和 SocketSet 不是多线程并发对象,因此 ax-net 保持单协议核心,由专用 net-poll worker 独占执行完整 poll;应用线程和设备 worker 不直接成为协议栈驱动者。
本文按锁所在层级说明每个同步对象负责的状态、实际源码位置、常见获取路径和不应跨越的边界。代码片段只保留与锁边界相关的部分,完整实现以链接源码为准。
总体并发模型
图中的箭头表示常见访问方向,不表示所有对象都在同一个调用栈中嵌套。关键边界是:
- 应用线程可以修改 socket 状态并
request_poll(),但不直接执行完整Service::poll()。 - 设备 worker 只在设备和 Router queue 之间搬运 packet,不反向进入
SERVICE或SOCKET_SET。 - IRQ 路径只做 driver 短操作和 wake,不进入 smoltcp、Router 或 socket set。
- 控制面查询返回快照,不持锁暴露内部对象引用。
协议核心锁
协议核心由 lib.rs 中的全局单例组织:
// 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 POLLING_INTERFACES: AtomicBool = AtomicBool::new(false);
static POLL_AGAIN: AtomicBool = AtomicBool::new(false);
static NET_POLL_REQUESTED: AtomicBool = AtomicBool::new(false);
static NET_POLL_WAKE: WaitQueue = WaitQueue::new();
SERVICE
SERVICE: Mutex<Service> 是协议核心最外层锁,保护 Service 内的 smoltcp Interface、Router、DHCP client/server 和 orphan reaper 状态。完整 poll 只通过 poll_until_idle() 进入:
// lib.rs:439-440
while POLL_AGAIN.swap(false, Ordering::AcqRel) {
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。
SOCKET_SET.inner
SOCKET_SET.inner 定义在 wrapper.rs,保护全局 smoltcp SocketSet:
// wrapper.rs:44-50
pub(crate) struct SocketSetWrapper<'a> {
pub inner: Mutex<SocketSet<'a>>,
udp_binds: Mutex<HashMap<UdpBindKey, SocketHandle>>,
}
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 锁。
poll 原子量与 worker 唤醒
poll_until_idle() 使用 POLLING_INTERFACES 防重入,用 POLL_AGAIN 合并 poll 过程中新到的请求:
// lib.rs:415-429
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);
这些原子量不是数据结构锁。它们只表达“是否已有线程在 poll”“poll 过程中是否又有新事件”,从而让 socket path 和 device worker 只需轻量 request_poll()。
控制面与路由锁
控制面状态定义在 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,
}
NetControl.state
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。
SharedRouteTable
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 路由规则。
Socket 层锁
Socket 层锁分为三类:全局 smoltcp socket set、协议 public side table、单 socket 局部状态。它们不是 SERVICE 的重复,而是为了让不同语义有不同粒度。
TCP public state、端口表与 listen bucket
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, HashSet<Option<smoltcp::wire::IpAddress>>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult {
let mut bound_ports = TCP_BOUND_PORTS.lock();
let bound_addrs = bound_ports.entry(endpoint.port).or_default();
if bound_addrs
.iter()
.any(|&addr| listen_addrs_conflict(addr, endpoint.addr))
{
return Err(AxError::AddrInUse);
}
bound_addrs.insert(endpoint.addr);
Ok(())
}
它只记录 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)?
};