3. TUN/TAP 设备驱动架构:字符设备与网络设备的结合

TUN/TAP 驱动在内核中扮演着独特的“桥梁”角色:它同时注册为字符设备和虚拟网络设备,使得用户态程序可以通过文件 I/O 操作网络数据包。本章深入分析 drivers/net/tun.c 的架构设计,揭示这种双重身份的实现原理。

3.1 双重设备模型:字符设备 + 网络设备

TUN/TAP 驱动并非简单的网络设备,它通过两个独立的子系统接口与内核交互:

设备类型 注册接口 主要作用 用户态交互方式
字符设备 (/dev/net/tun) misc_register() 提供 open/ioctl/read/write 接口,供用户态程序控制 open() → ioctl() → read()/write()
网络设备 (虚拟网卡) register_netdevice() 挂载到内核网络协议栈,收发数据包 通过协议栈自动调用 ndo_start_xmit
核心设计思想: 字符设备负责“控制面”和“数据面”的用户态入口,网络设备负责“数据面”的内核协议栈入口。两者通过驱动内部的 struct tun_file 和 struct tun_struct 共享数据。
TUN/TAP 双重设备驱动架构 用户态空间 (Userspace) 应用程序 (OpenVPN / QEMU / 自定义程序) 系统调用层 (read / write / ioctl) 字符设备侧 (misc device) tun_chr_open / tun_chr_ioctl tun_chr_read_iter / tun_chr_write_iter 文件操作表: struct file_operations 网络设备侧 (net device) tun_net_open / tun_net_close tun_net_xmit (ndo_start_xmit) 网络操作表: struct net_device_ops 内部数据桥接层 struct tun_struct (全局状态) + struct tun_file (每打开实例) skb 队列: skb_array / 数据包转发逻辑 内核网络协议栈 (TCP/IP Stack)

3.2 tun.c 源码结构总览

drivers/net/tun.c 是 Linux 内核中 TUN/TAP 驱动的核心实现,约 3000+ 行代码。其整体结构可分为以下几个逻辑模块:

  1. 数据结构定义:struct tun_struct、struct tun_file、struct tun_flow_entry 等核心结构体。
  2. 字符设备操作:实现 file_operations 中的 open、release、ioctl、read/write 回调。
  3. 网络设备操作:实现 net_device_ops 中的 open、stop、start_xmit、get_stats 等回调。
  4. 数据包收发路径:用户态 ↔ 内核态的数据包拷贝与 skb 管理。
  5. 多队列与多线程支持:tun_flow 流控、skb_array 无锁队列。
  6. ioctl 控制命令:TUNSETIFF、TUNSETPERSIST、TUNSETQUEUE 等配置接口。
阅读建议: 初学者应重点关注 tun_net_xmit()(发包入口)和 tun_chr_read_iter()(收包入口)这两个函数,它们构成了数据流的核心闭环。

3.3 核心数据结构:tun_struct 与 tun_file

驱动通过两个主要结构体管理状态:

结构体 作用域 关键字段 说明
struct tun_struct 全局(每个虚拟网卡一个) dev, tfiles, flags, numqueues 代表一个 tun/tap 虚拟网卡设备,管理所有打开的文件描述符
struct tun_file 每打开实例(每个 fd 一个) tun, socket, skb_array, flags 代表用户态程序的一次 open() 实例,包含数据接收队列
/* tun_struct: 代表一个虚拟网卡设备 */
struct tun_struct {
    struct net_device       *dev;           /* 关联的 net_device */
    struct list_head        tfiles;         /* 所有打开该设备的 tun_file 列表 */
    unsigned int            flags;          /* TUN/TAP 模式标志 */
    int                     numqueues;      /* 多队列数量 */
    struct tun_flow_entry   *flows;         /* 流控表(可选) */
    /* ... 其他字段 */
};

/* tun_file: 代表用户态的一个打开实例 */
struct tun_file {
    struct sock             sk;             /* 嵌入的 socket 结构 */
    struct tun_struct       *tun;           /* 指向所属的 tun_struct */
    struct skb_array        tx_array;       /* 发送 skb 队列(多队列场景) */
    struct socket           socket;         /* 虚拟 socket */
    u16                     flags;          /* 文件实例标志 */
    /* ... 其他字段 */
};

3.4 字符设备操作实现

驱动通过 misc_register() 注册为 /dev/net/tun 字符设备,主设备号 10,次设备号 200。其 file_operations 定义如下:

static const struct file_operations tun_fops = {
    .owner      = THIS_MODULE,
    .open       = tun_chr_open,
    .release    = tun_chr_close,
    .read_iter  = tun_chr_read_iter,
    .write_iter = tun_chr_write_iter,
    .poll       = tun_chr_poll,
    .unlocked_ioctl = tun_chr_ioctl,
    .compat_ioctl   = tun_chr_compat_ioctl,
};

关键操作流程:

  • open:分配 struct tun_file,初始化 skb 队列,关联到当前命名空间。
  • ioctl(TUNSETIFF):创建或关联一个虚拟网卡,指定设备名和模式(TUN/TAP)。
  • read_iter:从 tun_file 的 skb 队列中取出数据包,拷贝到用户缓冲区。
  • write_iter:从用户缓冲区拷贝数据构造 skb,调用 netif_rx() 注入内核协议栈。

3.5 网络设备操作实现

虚拟网卡通过 register_netdevice() 注册到内核,其 net_device_ops 定义如下:

