dpdk之kni实现

一、为什么要用kni

        通常情况下dpdk用于二三层报文转发,接收到来自网卡的报文后,如果是二层报文则查找fdb表; 如果是三层报文,则进行dnat, snat处理后,查找路由表, 将报文转发给下一跳路由。这些二三层转发操作都是直接转发到另一台设备上,不需要经过内核,无需内核协议栈的参与。

        然而有些场景下报文是直接发给运行dpdk程序的这台设备本身的。例如ping运行dpdk程序这台设备;或者访问dpdk程序这台设备上运行的nginx服务器, ftp服务器,smtp邮件服务器等等。 这些操作都是发给运行dpdk程序本机这台设备, 因此报文是一定需要经过内核,由tcp协议栈进行处理。也就是说dpdk收到这些报文后,需要将报文转发给内核。例如ping操作,dpdk收到ping请求后,将报文发给内核协议栈,由内核协议栈处理完ping请求后,发送ping响应。dpdk收到ping响应后再转发给源主机。在如访问本机的nginx服务器, dpdk收到http请求后,转发给内核协议栈,内核协议栈收到http请求后,在将这个请求转发到应用层监听80端口的服务器进程。

dpdk之kni实现_第1张图片

        那dpdk通过什么方式将报文转发给内核呢? 可以通过kni设备,也可以通过tun/tap虚拟网卡来实现。 相对于tun/tap实现, kni减少了内核态与应用层之间内存拷贝的操作,具有更高的转发性能。

二、kni的使用

        kni分为应用层实现与驱动层实现。        

 1、驱动层使用

       当编译好dpdk后,在dpdk安装目录下有一个kmod目录,里面会生成rte_kni.ko驱动。在kmod目录下执行insmod ./rte_kni.ko加载kni驱动。加载kni驱动时可以指定参数,例如指定多线程参数等,如果不指定参数,则默认是单线程模型。需要注意的是,如果dpdk编译完成后,在kmod目录下找不到rte_kni.ko驱动,那换更高的dpdk版本吧。我使用的dpdk1.8版本就没有生成这个驱动,换到最新版本就有了。

        内核加载完这个kni驱动后,此时会在/dev目录下生产一个kni设备文件。

 2、应用层使用

        应用层examples目录下提供了一个kni的例子,编译好这个kni例子就可以执行了。如果编译kni报错,则有可能是kni相关的开关没有打开,也就不会参与编译。此时需要在dpdk安装目录下搜索所有的kni关键词, 将搜索到所有关于kni的开关打开就好了,重新编译整个dpdk。

        编译好kni例子后,就可以执行./kni -c 0xf -- -p 0x1 -P --config="(0,0,1,2)"运行kni程序。 

        执行ifconfig -a就可以看到这个kni设备名,例如:vEth0_0。 后续dpdk与内核的交互,都是通过这个kni设备来进行。

        dpdk之kni实现_第2张图片

        当生成了kni设备后,后续就可以使用应用层工具ifconfig, ethtool, tcpdump对这个kni设备进行操作。例如

 ifcofnig vEth0_0 up			开启kni设备
 ifcofnig vEth0_0 down			关闭kni设备
 ifcofnig vEth0_0 192.168.0.1 netmast 255.255.255.0 promisc 设置ip
 ethtool -i vEth0_0				查看kni设备信息
 tcpcudp -i vEth0_0 -nne -s0 -v 抓到这个kni设备的报文

三、kni实现原理

        要使得dpdk能够利用kni设备将报文发给内核协议栈, kni需要实现应用层功能与驱动层功能。 驱动层需要创建一个/dev/kni混合设备,这个在应用层加载kni驱动的时候自动完成创建。 通过这个/dev/kni混合设备,可以接收应用层的ioctl消息,按需来创建各种kni设备、删除kni设备、打开kni设备、关闭kni设备、设置mtu、接收ethtool工具的命令操作消息等等。需要注意的是,驱动层创建的两种设备,一个是/dev/kni混合设备, 另一个是kni设备,这两个是不同的设备类型。

          应用层则提供给调用者操作kni设备的接口。例如kni的初始化、按照需要为每个网卡分配一个或者多个kni设备、将来自网卡的报文通过kni设备发给内核、接收来自内核的报文后将报文通过网卡发送出去。另外也可以使用linux工具ifconfig、ethtool、tcpdump来操作kni设备。例如给kni设备配置ip地址,抓包等。   

       下面分别从应用层与驱动层,来看下kni设备的具体实现。

