惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
P
Privacy International News Feed
Security Latest
Security Latest
H
Hacker News: Front Page
T
Tenable Blog
The Hacker News
The Hacker News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
Project Zero
Project Zero
O
OpenAI News
AI
AI
Spread Privacy
Spread Privacy
C
CERT Recently Published Vulnerability Notes
The Last Watchdog
The Last Watchdog
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Scott Helme
Scott Helme
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
NISL@THU
NISL@THU
A
Arctic Wolf
T
Threat Research - Cisco Blogs
PCI Perspectives
PCI Perspectives
N
News and Events Feed by Topic
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Google Online Security Blog
Google Online Security Blog
罗磊的独立博客
L
LINUX DO - 最新话题
U
Unit 42
S
Security Affairs
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
博客园 - 【当耐特】
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
月光博客
月光博客
Engineering at Meta
Engineering at Meta
腾讯CDC
F
Full Disclosure
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Recorded Future
Recorded Future
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
The Cloudflare Blog

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【Linux 网络子系统深度拆解】网络命名空间:内核级网络隔离的实现
2026-04-24 · via 土法炼钢兴趣小组的算法知识备份

每个容器都有独立的 IP 地址、独立的路由表、独立的 iptables 规则——这不是虚拟机,而是同一个内核里运行的进程。实现这一切的基础设施,是网络命名空间(network namespace)。

ip netns add test 一条命令就能创建一个全新的网络栈,但内核为此做了什么?一个 struct net 有多少字段?路由表、netfilter 钩子、socket 哈希表如何做到 per-namespace 隔离?veth 是怎么把一个包从容器命名空间”传送”到宿主机命名空间的?

本文从内核源码回答这些问题。

网络命名空间架构:struct net 与跨命名空间通信

一、struct net:网络命名空间的内核表示

网络命名空间在内核中对应 struct net,定义在 include/net/net_namespace.h。这个结构体是整个网络栈的”根节点”——路由表、netfilter 钩子、接口列表、socket 哈希表、sysctl 参数,全部挂在它下面。

关键字段分组:

// include/net/net_namespace.h
struct net {
    /* 生命周期管理 */
    refcount_t          passive;        // 销毁引用计数
    struct ns_common    ns;             // ns.count = 活跃引用计数
    struct list_head    list;           // 链入全局 net_namespace_list
    struct llist_node   cleanup_list;   // "死亡队列"——等待销毁

    /* 设备管理 */
    struct list_head    dev_base_head;  // 该命名空间所有网络设备链表
    struct hlist_head   *dev_name_head; // 按名称哈希查找设备
    struct hlist_head   *dev_index_head;// 按 ifindex 哈希查找设备
    struct xarray       dev_by_index;   // XArray 快速索引查找
    u32                 ifindex;        // 下一个待分配的接口编号
    struct net_device   *loopback_dev;  // 每个命名空间都有自己的 lo

    /* 协议栈状态 */
    struct netns_ipv4   ipv4;           // 路由表、sysctl、frag 队列
    struct netns_ipv6   ipv6;           // IPv6 路由与配置
    struct netns_nf     nf;             // netfilter 钩子数组
    struct netns_ct     ct;             // conntrack 哈希表
    struct netns_bpf    bpf;            // BPF 程序绑定

    /* 归属与标识 */
    struct user_namespace *user_ns;     // 所属用户命名空间
    struct idr          netns_ids;      // 对端命名空间 ID 映射
    u64                 net_cookie;     // 唯一标识符(写一次)
    u32                 hash_mix;       // 哈希混淆因子

    /* 策略路由 */
    struct list_head    rules_ops;      // fib_rules 操作链
    spinlock_t          rules_mod_lock; // 路由规则修改锁

    /* ... 更多子系统:XFRM、IPVS、MPLS 等 */
};

几个设计要点:

双重引用计数ns.count 跟踪活跃用户(进程、socket、设备),当它归零时触发销毁流程;passive 跟踪销毁过程中的异步清理任务,当它也归零时才释放内存。这是因为网络子系统的销毁涉及大量异步回调,不能在 ns.count 归零时立即 kfree()

per-netns loopback。每个命名空间创建时都会自动生成一个 lo 设备。loopback_dev 字段直接指向它,协议栈中大量路径需要快速访问 loopback——比如发给本机的包需要重定向到 lo。

hash_mix 缓存行对齐hash_mix 字段被标记为 ____cacheline_aligned_in_smp,因为它在每次路由查找、socket 查找中都会被读取,对齐到缓存行避免 false sharing。

二、possible_net_t 与 RCU 访问模式

内核中大量对象需要知道自己”属于哪个命名空间”:net_device 需要知道、sock 需要知道、sk_buff 的处理路径需要知道。问题是:如果内核编译时关闭了 CONFIG_NET_NS,这些字段就是浪费空间。

解决方案是 possible_net_t——一个条件编译的指针包装:

// include/net/net_namespace.h
typedef struct {
#ifdef CONFIG_NET_NS
    struct net __rcu *net;
#endif
} possible_net_t;

CONFIG_NET_NS 关闭时,possible_net_t 是空结构体,占零字节。对应的访问函数也变成返回全局 init_net 的常量:

static inline struct net *read_pnet(const possible_net_t *pnet)
{
#ifdef CONFIG_NET_NS
    return rcu_dereference_protected(pnet->net, true);
#else
    return &init_net;  // 无命名空间支持时,只有一个全局命名空间
#endif
}

static inline void write_pnet(possible_net_t *pnet, struct net *net)
{
#ifdef CONFIG_NET_NS
    rcu_assign_pointer(pnet->net, net);
#endif
}

read_pnet() 使用 rcu_dereference_protected(),要求调用者持有适当的锁或处于 RCU 读临界区。还有一个 read_pnet_rcu() 变体使用 rcu_dereference(),适用于纯 RCU 读路径。

基于此,内核为网络设备和 socket 提供了便捷访问器:

// include/linux/netdevice.h
static inline struct net *dev_net(const struct net_device *dev)
{
    return read_pnet(&dev->nd_net);
}

// include/net/sock.h
static inline struct net *sock_net(const struct sock *sk)
{
    return read_pnet(&sk->sk_net);
}

收包路径中,dev_net(skb->dev) 一次间接寻址就能拿到当前包所在的命名空间,然后查该命名空间的路由表、netfilter 钩子、socket 哈希表。

三、nsproxy:进程与命名空间的绑定

一个进程不只有网络命名空间,还有 PID、mount、UTS、IPC、cgroup、time 命名空间。内核用 struct nsproxy 把所有命名空间指针聚合在一起:

// include/linux/nsproxy.h
struct nsproxy {
    refcount_t count;
    struct uts_namespace  *uts_ns;
    struct ipc_namespace  *ipc_ns;
    struct mnt_namespace  *mnt_ns;
    struct pid_namespace  *pid_ns_for_children;
    struct net            *net_ns;      // 网络命名空间
    struct time_namespace *time_ns;
    struct time_namespace *time_ns_for_children;
    struct cgroup_namespace *cgroup_ns;
};

每个 task_struct 持有一个 nsproxy 指针。同一个 nsproxy 可以被多个进程共享(通过引用计数),这在 fork 时不指定 CLONE_NEW* 标志的情况下发生。

当进程调用 clone(CLONE_NEWNET, ...)unshare(CLONE_NEWNET) 时:

  1. 内核分配新的 nsproxy(或修改当前的)
  2. 调用 copy_net_ns() 创建新的 struct net
  3. nsproxy->net_ns 指向新创建的命名空间
  4. 进程后续的所有网络操作都在新命名空间中进行

四、命名空间创建:copy_net_ns() 与 pernet_operations

4.1 创建路径

clone(CLONE_NEWNET) 触发命名空间创建时,调用链为:

clone() / unshare()
  → create_new_namespaces()
    → copy_net_ns(flags, user_ns, old_net)
      → net_alloc()            // 分配 struct net
      → setup_net(net, user_ns)
        → 遍历 pernet_list,调用每个 ops->init(net)
      → 链入 net_namespace_list

setup_net() 是核心:它遍历所有通过 register_pernet_subsys() 注册的子系统,依次调用 init() 回调。每个子系统负责在新命名空间中分配自己的状态。

4.2 pernet_operations:子系统注册机制

每个需要 per-namespace 状态的子系统(路由、netfilter、conntrack、socket 等)都通过 pernet_operations 注册初始化和销毁回调:

// include/net/net_namespace.h
struct pernet_operations {
    struct list_head list;
    int  (*init)(struct net *net);          // 命名空间创建时调用
    void (*pre_exit)(struct net *net);      // 销毁前准备
    void (*exit)(struct net *net);          // 销毁时清理
    void (*exit_batch)(struct list_head *net_exit_list); // 批量销毁
    unsigned int *id;                       // 子系统 ID
    size_t size;                            // 自动分配的 per-ns 数据大小
};

以 IPv4 路由为例,简化的注册流程:

// net/ipv4/fib_frontend.c(示意)
static struct pernet_operations fib_net_ops = {
    .init = fib_net_init,    // 创建 per-ns 路由表
    .exit = fib_net_exit,    // 销毁 per-ns 路由表
};

static int __init ip_fib_init(void)
{
    register_pernet_subsys(&fib_net_ops);
    ...
}

当新命名空间创建时,fib_net_init() 被调用,它会:

  • 分配该命名空间的 fib_table_hash
  • 创建默认的 local 表和 main 表
  • 添加默认路由规则

注册顺序即初始化顺序。子系统注册的先后决定了创建新命名空间时 init() 回调的调用顺序,也决定了销毁时 exit() 回调的逆序调用。这意味着依赖关系必须通过注册顺序隐式表达。

4.3 两种注册函数的区别

int register_pernet_subsys(struct pernet_operations *);   // 子系统级
int register_pernet_device(struct pernet_operations *);    // 设备级

register_pernet_subsys() 的回调在 register_pernet_device() 之前执行。这保证了底层子系统(路由表、netfilter)在设备初始化之前就绑定好。销毁顺序相反:先销毁设备,再销毁子系统。

五、Per-Namespace 资源隔离详解

5.1 路由表隔离

每个命名空间有独立的 FIB(Forwarding Information Base):

// include/net/netns/ipv4.h
struct netns_ipv4 {
    struct hlist_head       *fib_table_hash;   // 路由表哈希
    struct fib_table __rcu  *fib_main;         // main 表快速指针
    struct fib_table __rcu  *fib_default;      // default 表快速指针
    struct fib_rules_ops    *rules_ops;        // 策略路由规则引擎
    struct inet_peer_base   *peers;            // per-ns 对端缓存
    struct fqdir            *fqdir;            // per-ns IP 分片队列
    // ... 100+ sysctl 参数
};

路由查找的入口 fib_lookup() 通过 net->ipv4.fib_table_hash 定位到当前命名空间的路由表。这意味着:

  • 容器 A 的默认路由指向 10.0.0.1
  • 容器 B 的默认路由指向 172.16.0.1
  • 宿主机的默认路由指向 192.168.1.1

三者互不干扰,因为它们查的是不同的 fib_table_hash

5.2 Netfilter 钩子隔离

每个命名空间有独立的 netfilter 钩子数组:

// include/net/netns/netfilter.h
struct netns_nf {
    struct nf_hook_entries __rcu *hooks_ipv4[NF_INET_NUMHOOKS];
    struct nf_hook_entries __rcu *hooks_ipv6[NF_INET_NUMHOOKS];
    struct nf_hook_entries __rcu *hooks_arp[NF_ARP_NUMHOOKS];
    struct nf_hook_entries __rcu *hooks_bridge[NF_INET_NUMHOOKS];
    // ...
};

