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 共享数据。
3.2 tun.c 源码结构总览
drivers/net/tun.c 是 Linux 内核中 TUN/TAP 驱动的核心实现,约 3000+ 行代码。其整体结构可分为以下几个逻辑模块:
- 数据结构定义:
struct tun_struct、struct tun_file、struct tun_flow_entry等核心结构体。 - 字符设备操作:实现
file_operations中的 open、release、ioctl、read/write 回调。 - 网络设备操作:实现
net_device_ops中的 open、stop、start_xmit、get_stats 等回调。 - 数据包收发路径:用户态 ↔ 内核态的数据包拷贝与 skb 管理。
- 多队列与多线程支持:
tun_flow流控、skb_array无锁队列。 - 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 驱动的关键:
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.
2.
3.
4.
5. 使用
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 队列实现数据的高效传递。