四、应用层kni的实现

        对于每一个网卡,都可以创建一个或者多个kni设备。具体每一张网卡可以创建多少个kni设备,由应用层自行指定。创建完kni设备后,ifconfig -a命令执行后就可以看到这些虚拟网卡名,例如veth1_0, veth1_1。如果加载驱动的时候,指定了单线程模型,则kni驱动将只会创建一个线程,用于所有的kni设备接收来自应用层的报文。 如果加载kni驱动的时候,指定了多线程模型,则对于每个kni设备,kni驱动都会创建一个线程去接收来自应用层发来的报文。 kni设备与线程是一一对应的关系。

                             dpdk之kni实现_第3张图片

先以一张图来整体说明下kni设备应用层整体的结构。

                     dpdk之kni实现_第4张图片

        应用层使用一个struct rte_kni_memzone_slot数组来存放所有的kni设备, 每个数组元素对应一个kni设备。 每个kni设备本身,都有一个独占的发送队列、接收队列、分配队列、释放队列、请求队列、响应队列。需要注意的是m_ctx成员指向的struct rte_kni结构本身,内部也有各种队列,但这些都是一个指针,指向刚才提到的些队列,是一种引用关系,而不会为它重复开辟这些队列空间。

        当应用层从网卡收到报文后,将报文放到kni设备的rx接收队队列。kni驱动就会从这个rx接收队列中取出mbuf报文,将mbuf报文转为内核协议栈支持的sk_buff,调用netif_rx内核接口发给内核。将报文发给内核后,会将这些mbuf报文放到free释放队列,由应用层读取释放队列中待释放的mbuf进行释放操作。为什么要由应用层释放呢? 秉承谁开辟空间,那就谁释放的原则。另外也是为了使得驱动层代码最简洁化,驱动只实现最少的功能,将mbuf内存申请与释放的操作交给应用层来操作。驱动与应用层通过malloc, free队列来传递已经分配好或者待释放的报文。

        当kni驱动收到来自内核的报文后,会调用kni_net_tx从malloc分配队列中获取一个应用层已经分配好的mbuf结构。同时将sk_buff报文转为mbuf报文,存放到mbuf中。之后将mbuf报文发到tx发送队列中。应用层从tx发送队列中获取报文后,将报文通过网卡发送出去。应用层也会重新开辟mbuf空间,放到malloc队列中,供后续kni驱动发包给应用层使用。这也可以体现刚才说的内容,驱动只实现最少的功能,将mbuf内存申请与释放的操作交给应用层来操作。驱动与应用层通过malloc, free队列来传递已经分配好或者待释放的报文。

        那请求与发送队列是做什么的?当应用层调用ifconfig, ethtool等工具,设置kni设备的mtu,  使得kni设备up/down的时候。kni驱动收到这些设置操作,会构造一个请求报文,将这个请求报文放到req请求队列。应用层就会从这个req请求队列中获取一个请求,执行这个请求操作。例如设置mtu,  使得网卡up等。应用层会调用pmd用户态驱动实现的接口,来真正的对网卡设置mtu, 使得网卡up等操作。之后应用层会构造一个响应消息,将消息放到resp队列中,驱动从这个resp队列中获取响应消息就知道请求的执行结果了。

dpdk之kni实现_第5张图片

        接下里进入代码分析环节,看下应用层代码的实现。

1、kni初始化

        应用层调用rte_kni_init接口执行kni初始化操作。所谓的kni初始化,其实就是为所有的kni设备分配好空间,构成上图中提到的struct rte_kni_memzone_slot数组。 每个数组元素对应一个kni设备。并为每一个kni设备,分配好发送队列、接收队列、分配队列、释放队列、请求队列、响应队列。另外会将每个kni设备构成一个数组链表, 既能有数组快速遍历功能,也有链表快速插入删除操作的高效。

        另外也会执行打开/dev/kni混合设备的操作,之所以要打开/dev/kni混合设备,是为了后续能通过ioctl操作这个混合设备,进而能创建kni设备。

