tun/tap 驱动源码分析

tun/tap驱动程序是Linux平台虚拟网卡驱动程序,驱动加载后会建立网络接口tun0,与其他网卡驱动不同的是,tun驱动并不会把到达的数据包发送出去,而是会暂时存储于队列之中,用户态进程可以通过read,write读写网络数据包,实质上此驱动是一种把网络数据包直接定向至用户态进程的一种方式。用户态进程充当网络的角色,通过read接受网卡数据包,write发送数据包给网卡。

此驱动运行时可设置tun模式和tap模式,tun模式能取到IP数据包,无法获得ARP数据,而tap模式取到的是以太包,可以得到链路层以上的一切数据包。

由于项目需要使用tun驱动,而又不想不求甚解,从而阅读了驱动代码,想对此做一些记录,可以使自己理解的更为透彻,虽然代码并不多,但是涉及内核态编程,倘若追根溯源,恐怕需读完Linux协议栈代码了。

我阅读的代码取自Linux 2.6.34内核源码,路径 drivers\net\tun.c

驱动入口点,Linux驱动入口点不同Windows平台,入口函数都是DriverEntry,Linux平台需要通过module_init宏指定入口点(当然最终宏也会统一把函数转化成init_module作为入口点),当执行modprobe加载内核模块时由内核调用,module_exit宏指定清理函数,当执行rmmod时由内核调用。另外,通过 MODULE_* 一系列宏记录作者,驱动描述,以及协议信息。

module_init(tun_init);
module_exit(tun_cleanup);
MODULE_DESCRIPTION(DRV_DESCRIPTION);
MODULE_AUTHOR(DRV_COPYRIGHT);
MODULE_LICENSE("GPL");
MODULE_ALIAS_MISCDEV(TUN_MINOR);

这里指明了驱动程序的基本信息,MODULE_DESCRIPTION声明驱动描述,MODULE_AUTHOR声明作者,在程序开始已有定义。

#define DRV_DESCRIPTION	"Universal TUN/TAP device driver"
#define DRV_COPYRIGHT	"(C) 1999-2004 Max Krasnyansky "

TUN_MINOR 位于文件 include/linux/miscdevice.h

#define TUN_MINOR 200

再来看初始化和清理函数的实现,初始化函数所作工作非常简单,主要功能是建立一个设备节点供用户态进程控制 ret = misc_register(&tun_miscdev); 函数misc_register注册一个字符设备,所注册信息位于tun_miscdev结构。

static struct miscdevice tun_miscdev = {
	.minor = TUN_MINOR,
	.name = "tun",
	.nodename = "net/tun",
	.fops = &tun_fops,
};

调用过后会在/dev下以 nodename 为名创建节点,即/dev/net/tun,用户进程可通过open函数打开并操作驱动,操作驱动函数由 tun_fops 指定。

static const struct file_operations tun_fops = {
	.owner	= THIS_MODULE,
	.llseek = no_llseek,
	.read  = do_sync_read,
	.aio_read  = tun_chr_aio_read,
	.write = do_sync_write,
	.aio_write = tun_chr_aio_write,
	.poll	= tun_chr_poll,
	.unlocked_ioctl	= tun_chr_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = tun_chr_compat_ioctl,
#endif
	.open	= tun_chr_open,
	.release = tun_chr_close,
	.fasync = tun_chr_fasync
};

这里操作都是字符设备操作,用户open设备节点/dev/net/tun时内核调用tun_chr_open回调,这里函数都与用户态操作相对应,用户对设备调用read,write时最终会回调至此,这也是字符设备驱动编程规范。用户态核心操作都在这几个函数当中了。