五个钩子点(PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING)每个协议族都是独立数组。在容器内执行 iptables -A INPUT -j DROP 只影响该容器的命名空间,宿主机的 INPUT 链完全不受影响。

nf_hook() 的第一件事就是通过 dev_net(skb->dev) 拿到命名空间,然后查该命名空间的钩子数组:

// 简化的钩子遍历路径
static inline int nf_hook(u_int8_t pf, unsigned int hook,
                          struct net *net, ...)
{
    struct nf_hook_entries *e = rcu_dereference(net->nf.hooks_ipv4[hook]);
    // 遍历 e->hooks[] 中注册的回调
}

5.3 Socket 隔离

socket 通过 sk->sk_net 绑定到命名空间。端口绑定(bind)、连接查找(socket lookup)都在命名空间范围内进行:

// include/net/sock.h
static inline struct net *sock_net(const struct sock *sk)
{
    return read_pnet(&sk->sk_net);
}

容器 A 和容器 B 可以同时监听 80 端口,因为它们的 socket 哈希表是不同的。收包路径中的 __inet_lookup()sock_net(sk) 匹配的前提下才会返回结果。

5.4 网络设备隔离

每个 net_device 通过 nd_net 字段绑定到一个命名空间:

// include/linux/netdevice.h
static inline struct net *dev_net(const struct net_device *dev)
{
    return read_pnet(&dev->nd_net);
}

static inline void dev_net_set(struct net_device *dev, struct net *net)
{
    write_pnet(&dev->nd_net, net);
}

设备可以跨命名空间迁移——ip link set eth0 netns container_ns 的内核实现就是调用 dev_change_net_namespace(),它会:

  1. 从旧命名空间的设备链表中摘除
  2. 调用 dev_net_set() 更新 nd_net
  3. 插入新命名空间的设备链表
  4. 重新注册 sysfs 和 netlink 通知

物理网卡也可以迁移到容器命名空间(SR-IOV VF 常这么做),但 loopback 设备不能迁移——它是命名空间的”内置设备”。

六、init_net:默认命名空间

系统启动时,所有网络资源都在 init_net 这个全局命名空间中:

// include/net/net_namespace.h
extern struct net init_net;

init_net 是所有命名空间链表(net_namespace_list)的起点。所有宿主机进程默认使用 init_net,物理网卡初始注册在 init_net 中。

内核中有大量代码通过 &init_net 直接引用默认命名空间。当 CONFIG_NET_NS 未启用时,所有 read_pnet() 调用返回 &init_net,整个命名空间机制退化为零开销。

全局命名空间链表的遍历:

// 需要持有 net_rwsem 或 rtnl_lock
#define for_each_net(VAR) \
    list_for_each_entry(VAR, &net_namespace_list, list)

// RCU 保护的遍历
#define for_each_net_rcu(VAR) \
    list_for_each_entry_rcu(VAR, &net_namespace_list, list)

ip netns list 命令就是通过 netlink 遍历这个链表实现的。/proc/[pid]/ns/net 符号链接指向进程的网络命名空间文件描述符。

七、veth pair:跨命名空间的数据通道

veth(virtual ethernet)是一对虚拟网卡,一端在容器命名空间,另一端在宿主机命名空间。它是容器网络最常用的跨命名空间通信机制。

7.1 veth 的数据结构

// drivers/net/veth.c
struct veth_priv {
    struct net_device __rcu *peer;   // 对端设备指针
    atomic64_t          dropped;
    struct bpf_prog     *_xdp_prog;  // XDP 程序
    struct veth_rq      *rq;         // 接收队列(XDP 模式)
    unsigned int        requested_headroom;
};

veth 的核心就是 peer 指针——它指向另一端的 net_device。两端设备可以在不同的命名空间中。