void rte_kni_init(unsigned int max_kni_ifaces)
{
	//打开/dev/kni设备
	kni_fd = open("/dev/" KNI_DEVICE, O_RDWR);
	//为每个kni设备开辟队列空间
	for (i = 0; i < max_kni_ifaces; i++)
	{
		it = &kni_memzone_pool.slots[i];
		//开辟kni结构
		snprintf(mz_name, RTE_MEMZONE_NAMESIZE, "KNI_INFO_%d", i);
		mz = kni_memzone_reserve(mz_name, sizeof(struct rte_kni), SOCKET_ID_ANY, 0);
		it->m_ctx = mz;
		//开辟发送队列空间
		snprintf(obj_name, OBJNAMSIZ, "kni_tx_%d", i);
		mz = kni_memzone_reserve(obj_name, KNI_FIFO_SIZE, SOCKET_ID_ANY, 0);
		it->m_tx_q = mz;
		//开辟接收队列空间
		snprintf(obj_name, OBJNAMSIZ, "kni_rx_%d", i);
		mz = kni_memzone_reserve(obj_name, KNI_FIFO_SIZE, SOCKET_ID_ANY, 0);
		it->m_rx_q = mz;
		/*开辟分配队列空间 ALLOC RING */
		snprintf(obj_name, OBJNAMSIZ, "kni_alloc_%d", i);
		mz = kni_memzone_reserve(obj_name, KNI_FIFO_SIZE, SOCKET_ID_ANY, 0);
		it->m_alloc_q = mz;
		/* 开辟释放队列空间FREE RING */
		snprintf(obj_name, OBJNAMSIZ, "kni_free_%d", i);
		mz = kni_memzone_reserve(obj_name, KNI_FIFO_SIZE, SOCKET_ID_ANY, 0);LL);
		it->m_free_q = mz;
		/* 开辟请求队列空间 */
		snprintf(obj_name, OBJNAMSIZ, "kni_req_%d", i);
		mz = kni_memzone_reserve(obj_name, KNI_FIFO_SIZE, SOCKET_ID_ANY, 0);
		it->m_req_q = mz;
		/* 开辟响应队列空间 */
		snprintf(obj_name, OBJNAMSIZ, "kni_resp_%d", i);
		mz = kni_memzone_reserve(obj_name, KNI_FIFO_SIZE, SOCKET_ID_ANY, 0);
		it->m_resp_q = mz;
		
		//构成一个数组链表
		it->next = &kni_memzone_pool.slots[i+1];
	}
}

2、应用层kni设备的创建

        调用rte_kni_alloc接口将会创建一个kni设备。具体实现方式就是向/dev/kni混合设备发送ioctl消息,kni驱动收到ioctl消息后后,kni驱动负责创建kni设备。另外也会对发送队列,接收队列、分配队列、释放队列、请求队列、响应队列进行初始化操作。同时将将rte_kni结构中的各种队列与struct rte_kni_memzone_slot中的相应队列关联起来,也就是一种引用关系。

struct rte_kni * rte_kni_alloc(struct rte_mempool *pktmbuf_pool, const struct rte_kni_conf *conf, struct rte_kni_ops *ops)	
{
	//从所有空闲的kni设备曹中获取一个空闲曹
	slot = kni_memzone_pool_alloc();	
	//得到struct rte_kni结构
	ctx = slot->m_ctx->addr;
	//将rte_kni结构中的发送、接收、分片、释放、请求、响应队列与struct rte_kni_memzone_slot中的
	//发送、接收、分片、释放、请求、响应队列关联起来。是一种引用关系
	mz = slot->m_tx_q;
	ctx->tx_q = mz->addr;
	//发送队列初始化
	kni_fifo_init(ctx->tx_q, KNI_FIFO_COUNT_MAX);
	
	//通过/dev/kni设备发送ioctl,用来创建kni设备
	ret = ioctl(kni_fd, RTE_KNI_IOCTL_CREATE, &dev_info);
}