现逐条分析每个函数:

  • .llseek = no_llseek : no_llseek为内核函数,实现也十分简单,直接返回-ESPIPE,就是说用户态对设备文件调用lseek就会出Illegal seek错。 字符设备,块设备

  • .read = do_sync_read : do_sync_read 也是内核函数,直接调用aio_read异步读写函数

  • .aio_read = tun_chr_aio_read : tun_chr_aio_read -> tun_do_read -> skb_dequeue 以非阻塞方式从接收队列 tun->socket.sk->sk_receive_queue 取出一个网络包返给用户态。

  • .write = do_sync_write : 同aio_read,调用aio_write

  • .aio_write = tun_chr_aio_write : tun_chr_aio_write -> tun_get_user -> netif_rx_ni ,netif_rx_ni函数为内核函数,最终调用netif_rx返包给TCP/IP协议栈,Linux系统网络数据包都是以sk_buff结构存在,这里函数大部分都是构造此结构。

  • .poll = tun_chr_poll : tun_chr_poll 调用 poll_wait 实现poll功能。

  • .unlocked_ioctl = tun_chr_ioctl : tun_chr_ioctl -> __tun_chr_ioctl 就是垃圾桶函数ioctl的实现了,所有对驱动程序的操控基本都实现于此函数。函数处理各种不同命令,使用switch-case处理不同命令号,设置硬件地址获取信息等等,关键部分在命令TUNSETIFF,处理在switch之前,这个命令设置基本信息并启动驱动程序的网卡部分。TUNSETIFF 命令最终实现于 tun_set_iff 函数。

  • .open = tun_chr_open : tun_chr_open 当用户调用,此函数仅仅分配自定义结构tun_file,存至文件节点私有数据。

  • .release = tun_chr_close : tun_chr_close 与 open 操作相反,释放结构体,以及结构体之中的子结构。

  • .fasync = tun_chr_fasync

以上是tun驱动中字符驱动部分,其中省略了细节,如等待队列等内容,跟内核机制有关,我想再另一篇文章中单独总结更好。

网卡驱动部分

初始化工作在函数 tun_set_iffalloc_netdev 分配网络设备 -> tun_net_init 初始化网络设备 -> register_netdevice 注册网络设备 其中自定义函数 tun_net_init 关键部分如下:

switch (tun->flags & TUN_TYPE_MASK) {
	case TUN_TUN_DEV:
		dev->netdev_ops = &tun_netdev_ops;

		/* ... */
	case TUN_TAP_DEV:
		dev->netdev_ops = &tap_netdev_ops;
		/* ... */
}

程序查看设置模式,若是tun模式,设置回调函数为 tun_netdev_ops, 若是tap模式,设置回调函数为 tap_netdev_ops

先来分析 tun_netdev_ops

static const struct net_device_ops tun_netdev_ops = {
	.ndo_uninit		= tun_net_uninit,
	.ndo_open		= tun_net_open,
	.ndo_stop		= tun_net_close,
	.ndo_start_xmit		= tun_net_xmit,
	.ndo_change_mtu		= tun_net_change_mtu,
};

同字符设备驱动的范式,网络驱动也是设置一系列回调函数,当有数据传输时调用相应回调。

.ndo_open = tun_net_open open 函数调用 netif_start_queue(dev); 通知上层开始接受数据包

.ndo_stop = tun_net_close close 函数调用 netif_stop_queue(dev); 通知上层停止接受数据包

.ndo_start_xmit = tun_net_xmit 当有数据包到达时调用 tun_net_xmit 函数,通知网卡发送数据包,函数处理数据包时调用 skb_queue_tail 把数据包压入接收队列 tun->socket.sk->sk_receive_queue

.ndo_change_mtu = tun_net_change_mtu 改变网卡mtu,控制数据帧大小。

分析 tap 模式下操作回调 tap_netdev_ops

static const struct net_device_ops tap_netdev_ops = {
	.ndo_uninit		= tun_net_uninit,
	.ndo_open		= tun_net_open,
	.ndo_stop		= tun_net_close,
	.ndo_start_xmit		= tun_net_xmit,
	.ndo_change_mtu		= tun_net_change_mtu,
	.ndo_set_multicast_list	= tun_net_mclist,
	.ndo_set_mac_address	= eth_mac_addr,
	.ndo_validate_addr	= eth_validate_addr,
};

其中大部分函数都同 tun 模式相同,因为这些函数都无需关心数据包是否含有以太头。而tun_net_mclist仅实现为空函数,eth_mac_addreth_validate_addr操作函数直接回调系统默认函数,此模式下并无新回调函数出现。

虽然Linux系统是用纯C语言写的,但是其中到处充斥着面向对象的思想,分析驱动程序首先理清结构,以及结构对应的方法,对字符设备驱动对象,file存储数据,file_operations回调是其方法;网络设备对象也是如此。

Built with Hugo
主题 StackJimmy 设计