Socket 系统
ax-net 的 socket 层向上提供统一的 POSIX-like socket facade,向下分别连接 smoltcp IP socket、内核 Unix domain socket transport 和可选 vsock transport。IP 类 socket 共享单个 smoltcp SocketSet,但 TCP 监听、UDP bind 冲突、raw packet 过滤、Unix namespace 和 vsock connection manager 都由 ax-net 在协议核心外补齐。
核心源码:
| 源码 | 职责 |
|---|---|
socket.rs | 统一地址、send/recv 选项、SocketOps trait、Socket 枚举分发 |
general.rs | 通用 socket 选项、超时、nonblocking、SO_BINDTODEVICE、poll helper |
wrapper.rs | 全局 smoltcp SocketSet 包装和 UDP bind side table |
state.rs | TCP 等 socket 的轻量状态门禁 |
listen_table.rs | TCP listen bucket、SYN/accept 队列、accept waker |
tcp.rs | TCP stream socket、端口仲裁、connect/listen/accept、orphan 接入 |
udp.rs | UDP datagram socket、connected peer、MSG_MORE corking、route-aware source selection |
raw.rs | Raw IP socket、ICMP loopback、peer filter、deferred RX |
unix/ | Unix stream/datagram transport、abstract/path namespace |
vsock/ | 可选 AF_VSOCK facade 和 stream transport |
源码表将公共外观、IP backend、本地 transport 和辅助状态分别定位到模块所有者。设计边界据此强调统一 API 只共享调用形状,各 backend 仍独立拥有协议状态、端口仲裁和清理规则。
1. 设计边界
socket 层通过 SocketOps trait 和 Socket 枚举把系统调用语义映射到协议栈内部对象。它将 AF_INET、AF_UNIX、AF_VSOCK 的地址统一为 SocketAddrEx,将 bind/connect/listen/accept/send/recv/shutdown 统一为 trait 方法,并通过 GeneralOptions 维护 O_NONBLOCK、SO_REUSEADDR/SO_REUSEPORT、超时、IP_MTU_DISCOVER 和 SO_BINDTODEVICE 等通用选项。
IP 类 socket(TCP/UDP/raw)持有 smoltcp SocketHandle,注册到全局 SocketSetWrapper。socket 层补齐 smoltcp 不直接提供的 POSIX 语义——TCP accept queue(ListenTable)、UDP wildcard bind 冲突(udp_binds side table)、raw connected-peer 过滤。出接口选择由控制面 NetControl 在 bind/connect 时决策,实际发包由 Router::dispatch() 在 net-poll worker 中完成;socket 操作本身不同步推进 Interface::poll(),只调用 request_poll() 请求 worker 推进。
Unix domain socket 和 vsock 不经过 smoltcp,各自维护独立的 transport、namespace 和连接状态,但共享 SocketOps/Configurable/Pollable 入口,向上层呈现一致的 socket facade。
典型关系如下:
架构图显示 TCP、UDP 与 Raw Socket 共享 SocketSet、控制面和网络轮询,而 Unix 与 vsock 使用各自 transport。公共外观层只负责把调用路由到这些所有者,不应把协议专属状态提升为所有 socket 的共同字段。
2. 公共外观层
公共 facade 定义跨协议族共享的数据形状和操作入口。系统调用层只需要持有 Socket,不需要知道底层是 smoltcp、Unix transport 还是 vsock connection manager。
2.1 地址选项
SocketAddrEx 是 syscall 层与 backend 之间的跨地址族枚举,分别承载 IP endpoint、Unix 路径或抽象地址以及 vsock CID/port。统一类型只收敛调用形状,不放宽地址族校验;每个 backend 仍需在 bind、connect 和 name 查询入口拒绝错误变体。
// socket.rs
pub enum SocketAddrEx {
Ip(SocketAddr),
Unix(UnixSocketAddr),
#[cfg(feature = "vsock")]
Vsock(VsockAddr),
}
send/recv 选项保留 Linux MSG_* 语义:
pub struct SendOptions {
pub to: Option<SocketAddrEx>,
pub flags: SendFlags,
pub cmsg: Vec<CMsgData>,
pub sender_credentials: Option<UnixCredentials>,
}
pub struct RecvOptions<'a> {
pub from: Option<&'a mut SocketAddrEx>,
pub flags: RecvFlags,
pub cmsg: Option<&'a mut Vec<CMsgData>>,
pub truncated: Option<&'a mut bool>,
}
其中 MSG_DONTWAIT 只影响当前调用,不修改 socket 自身的 O_NONBLOCK;MSG_PEEK、MSG_TRUNC、MSG_OOB、MSG_MORE 由具体 transport 按协议语义解释。sender_credentials 由 OS syscall 层在真实发送点填写,供 Unix SO_PASSCRED 使用。
2.2 Socket 能力
SocketOps 定义所有 backend 对系统调用层承诺的生命周期、地址操作、I/O 和 readiness 能力。TCP、UDP、raw、Unix 与 vsock 通过同一 trait 被 Socket 外观层持有,但错误语义和支持的 flags 仍由具体实现负责。
pub trait SocketOps: Configurable {
fn bind(&self, local_addr: SocketAddrEx) -> NetResult;
fn connect(&self, remote_addr: SocketAddrEx) -> NetResult;
fn listen(&self, _backlog: usize) -> NetResult {
Err(NetError::OperationNotSupported)
}
fn is_listening(&self) -> bool { false }
fn accept(&self) -> NetResult<Socket> {
Err(NetError::OperationNotSupported)
}
fn send(&self, src: impl Read + IoBuf, options: SendOptions) -> NetResult<usize>;
fn recv(&self, dst: impl Write + IoBufMut, options: RecvOptions<'_>) -> NetResult<usize>;
fn recv_available(&self) -> NetResult<usize> {
Err(NetError::OperationNotSupported)
}
fn local_addr(&self) -> NetResult<SocketAddrEx>;
fn peer_addr(&self) -> NetResult<SocketAddrEx>;
fn shutdown(&self, how: Shutdown) -> NetResult;
}
默认实现只给出“不支持”的语义,具体 backend 再按协议覆盖。例如 TCP、Unix stream/seqpacket 和 vsock stream 支持 listen/accept,UDP/raw/Unix datagram 不支持。
2.3 后端分发
Socket 枚举是封闭的运行期分发边界,把统一 API 转交给具体 backend,同时保留穷尽匹配带来的可审计性。新增 transport 时需要在该枚举中显式覆盖地址、I/O、poll、option 和清理路径,避免出现仅能创建却无法完整关闭的半实现状态。
pub enum Socket {
Udp(Box<UdpSocket>),
Tcp(Box<TcpSocket>),
Raw(Box<RawSocket>),
Unix(Box<UnixSocket>),
#[cfg(feature = "vsock")]
Vsock(Box<VsockSocket>),
}
枚举变体只表达运行期 transport 选择,每个 backend 仍拥有不同协议核心、side table 和本地状态。下表把地址族与关键所有者对应起来,新增变体时可以据此检查是否补齐了创建、I/O、poll 和清理路径。
| Backend | 地址族/类型 | 协议核心 | 关键状态 |
|---|---|---|---|
TcpSocket | AF_INET / SOCK_STREAM | smoltcp TCP socket | StateLock、TCP_BOUND_PORTS、ListenTable、orphan |
UdpSocket | AF_INET / SOCK_DGRAM | smoltcp UDP socket | UDP bind side table、connected peer、cork |
RawSocket | AF_INET / SOCK_RAW | smoltcp raw socket | local/peer filter、loopback RX、deferred RX |
UnixSocket | AF_UNIX / stream,dgram | in-kernel transport | abstract/path namespace、stream/dgram transport |
VsockSocket | AF_VSOCK / stream | rdif-vsock transport | connection manager、stream ring buffers |
backend 对照表说明“共享”只发生在外观和若干辅助能力层,不意味着所有 transport 进入 smoltcp。下面的共享状态章节专门描述跨 IP backend 或跨全部 socket 复用的选项、handle 与状态门禁。
3. 共享 Socket 状态
共享状态层包含三类内容:通用 socket 选项、smoltcp handle 空间,以及状态转换门禁。它们不表达某个协议的完整语义,只提供所有 backend 复用的基础设施。
3.1 通用选项状态
GeneralOptions 被 TCP、UDP、raw、Unix、vsock transport 复用,用来维护通用 socket option 和阻塞等待入口:
// general.rs
pub(crate) struct GeneralOptions {
nonblock: AtomicBool,
reuse_address: AtomicBool,
reuse_port: AtomicBool,
send_timeout_nanos: AtomicU64,
recv_timeout_nanos: AtomicU64,
bound_if: AtomicU32,
ip_tos: AtomicU8,
ip_mtu_discover: AtomicU8,
recv_tos: AtomicBool,
recv_traffic_class: AtomicBool,
priority: AtomicI32,
socket_type: AtomicI32,
domain: i32,
protocol: i32,
}
构造时固定 (SOCK_*, AF_*, IPPROTO_*),后续 getsockopt() 直接从这里读取:
| socket | SOCK_* | AF_* | protocol |
|---|---|---|---|
| TCP | SOCK_STREAM | AF_INET | IPPROTO_TCP |
| UDP | SOCK_DGRAM | AF_INET | IPPROTO_UDP |
| Raw | SOCK_RAW | AF_INET | 创建时指定的 IpProtocol |
| Unix stream | SOCK_STREAM | AF_UNIX | 0 |
| Unix dgram | SOCK_DGRAM | AF_UNIX | 0 |
| Unix seqpacket | SOCK_SEQPACKET | AF_UNIX | 0 |
| Vsock stream | SOCK_STREAM | AF_VSOCK | 0 |
bound_if 保存的是稳定的 InterfaceId,不是 Router 内部设备索引:
pub fn set_device_binding(&self, binding: DeviceBinding) {
self.bound_if.store(
binding.bound_if.map_or(0, InterfaceId::get),
Ordering::Release,
);
}
pub fn device_binding(&self) -> DeviceBinding {
let raw = self.bound_if.load(Ordering::Acquire);
DeviceBinding {
bound_if: (raw != 0).then_some(InterfaceId::new(raw)),
}
}
QoS 相关选项也集中在这里保存:
ip_tos:setsockopt(IP_TOS)写入时会清掉 ECN 两位;TCP/UDP 通过ip_tos.rs注册 per-socket egress policy,Router dispatch 时改写 IPv4 DSCP/ECN 或 IPv6 traffic class;raw socket 在构造 IP header 后直接改写。recv_tos/recv_traffic_class:UDPrecvmsg()根据rx_meta.rs放在 smoltcpPacketMeta.id中的 ingress metadata 生成IpCmsg::Ipv4Tos或IpCmsg::Ipv6TrafficClass。priority:SO_PRIORITY仅接受 Linux 普通非特权范围0..=6并保存数值;当前不参与设备队列调度。
通用选项列表中只有一部分字段直接影响协议 backend,其余字段用于等待、身份 或 ABI 回显。SocketSetWrapper 则只服务 IP socket 的 smoltcp handle 与 UDP bind 表,不被 Unix 或 vsock transport 使用。
3.2 SocketSet 包装器
TCP、UDP 和 raw socket 都注册到同一个 smoltcp SocketSet,由 SocketSetWrapper 持有:
pub(crate) struct SocketSetWrapper<'a> {
pub inner: Mutex<SocketSet<'a>>,
udp_binds: Mutex<HashMap<u16, Vec<UdpBoundEntry>>>,
}
统一 SocketSet 的意义:
- TCP/UDP/raw 共享同一 handle 空间。
- net-poll worker 可以一次性推进所有 IP socket。
- TCP listen table 和 orphan reaper 可以通过 handle 操作 child socket。
- 不需要为每个接口复制 socket set,wildcard listen 和动态 route 选择保持简单。
SocketSetWrapper 只封装 smoltcp socket 访问,不持有 Service 锁,也不直接唤醒任务:
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)
}
SocketSet 包装代码表明 handle 操作和 UDP bind 表更新都在同一包装边界内完成,避免 drop 后残留占用。状态锁位于单个 socket 外观层,用于阻止并发生命周期转换,而不替代全局 handle 锁。