3、应用层发包到内核

        应用层在收到来自网卡的报文后,通过调用rte_kni_tx_burst接口将报文发给内核。具体实现方式就是将报文放到kni设备所在的发送队列,kni驱动就会从这个队列中取出mbuf报文。kni驱动将这个mbuf报文转为内核支持的sk_buff,通过调用netif_rx内核函数发给内核。

        将报文发给内核后,kni驱动会将这些已经发给内核的mbuf报文放到free释放队列,由应用层读取释放队列中待释放的mbuf进行释放操作

//发包给内核
unsigned rte_kni_tx_burst(struct rte_kni *kni, struct rte_mbuf **mbufs, unsigned num)
{
	//将报文写入kni设备所在发送队列,内核从这个队列读数据
	unsigned ret = kni_fifo_put(kni->rx_q, (void **)mbufs, num);
	//释放内核已经接收的报文
	kni_free_mbufs(kni);
	return ret;
}

4、应用层接收内核的报文

         当kni驱动收到来自内核的报文后,会调用kni_net_tx从malloc分配队列中获取一个应用层已经分配好的mbuf结构。同时将sk_buff报文转为mbuf报文,存放到mbuf中。之后kni驱动将mbuf报文放到tx发送队列中。

         应用层从tx发送队列中获取报文后,将报文通过网卡发送出去。应用层也会重新开辟mbuf空间,放到malloc队列中,供后续kni驱动发包给应用层使用

//从内核收包
unsigned rte_kni_rx_burst(struct rte_kni *kni, struct rte_mbuf **mbufs, unsigned num)
{
	//从队列获取来自内核的报文
	unsigned ret = kni_fifo_get(kni->tx_q, (void **)mbufs, num);
	//分配新的空间给驱动使用
	kni_allocate_mbufs(kni);
	return ret;
}

5、应用层对kni设备的配置操作

        当应用层调用ifconfig, ethtool等工具,设置kni设备的mtu,  使得kni设备up/down的时候。kni驱动收到这些设置操作,会构造一个请求报文,将这个请求报文放到req请求队列。应用层就会从这个req请求队列中获取一个请求,执行这个请求操作。例如设置mtu,  使得网卡up等。应用层会调用pmd用户态驱动实现的接口,来真正的对网卡设置mtu, 使得网卡up等操作。之后应用层会构造一个响应消息,将消息放到resp队列中,驱动从这个resp队列中获取响应消息就知道请求的执行结果了。

        驱动层操作的接口:


 //打开kni设备
static int kni_net_open(struct net_device *dev)
{	
	struct rte_kni_request req;
	//构造设置网卡up的请求内容
	req.req_id = RTE_KNI_REQ_CFG_NETWORK_IF;
	req.if_up = 1;
	ret = kni_net_process_request(kni, &req);
}
static int kni_net_process_request(struct kni_dev *kni, struct rte_kni_request *req)
{
	//请求消息放到队列
	num = kni_fifo_put(kni->req_q, &kni->sync_va, 1);
	//等待响应
	ret_val = wait_event_interruptible_timeout(kni->wq, kni_fifo_count(kni->resp_q), 3 * HZ);
	//获取响应
	num = kni_fifo_get(kni->resp_q, (void **)&resp_va, 1);
}

        应用层主动发起操作,kni驱动接收到消息后会构造请求放到请求队列,之后应用层读取队列的请求后调用pmd用户态驱动提供的接口。处理完后应用层构造响应消息,放入到响应队列。kni驱动读取响应队列中的响应消息就知道结果了。是不是感觉兜了一大圈的节奏。

        应用层操作的接口:

int rte_kni_handle_request(struct rte_kni *kni)
{
	//应用层从队列中获取一个请求
	ret = kni_fifo_get(kni->req_q, (void **)&req, 1);
	//根据消息id, 开始处理请求
	switch (req->req_id)
	{
		case RTE_KNI_REQ_CHANGE_MTU: 
			//设置网卡的mtu
			req->result = kni->ops.change_mtu(kni->ops.port_id, req->new_mtu);
			break;
		case RTE_KNI_REQ_CFG_NETWORK_IF: 
			//设置网卡up/down
			req->result = kni->ops.config_network_if(kni->ops.port_id, req->if_up);
			break;
	}
	//处理完请求后,将请求的结果写入队列。kni驱动从这个队列获取结果
	ret = kni_fifo_put(kni->resp_q, (void **)&req, 1);
}