static const struct net_device_ops tun_netdev_ops = {
    .ndo_open               = tun_net_open,
    .ndo_stop               = tun_net_close,
    .ndo_start_xmit         = tun_net_xmit,
    .ndo_get_stats64        = tun_net_get_stats64,
    .ndo_change_mtu         = tun_net_change_mtu,
    .ndo_set_rx_mode        = tun_net_set_rx_mode,
};

关键操作流程:

  • tun_net_open:启用设备,设置 IFF_UP 标志。
  • tun_net_xmit:协议栈调用此函数发送数据包。驱动将 skb 放入 tun_file 的接收队列,唤醒等待的用户态进程。
  • tun_net_close:停用设备,清理队列。
注意: tun_net_xmit() 运行在软中断上下文(NET_TX_SOFTIRQ),不能睡眠。因此 skb 入队操作必须使用无锁或自旋锁保护,不能调用可能引起阻塞的函数。

3.6 数据包流:用户态 ↔ 内核态

理解数据包在两个方向上的流动路径是掌握 TUN/TAP 驱动的关键:

TUN/TAP 数据包双向流动路径 用户态应用程序 (OpenVPN / QEMU / 自定义程序) write() read() 内核态 TUN 驱动 tun_chr_write_iter / tun_chr_read_iter netif_rx() tun_net_xmit() 内核网络协议栈 TCP/IP 协议处理 / 路由查找 / netfilter 下行(红色):用户态写入 → 协议栈接收 相当于“虚拟网卡从外部收到包” 上行(绿色):协议栈发送 → 用户态读取 相当于“虚拟网卡向外部发包”

3.7 关键 ioctl 命令解析

用户态程序通过 ioctl() 控制 TUN/TAP 设备,以下是最常用的命令:

命令 参数 功能
TUNSETIFF struct ifreq * 创建或关联虚拟网卡,设置名称和模式(IFF_TUN / IFF_TAP)
TUNSETPERSIST int 设置设备持久化,关闭 fd 后设备不自动销毁
TUNSETOWNER int 设置设备所有者(uid),非 root 用户可操作
TUNSETGROUP int 设置设备所属组(gid)
TUNSETQUEUE struct ifreq * 启用/禁用多队列模式下的某个队列
TUNGETFEATURES unsigned int * 查询驱动支持的特性(如多队列、vnet header 等)
典型初始化流程:
1. fd = open("/dev/net/tun", O_RDWR) 打开字符设备
2. ioctl(fd, TUNSETIFF, &ifr) 创建名为 "tun0" 的 TUN 设备
3. ioctl(fd, TUNSETPERSIST, 1) 使设备持久化
4. ioctl(fd, TUNSETOWNER, uid) 设置非 root 用户可访问
5. 使用 read()/write() 或 select()/poll() 收发数据包

3.8 多队列与性能优化

现代 TUN 驱动支持多队列(multi-queue)模式,通过 TUNSETIFF 时设置 IFF_MULTI_QUEUE 标志启用:

  • 每个队列对应一个 tun_file:用户态可以多次 open() 设备,每个 fd 绑定一个独立的 skb 队列。
  • 流控(tun_flow):根据数据包的五元组哈希值,将同一流的数据包分发到固定队列,避免乱序。
  • skb_array 无锁队列:使用 ptr_ring 或 skb_array 实现生产者-消费者模型,减少锁竞争。
/* 多队列模式下,tun_net_xmit 选择队列的逻辑 */
static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct tun_struct *tun = netdev_priv(dev);
    int numqueues = READ_ONCE(tun->numqueues);

    if (numqueues > 1) {
        /* 根据 skb 哈希选择目标队列 */
        u32 rxhash = skb_get_hash(skb);
        int queue = rxhash % numqueues;
        struct tun_file *tfile = rcu_dereference(tun->tfiles[queue]);
        /* 将 skb 放入对应 tfile 的队列 */
    } else {
        /* 单队列模式:使用第一个 tfile */
        struct tun_file *tfile = rcu_dereference(tun->tfiles[0]);
    }
    /* ... */
}

3.9 模块初始化与退出

驱动的生命周期管理通过标准的模块机制实现:

static int __init tun_init(void)
{
    int ret;

    /* 注册 misc 字符设备 /dev/net/tun */
    ret = misc_register(&tun_miscdev);
    if (ret) {
        pr_err("tun: Can't register misc device\n");
        return ret;
    }

    /* 注册 rtnetlink 链路类型,支持 ip link 创建 */
    ret = rtnl_link_register(&tun_link_ops);
    if (ret) {
        misc_deregister(&tun_miscdev);
        return ret;
    }

    pr_info("tun: Universal TUN/TAP device driver initialized\n");
    return 0;
}

static void __exit tun_cleanup(void)
{
    rtnl_link_unregister(&tun_link_ops);
    misc_deregister(&tun_miscdev);
}

module_init(tun_init);
module_exit(tun_cleanup);
知识点: 除了通过 /dev/net/tun 字符设备创建外,TUN/TAP 还支持通过 ip tuntap add 命令(基于 rtnetlink)创建。两种方式最终都调用 tun_newlink() 或 tun_set_iff() 完成设备初始化。

3.10 本章小结

TUN/TAP 驱动的架构精髓在于“一个驱动,两个面孔”:

  • 作为字符设备,它向用户态暴露标准的文件操作接口,使得网络数据包可以像读写文件一样简单。
  • 作为网络设备,它无缝接入内核协议栈,让虚拟网卡的行为与物理网卡一致。
  • 驱动内部的 tun_struct 和 tun_file 结构体是连接两个世界的桥梁,通过 skb 队列实现数据的高效传递。