3. Linux 内核 TUN/TAP 驱动原理

TUN/TAP 是 Linux 内核提供的一对虚拟网络设备驱动,它们工作在操作系统的网络协议栈与用户空间应用程序之间,是实现 VPN、隧道、虚拟化网络的关键基础设施。理解 TUN/TAP 的工作原理,是掌握 DPDK 与内核协议栈集成方案(如 KNI)的前提。

3.1 TUN 与 TAP 的工作模式

TUN 和 TAP 虽然共享同一套驱动框架,但它们在 OSI 模型中的工作层级完全不同:

特性 TUN 设备 TAP 设备
OSI 层级 第三层(网络层) 第二层(数据链路层)
处理单元 IP 数据包(不含以太网头) 完整的以太网帧(含 MAC 头)
设备类型标志 IFF_TUN IFF_TAP
典型应用 VPN(OpenVPN、WireGuard) 虚拟机网桥(QEMU/KVM)、容器网络
用户空间接口 /dev/net/tun + 文件描述符 /dev/net/tun + 文件描述符
关键区别: TUN 设备只处理 IP 包,不关心 MAC 地址和二层协议;TAP 设备则模拟完整的物理网卡行为,包括 ARP、广播、MAC 学习等。在 DPDK 集成场景中,KNI 更接近 TAP 的工作模式,因为它需要与内核协议栈交换完整的二层帧。

下图展示了 TUN 与 TAP 在协议栈中的位置差异:

TUN 与 TAP 工作层级对比 TUN 设备(三层) 用户空间应用程序 read/write TUN 驱动(/dev/net/tun) IP 包(无 MAC 头) 内核网络协议栈(IP 层) 路由决策 TAP 设备(二层) 用户空间应用程序 read/write TAP 驱动(/dev/net/tun) 以太网帧(含 MAC 头) 内核网络协议栈(链路层) 网桥 / 转发

3.2 数据包收发路径详解

TAP 设备的数据包收发路径涉及用户空间与内核空间的双向交互。以下以 TAP 设备为例,展示一个数据包从物理网卡到达用户空间应用程序的完整路径:

3.2.1 接收路径(上行)

  1. 物理网卡接收:数据帧到达物理网卡,通过 DMA 写入内存环形缓冲区。
  2. 内核协议栈处理:网卡驱动产生硬中断,NAPI 轮询收包,数据帧进入内核协议栈的链路层处理。
  3. 桥接/路由决策:内核根据目的 MAC/IP 地址决定将数据帧转发到 TAP 设备。
  4. TAP 驱动接收:内核调用 TAP 设备的 ndo_start_xmit 回调,将数据帧放入 TAP 设备的接收队列。
  5. 唤醒用户进程:TAP 驱动唤醒在 /dev/net/tun 上阻塞 read() 的用户空间进程。
  6. 用户空间读取:用户进程通过 read() 系统调用从 TAP 文件描述符获取完整的以太网帧。

3.2.2 发送路径(下行)

  1. 用户空间写入:用户进程构造以太网帧,通过 write() 系统调用写入 TAP 文件描述符。
  2. TAP 驱动接收:驱动将用户空间数据拷贝到内核缓冲区,构造 sk_buff 结构。
  3. 注入内核协议栈:驱动调用 netif_rx() 或 netif_receive_skb() 将数据包注入内核协议栈。
  4. 协议栈处理:内核协议栈对数据包进行路由/桥接决策,确定下一跳。
  5. 物理网卡发送:数据包通过物理网卡驱动发送到物理网络中。
性能关键点: 上述路径中涉及两次数据拷贝(用户空间 ↔ 内核空间)和一次完整的协议栈处理。在高速网络场景下(>10Gbps),这种路径的 CPU 开销极高,这也是 DPDK 绕过内核协议栈的根本原因。

下图展示了 TAP 设备的数据包收发完整路径:

TAP 设备数据包收发路径 用户空间 应用程序 read() 应用程序 write() 系统调用 系统调用 内核空间 TAP 驱动接收队列 TAP 驱动构造 sk_buff netif_receive_skb() netif_rx() 内核网络协议栈(链路层 + IP 层 + 传输层) 路由/桥接决策 物理网卡驱动(DMA / 硬中断)