6、应用层内存队列的分配与释放

        驱动只实现最少的功能,将mbuf内存申请与释放的操作交给应用层来操作。驱动与应用层通过malloc, free队列来传递已经分配好或者待释放的报文。具体来说就是应用层开辟好报文空间后,将空间放到分配队列中给kni驱动使用,kni驱动从这个分配队列中获取报文空间。 kni驱动使用完这个报文空间后,会放到释放队列,应用层读取这个释放队列中待释放的报文进行释放操作。

static void kni_allocate_mbufs(struct rte_kni *kni)
{
	//从内存池中mbuf空间
	for (i = 0; i < MAX_MBUF_BURST_NUM; i++) 
	{
		pkts[i] = rte_pktmbuf_alloc(kni->pktmbuf_pool);
	}
	//将mbuf放入队列,给kni驱动使用
	ret = kni_fifo_put(kni->alloc_q, (void **)pkts, i);
}
static void kni_free_mbufs(struct rte_kni *kni)
{
	int i, ret;
	struct rte_mbuf *pkts[MAX_MBUF_BURST_NUM];
	//从队列中获取报文进行释放操作
	ret = kni_fifo_get(kni->free_q, (void **)pkts, MAX_MBUF_BURST_NUM);
	for (i = 0; i < ret; i++)
	{
		rte_pktmbuf_free(pkts[i]);
	}
}

        到此为止,kni设备应用层部分已经分析完了,接下里分析驱动层kni的实现。

五、驱动层kni的实现

         驱动层会创建一个/dev/kni混合设备,这个在应用层加载kni驱动的时候自动完成创建。 通过这个/dev/kni混合设备,可以接收应用层的ioctl消息,按需来创建各种kni设备、删除kni设备、打开kni设备、关闭kni设备、设置mtu、接收ethtool工具的命令操作消息等等。需要注意的是,驱动层创建的两种设备,一个是/dev/kni混合设备, 另一个是kni设备,这两个是不同的设备类型。

dpdk之kni实现_第6张图片

1、kni驱动初始化

        kni驱动初始化的时候,会在/dev目录下创建一个/dev/kni混合设备, 后续由这个混合设备创建kni设备。另外kni驱动初始化的时候,还会初始化线程模型,根据加载驱动的参数来决定是启用单线程还是多线程。以此同时注册接收应用层报文的回调,正常情况下都使用kni_net_rx_normal这个接口来接收来自应用层的报文

static int __init kni_init(void)
{
	//线程模型初始化
	kni_parse_kthread_mode();
	//注册一个/dev/kni设备
	misc_register(&kni_misc);
	//根据loopback模式,注册接收来自应用层报文的接收回调
	kni_net_config_lo_mode(lo_mode);
	return 0;
}

        来看下可以对/dev/kni混合设备执行哪些操作。从中可以看出应用层可以对/dev/kni混合设备执行打开混合设备、关闭混合设备、对混合设备执行ioctl操作。

//dev/kni设备操作接口
static struct file_operations kni_fops = 
{
	.owner = THIS_MODULE,
	.open = kni_open,                   //打开/dev/kni混合设备
	.release = kni_release,             //关闭/dev/kni混合设备
	.unlocked_ioctl = (void *)kni_ioctl,
	.compat_ioctl = (void *)kni_compat_ioctl,
};

//dev/kni混合设备
static struct miscdevice kni_misc =
{
	.minor = MISC_DYNAMIC_MINOR,
	.name = KNI_DEVICE,
	.fops = &kni_fops,					//混合设备操作接口
};

2、驱动中kni设备的创建

        应用层对/dev/kni执行ioctl系统调用时,可以创建或者删除一个kni设备。创建完kni设备后,执行ifconfig -a就可以看到对应的kni设备,例如veth0_0, veth0_1等。