7.2 veth_xmit:命名空间切换的魔法

// drivers/net/veth.c(简化)
static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct veth_priv *priv = netdev_priv(dev);
    struct net_device *rcv;

    rcu_read_lock();
    rcv = rcu_dereference(priv->peer);  // 获取对端设备
    if (unlikely(!rcv) || !pskb_may_pull(skb, ETH_HLEN)) {
        kfree_skb(skb);
        goto drop;
    }

    // 关键:skb->dev 从当前设备切换为对端设备
    // 对端设备在另一个命名空间 → 包进入另一个命名空间的协议栈
    if (likely(veth_forward_skb(rcv, skb, ...) == NET_RX_SUCCESS))
        ...

    rcu_read_unlock();
    return NETDEV_TX_OK;
}

veth_forward_skb() 内部调用 netif_rx()dev_forward_skb(),将 skb 注入对端设备的接收队列。由于 rcv->nd_net 指向对端命名空间,后续的协议栈处理(路由查找、netfilter、socket lookup)全部在对端命名空间中进行。

数据流示意:

容器命名空间                    宿主机命名空间
┌──────────────┐               ┌──────────────┐
│ 应用 send()  │               │              │
│     ↓        │               │              │
│ TCP/IP 协议栈│               │              │
│     ↓        │               │              │
│ eth0 (veth)  │──veth_xmit──→│ vethXXX      │
│ nd_net=容器ns│  skb->dev切换 │ nd_net=宿主ns│
└──────────────┘               │     ↓        │
                               │ netif_rx()   │
                               │     ↓        │
                               │ 宿主协议栈   │
                               └──────────────┘

7.3 性能特征

veth 的 veth_xmit() 本质是内存操作——没有 DMA、没有中断、没有 ring buffer。但它仍然需要经过完整的协议栈处理,包括 netfilter 钩子。

性能关键点:

  • 零拷贝veth_xmit() 不复制 skb 数据,只是修改 skb->dev 指针并重新注入接收路径
  • GRO 支持:veth 支持 GRO 聚合,通过 veth_gro_receive() 减少协议栈处理次数
  • XDP 支持:veth 的对端可以挂载 XDP 程序,在 netif_receive_skb() 之前就处理包,这是 Cilium 用 veth + XDP 加速容器网络的基础
  • NAPI 模式:当对端挂载了 XDP 程序时,veth 切换到 NAPI 模式接收,通过 veth_poll() 批量处理,减少锁竞争

八、命名空间的销毁与清理

命名空间的销毁比创建复杂得多——网络子系统中有大量异步资源(conntrack 条目、定时器、延迟工作队列),不能在最后一个引用释放时立即销毁。

8.1 销毁流程

最后一个 put_net(net) 使 ns.count → 0
  → __put_net(net)
    → 将 net 链入 cleanup_list(死亡队列)
    → 调度 net_cleanup_work(工作队列)
      → cleanup_net()
        → 遍历 pernet_list(逆序)
          → ops->pre_exit(net)     // 停止新请求
        → 遍历 pernet_list(逆序)
          → ops->exit(net)         // 释放资源
          → ops->exit_batch(list)  // 批量释放
        → 等待 passive 引用归零
        → net_free(net)            // 释放 struct net 内存

pre_exit 与 exit 分离pre_exit() 先标记”正在关闭”,阻止新的资源分配;exit() 再实际释放资源。这避免了在释放过程中有新的对象创建导致 use-after-free。

exit_batch 优化。当多个命名空间同时销毁时(比如一批容器被删除),exit_batch() 接收整个命名空间列表,可以批量处理,减少锁获取次数。

8.2 引用计数管理

static inline struct net *get_net(struct net *net)
{
    refcount_inc(&net->ns.count);
    return net;
}

static inline void put_net(struct net *net)
{
    if (refcount_dec_and_test(&net->ns.count))
        __put_net(net);  // 触发销毁
}