3.3 TAP 与物理网卡的核心区别

虽然 TAP 设备在用户空间看来与物理网卡行为相似(可以配置 IP、MAC、加入网桥),但两者在底层实现上有本质区别:

对比维度 物理网卡 TAP 虚拟设备
硬件依赖 需要 PCIe 插槽、PHY 芯片、MAC 控制器 纯软件实现,无硬件依赖
数据来源 物理网络介质(网线、光纤) 用户空间应用程序写入
中断机制 硬中断 + MSI-X + NAPI 无硬中断,通过唤醒等待队列模拟
数据拷贝 DMA 直接到内核缓冲区 用户空间 ↔ 内核空间两次拷贝
MAC 地址 硬件固化的 MAC(可覆盖) 随机生成,可任意修改
链路状态 由物理连接决定 可通过 ioctl 强制设置 UP/DOWN
性能上限 受硬件规格限制(如 100Gbps) 受 CPU 频率和内存带宽限制
多队列支持 硬件 RSS 多队列 单队列(内核 4.5+ 支持多队列 TAP)
常见误区: 很多开发者认为 TAP 设备“没有性能开销”,实际上 TAP 的每次数据收发都涉及完整的系统调用(上下文切换)和 sk_buff 的分配/释放。在 10Gbps 线速下,纯 TAP 转发路径的 CPU 占用率可达 60-80%,远高于物理网卡的直接转发。

3.4 TAP 设备的关键数据结构

在 Linux 内核源码中,TAP 设备的核心数据结构是 struct tun_struct,它关联了网络设备、套接字缓冲区队列和用户空间文件描述符:

/* drivers/net/tun.c 中的核心结构 */
struct tun_struct {
    struct net_device      *dev;          /* 关联的网络设备 */
    struct sock             sk;           /* 套接字结构,用于流量控制 */
    struct socket           socket;       /* 套接字接口 */
    struct file            *file;         /* 用户空间文件描述符对应的 file 结构 */
    struct list_head        list;         /* TUN/TAP 设备链表 */
    struct tun_file        *tfiles;       /* 多队列文件数组 */
    unsigned int            numqueues;    /* 队列数量 */
    unsigned int            flags;        /* IFF_TUN / IFF_TAP / IFF_NO_PI 等标志 */
    struct xdp_buff         xdp;          /* XDP 支持(内核 5.x+) */
    struct tun_pcpu_stats  *pcpu_stats;   /* 每 CPU 统计计数器 */
};

用户空间通过 struct ifreq 与 TAP 设备交互,设置设备名称、标志和属性:

/* 用户空间创建 TAP 设备的关键代码片段 */
int fd = open("/dev/net/tun", O_RDWR);
struct ifreq ifr = {0};

ifr.ifr_flags = IFF_TAP | IFF_NO_PI;  /* TAP 模式,不带额外包头 */
strncpy(ifr.ifr_name, "tap0", IFNAMSIZ);

int ret = ioctl(fd, TUNSETIFF, &ifr);
/* 成功后,ifr.ifr_name 返回内核分配的实际设备名 */
IFF_NO_PI 标志: 默认情况下,TUN/TAP 驱动会在数据包前附加 4 字节的额外包头(struct tun_pi),包含协议族和标志信息。设置 IFF_NO_PI 后,用户空间直接读写纯网络数据包,减少处理开销。DPDK 的 KNI 方案在内部使用了类似的无包头模式。

3.5 TAP 设备的局限性

在 DPDK 集成场景中,直接使用 TAP 设备存在以下问题:

  • 系统调用开销:每个数据包都需要 read()/write() 系统调用,上下文切换成本高。
  • 单队列瓶颈:传统 TAP 设备只有一个队列,无法利用多核 CPU 并行处理。
  • 无零拷贝支持:数据必须在用户空间和内核空间之间拷贝,无法像 DPDK 那样直接操作物理内存。
  • 协议栈冗余:即使 DPDK 已经完成了数据包处理,数据注入内核时仍要经过完整的协议栈处理。