static int kni_ioctl(struct inode *inode, unsigned int ioctl_num, unsigned long ioctl_param)
{
	switch (_IOC_NR(ioctl_num)) 
	{
		case _IOC_NR(RTE_KNI_IOCTL_CREATE):
			//创建kni设备
			ret = kni_ioctl_create(ioctl_num, ioctl_param);
			break;
		case _IOC_NR(RTE_KNI_IOCTL_RELEASE):
			//销毁kni设备
			ret = kni_ioctl_release(ioctl_num, ioctl_param);
			break;
	}
}

        创建kni设备过程比较多。首先将应用层的ioctl设置信息拷贝到内核空间来,根据应用层提供的参数来进行设置。创建好kni设备后,根据应用层传进来的参数,例如将各种队列从应用层空间转换到内核空间来(指向的内存位置是同一个)。另外也会设置ethtool的操作接口,驱动层实现这个接口,使得应用层能够使用ethtool工具对kni设备进行操作。最后,如果驱动被加载时指定了多线程模型,则会为这个kni设备创建一个线程,用于驱动与应用层之间的交互。


static int kni_ioctl_create(unsigned int ioctl_num, unsigned long ioctl_param)
{
	//从应用层拷贝数据到内核
	ret = copy_from_user(&dev_info, (void *)ioctl_param, sizeof(dev_info));
	//创建一个kni设备,内部会调用kni_net_init对net_dev初始化。kni_dev作为net_dev的私有结构
	net_dev = alloc_netdev(sizeof(struct kni_dev), dev_info.name, kni_net_init);	
	kni = netdev_priv(net_dev);
	//转换用户空间的队列,到内核空间
	kni->tx_q = phys_to_virt(dev_info.tx_phys);
	kni->rx_q = phys_to_virt(dev_info.rx_phys);

	//对kni设备,设置针对ethtool工具的操作接口
	kni_set_ethtool_ops(kni->net_dev);
	//将创建的kni设备注册到内核。注册完成后执行ifconfig -a就可以看到kni设备
	ret = register_netdev(net_dev);

	//如果是多线程模型,则创建kni线程,用于处理与应用层的交互
	if (multiple_kthread_on)
	{
		kni->pthread = kthread_create(kni_thread_multiple, (void *)kni,  "kni_%s", kni->name);
	}

	//将kni设备插入到链表
	list_add(&kni->list, &kni_list_head);
}

        来看下应用层可以对kni设备执行什么设置操作。应用层可以调用ifconfig工具,例如ifconfig veth0_0 up打开kni设备,调用

ifconfig veth0_0 down关闭kni设备。


//kni设备的操作接口
static const struct net_device_ops kni_net_netdev_ops = 
{
	.ndo_open = kni_net_open,					//打开kni设备
	.ndo_stop = kni_net_release,				//关闭kni设备
	.ndo_set_config = kni_net_config,
	.ndo_start_xmit = kni_net_tx,				//内核发包给应用层
	.ndo_change_mtu = kni_net_change_mtu,
	.ndo_do_ioctl = kni_net_ioctl,
	.ndo_get_stats = kni_net_stats,
	.ndo_tx_timeout = kni_net_tx_timeout,
	.ndo_set_mac_address = kni_net_set_mac,
};
//初始化kni设备
void kni_net_init(struct net_device *dev)
{
	//注册kni设备的操作接口
	ether_setup(dev); /* assign some of the fields */
	dev->netdev_ops      = &kni_net_netdev_ops;
}

        除此之外还kni设备还提供了对ethtool工具的操作接口。需要注意的是,为了使得kni设备能够支持ethtool工具,需要使用linux内核提供的标准ixgbe/igb驱动。

//ethtool工具操作接口
struct ethtool_ops kni_ethtool_ops = 
{
	.begin 				= kni_check_if_running,
	.get_drvinfo		= kni_get_drvinfo,
	.get_settings		= kni_get_settings,
	.set_settings		= kni_set_settings,
	.get_regs_len		= kni_get_regs_len,
	.get_regs			= kni_get_regs,
	.....................................
};
//ethtool工具操作接口
void kni_set_ethtool_ops(struct net_device *netdev)
{
	netdev->ethtool_ops = &kni_ethtool_ops;
}