static inline struct net *maybe_get_net(struct net *net)
{
    // 仅当 refcount > 0 时才成功获取引用
    if (!refcount_inc_not_zero(&net->ns.count))
        return NULL;
    return net;
}

maybe_get_net() 是命名空间正在销毁时的安全获取方式——如果命名空间已经进入销毁流程(refcount = 0),返回 NULL 而不是递增一个已经归零的计数器。

谁持有引用:

  • 每个属于该命名空间的 net_deviceget_net()register_netdevice()
  • 每个属于该命名空间的 sockget_net() 在 socket 创建时
  • 持有该命名空间的进程:通过 nsproxy->net_ns
  • /proc/[pid]/ns/net 文件描述符:bind mount 保持命名空间存活
  • netlink socket:get_net()netlink_kernel_create()

九、命名空间标识与互操作

9.1 netns_ids:跨命名空间引用

内核需要在不同命名空间之间建立引用关系(比如 veth 的两端),使用 struct idr netns_ids 来管理:

// struct net 中的字段
struct idr netns_ids;   // 映射:本地 ID → 对端 struct net *

ip link set veth0 netns <ns> 时,内核在两个命名空间的 netns_ids 中互相注册对方的 ID。netlink 消息中使用这个 ID 来引用其他命名空间,避免传递内核指针到用户态。

9.2 net_cookie:全局唯一标识

u64 net_cookie;  // 在 struct net 中

net_cookie 是命名空间的全局唯一标识符,在命名空间创建时写入一次,永不改变。BPF 程序通过 bpf_get_netns_cookie() 助手函数获取当前命名空间的 cookie,用于策略匹配和统计。

十、可观测性

10.1 查看命名空间信息

# 列出所有命名命名空间
ip netns list

# 查看进程的命名空间
ls -la /proc/$PID/ns/net

# 在指定命名空间中执行命令
ip netns exec test ip addr show

# 查看命名空间中的路由表
ip netns exec test ip route show table all

# 查看命名空间中的 iptables 规则
ip netns exec test iptables -L -v -n

10.2 bpftrace 追踪命名空间操作

追踪命名空间创建与销毁:

# 追踪网络命名空间创建
bpftrace -e '
kprobe:copy_net_ns {
    printf("netns create: pid=%d comm=%s\n", pid, comm);
}
kprobe:cleanup_net {
    printf("netns destroy batch\n");
}'

追踪设备跨命名空间迁移:

# 追踪 dev_change_net_namespace
bpftrace -e '
kprobe:dev_change_net_namespace {
    $dev = (struct net_device *)arg0;
    printf("dev %s moving to new netns, pid=%d comm=%s\n",
           $dev->name, pid, comm);
}'

追踪 veth 跨命名空间转发:

# veth_xmit 追踪
bpftrace -e '
kprobe:veth_xmit {
    $skb = (struct sk_buff *)arg0;
    $dev = (struct net_device *)arg1;
    printf("veth_xmit: dev=%s len=%d\n", $dev->name, $skb->len);
}'

10.3 perf 分析命名空间开销

# 统计命名空间创建耗时
perf trace -e 'syscalls:sys_enter_unshare' -p $PID

# 火焰图分析命名空间创建路径
perf record -g -e cycles -p $PID -- sleep 10
perf script | stackcollapse-perf.pl | flamegraph.pl > netns-create.svg

十一、关键参数与调优

参数 默认值 说明
/proc/sys/net/netns/max 无限制 系统最大命名空间数量(部分内核版本)
/proc/sys/net/core/somaxconn 4096 per-netns socket 监听队列深度
/proc/sys/net/ipv4/ip_forward 0 per-netns IP 转发开关
/proc/sys/net/ipv4/conf/all/rp_filter 0 per-netns 反向路径过滤
/proc/sys/net/netfilter/nf_conntrack_max 262144 per-netns conntrack 表大小

几乎所有 /proc/sys/net/ 下的参数都是 per-namespace 的。这意味着容器内 sysctl -w net.ipv4.tcp_fin_timeout=10 只影响该容器的 TCP 栈,不影响宿主机。

但有一个陷阱:容器默认没有权限修改 sysctl。需要在容器运行时配置 --sysctl 选项,或者授予 CAP_NET_ADMIN 能力。在 Kubernetes 中,可以通过 Pod 的 securityContext.sysctls 配置安全的 sysctl 参数。

十二、容器网络架构中的命名空间

12.1 Docker 默认网络模型

宿主机命名空间 (init_net)
┌─────────────────────────────────────┐
│ eth0 (物理网卡)                     │
│    ↑                                │
│ 路由表 + iptables NAT               │
│    ↑                                │
│ docker0 (bridge)                    │
│    ↑          ↑                     │
│ vethAAA    vethBBB                  │
└────┼──────────┼─────────────────────┘
     │          │
     │veth pair │veth pair
     ↓          ↓
┌────────┐ ┌────────┐
│容器 A  │ │容器 B  │
│eth0    │ │eth0    │
│10.0.0.2│ │10.0.0.3│
└────────┘ └────────┘

每个容器运行在独立的网络命名空间中。docker0 网桥在宿主机命名空间中,连接所有容器的 veth 对端。容器到外网的流量通过 iptables MASQUERADE 做 SNAT。

12.2 Kubernetes CNI 模式

Kubernetes 通过 CNI(Container Network Interface)插件管理命名空间网络:

  • Flannel VXLAN:每个 Pod 一个 netns → veth → cni0 bridge → flannel.1 (VXLAN) → 物理网卡
  • Calico:每个 Pod 一个 netns → veth → 宿主机路由表(无 bridge),BGP 通告路由
  • Cilium:每个 Pod 一个 netns → veth + XDP/TC BPF → 宿主机,eBPF 直接做策略和负载均衡

无论哪种 CNI,第一步都是 ip netns add + ip link add veth 创建隔离环境和跨命名空间通道。

12.3 性能考量

命名空间本身的开销很小——struct net 的分配和子系统初始化在微秒级完成。真正的开销在于跨命名空间通信:

  • veth 路径长度:每次跨命名空间通信都要经过完整的发送路径 + 接收路径,包括两次 netfilter 遍历
  • conntrack 开销:每个命名空间独立的 conntrack 表意味着宿主机和容器的 conntrack 条目不共享,总内存占用翻倍
  • 路由查找重复:从容器发出的包要查容器的路由表,到达宿主机后还要查宿主机的路由表

Cilium 通过 BPF 直接在 veth 的 TC/XDP 钩子上做转发决策,绕过了宿主机侧的完整协议栈,将跨命名空间的延迟降低了约 30-50%。

十三、参考文献

  • Linux 6.6 内核源码 include/net/net_namespace.hinclude/linux/nsproxy.h
  • Linux 6.6 内核源码 include/net/netns/ipv4.hinclude/net/netns/netfilter.h
  • Linux 6.6 内核源码 drivers/net/veth.c
  • Linux 6.6 内核源码 net/core/net_namespace.c
  • Rami Rosen,《Linux Kernel Networking: Implementation and Theory》
  • man 7 network_namespaces

上一篇Traffic Control 深度拆解:qdisc、class 与 filter

下一篇虚拟网络设备内核实现:veth、bridge 与 macvlan

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-25 · linux / networking

【Linux 网络子系统深度拆解】虚拟网络设备内核实现:veth、bridge 与 macvlan

容器网络不能没有虚拟设备。本文从 Linux 6.6 内核源码拆解四类核心虚拟网络设备的实现:veth pair 的 veth_xmit 零拷贝转发与 XDP native 模式、Linux bridge 的 br_handle_frame 转发路径与 FDB 学习/老化机制、macvlan 五种模式的内核实现差异、tun/tap 的内核态与用户态数据交换路径,以及各类设备的性能特征对比。