3、多线程模式下接收应用层报文

       多线程模式下,每一个kni设备都有一个与之一一对于的线程,用于从接收队列中接收来自应用层的报文。同时也会从响应队列接收来自应用层处理完成后的命令响应。多线程入口为:kni_thread_multiple

//多线程模式下,线程入口
static int kni_thread_multiple(void *param)
{
	while (!kthread_should_stop()) 
	{
		//接收来自应用层的报文
		kni_net_rx(dev);
		//接收来自应用层的响应消息
		kni_net_poll_resp(dev);
	}
}

        正常情况下接收应用层报文的接口为kni_net_rx_normal。首先会从rx接收队列中获取应用层传进来的报文,然后将报文转为内核协议栈支持的sk_buff节后,最后调用netif_rx内核接口将sk_buff发往内核。  对于已经发往内核的报文,将mbuf放回到释放队列,由应用层统一进行释放,保证驱动代码的简洁,使得驱动只做最小的事情。

//接收应用层报文处理,  将mbuf转为sk_buff后,将报文发往内核
static void kni_net_rx_normal(struct kni_dev *kni)
{
	//从应用层传进来的队列中获取报文
	ret = kni_fifo_get(kni->rx_q, (void **)va, num);
	//将来自应用层的报文转成sk_buff结构,然后发给内核协议栈
	for (i = 0; i < num; i++)
	{
		kva = (void *)va[i] - kni->mbuf_va + kni->mbuf_kva;
		len = kva->data_len;
		data_kva = kva->buf_addr + kva->data_off - kni->mbuf_va + kni->mbuf_kva;
		//开辟sk_bff空间
		skb = dev_alloc_skb(len + 2);
		//从mbuf拷贝报文到sk_buff
		memcpy(skb_put(skb, len), data_kva, len);
		skb->dev = dev;
		skb->protocol = eth_type_trans(skb, dev);
		skb->ip_summed = CHECKSUM_UNNECESSARY;
		//交给内核,发送到协议栈
		netif_rx(skb);
	}
	//已经处理完成的报文,放到释放队列,由应用层进行释放
	ret = kni_fifo_put(kni->free_q, (void **)va, num);
}

4、单线程模式下接收应用层报文

        单线程模式下,整个kni驱动将只会有一个线程,用于接收所有kni设备来自应用层的报文。在应用层执行open操作,打开/dev/kni混合设备的时候将会创建单线程。单线程入口为kni_thread_single

//打开/dev/kni混合设备
static int kni_open(struct inode *inode, struct file *file)
{
	//创建单线程
	kni_kthread = kthread_run(kni_thread_single, NULL, "kni_single");
}

        单线程模式下只有一个线程,轮询所有的kni设备,接收这个kni设备来自应用层的报文。

//单线程模式下,线程入口
static int kni_thread_single(void *unused)
{
	int j;
	struct kni_dev *dev, *n;

	while (!kthread_should_stop())
	{
		//遍历所有的kni设备
		list_for_each_entry_safe(dev, n, &kni_list_head, list) 
		{
			//接收kni设备来自应用层的报文
			kni_net_rx(dev);
			//接收kni设备来自应用层的响应消息
			kni_net_poll_resp(dev);
		}
	}
}

5、kni驱动发包给应用层

         当kni驱动收到来自内核的报文后,会调用kni_net_tx从malloc分配队列中获取一个应用层已经分配好的mbuf结构。同时将sk_buff报文转为mbuf报文,存放到mbuf中。之后将mbuf报文放到tx发送队列中。应用层从tx发送队列中获取报文后,将报文通过网卡发送出去。

static int kni_net_tx(struct sk_buff *skb, struct net_device *dev)
{
	//从应用层获取一个mbuf空间,将sk_buff的内容填充到这个mbuf中。然后发给应用层
	ret = kni_fifo_get(kni->alloc_q, (void **)&pkt_va, 1);
	pkt_kva = (void *)pkt_va - kni->mbuf_va + kni->mbuf_kva;
	data_kva = pkt_kva->buf_addr + pkt_kva->data_off - kni->mbuf_va + kni->mbuf_kva;
	//将sk_buff填充到mbuf中
	len = skb->len;
	memcpy(data_kva, skb->data, len);
	pkt_kva->pkt_len = len;
	pkt_kva->data_len = len;
	//将报文放到发送队列,由应用层读取
	ret = kni_fifo_put(kni->tx_q, (void **)&pkt_va, 1);
}

六、综合案例

        以一个邮件服务器案例来说明使用dpdk以及kni之间需要注意的地方。邮件服务器分为3个部分。

1、模块组成

        首先是nignx服务器,这是一个http服务器,是邮件服务器的控制页面,用于对邮件服务器进行设置操作。例如设置邮件服务器监听的端口,设置邮件服务器支持的协议类型等。 

         接着是邮件服务器本身,用于邮件协议的处理,处理邮件的收发。

         最后是dpdk程序,负责将邮件消息,以及邮件服务器的控制消息转发给邮件服务器。

dpdk之kni实现_第7张图片

2、实现过程

        dpdk与邮件服务处于两个不同的进程,双方之间使用ring无锁队列进行通信,也就是通过共享内存的方式通信。当网卡收到报文后,被dpdk托管,dpdk发现是邮件协议的报文,进而将报文写入到队列中,发给邮件服务器。邮件服务器从队列中接收报文后,对报文进行处理。

        同样,dpdk收到网卡的报文,发现报文不是邮件协议,而是一些控制报文,发现目的ip是本机自己。则将报文通过kni设备转发给内核。内核协议栈收包后,内核将报文发给应用层的nginx服务器。nginx服务器接收消息后,通过ipc进程通信的方式,发给邮件服务器,对邮件服务器进行配置操作。例如禁用smtp功能。

3、dpdk注意项

(1)、当报文是到达本机的,例如ping等。则dpdk将报文发给kni设备,进入内核协议栈处理

(2)、如果报文不是发给本机的, 接收到来自网卡的报文后,dpdk判断如果是二层报文则查找fdb表; 如果是三层报文,则进行dnat, snat处理后,查找路由表, 将报文转发给下一跳路由。 当然,二三层转发,路由,snat, dnat都需要应用层自己实现。

(3)、通常网卡被dpdk拖管后,ifconfig是看不到网卡信息了的,也就无法通过tcpdump进行抓包。怎么做呢?使用kni设备,或者tun设备,就可以给虚拟网卡设置一个ip地址, 自然也就可以通过tcpdcump抓包。但此时仅能够抓经过内核协议栈的报文,也就是经过本机的报文,无法抓转发给下一跳的报文。怎么做呢? 这就需要代码来实现了,其实也不会复杂。在抓转发报文的时候,将转发到下一跳的报文顺便发一份给内核就好了。在使用tcpdump抓包的时候,tcpdump内部使用libcap库就能从内核抓到所有的报文。

七、tun虚拟网卡拓展

         还有一种方式,也可以将报文发给内核协议栈,那就是tun虚拟网卡方式。这其实和kni操作是差不多的。dpdk提供了exception_path例子来介绍tun的使用。首先应用层打开/dev/net/tun设备,然后通过往这个/dev/net/tun设备发送ioctl消息,  内核接收到ioctl消息后创建虚拟网卡。这和kni设备的创建是不是很相似,kni设备是通过往/dev/kni混合设备发ioctl消息来创建kni设备的。在创建完虚拟网卡后,也可以和kni设备执行类似的操作。例如ifconfig配置tun虚拟网卡ip,  ethtool设置虚拟网卡信息,tcpdump抓包等。

        要使用tun虚拟网卡功能,大体上就下面三个调用操作就行了。

//读写方式打开tun设备
int tap_fd = open("/dev/net/tun", O_RDWR);
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
snprintf(ifr.ifr_name, IFNAMSIZ, "%s", name);
ret = ioctl(tap_fd, TUNSETIFF, (void *) &ifr);

//通过tun,将报文发往内核
ret = write(tap_fd, rte_pktmbuf_mtod(m, void*), rte_pktmbuf_data_len(m));
								
//通过tun接口,从内核接收报文
ret = read(tap_fd, rte_pktmbuf_mtod(m, void *), MAX_PACKET_SZ);

        到此kni设备的实现已经分析完成了。

你可能感兴趣的:(dpdk源码分析)