DPDK: KNI实现浅析

2023-10-17 21:50
文章标签 实现 浅析 dpdk kni

本文主要是介绍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设备,也可以通过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设备来进行。

         当生成了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设备与线程是一一对应的关系。

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

        应用层使用一个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队列中获取响应消息就知道请求的执行结果了。

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

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: //设置网卡的mtureq->result = kni->ops.change_mtu(kni->ops.port_id, req->new_mtu);break;case RTE_KNI_REQ_CFG_NETWORK_IF: //设置网卡up/downreq->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设备,这两个是不同的设备类型。

         

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_buffmemcpy(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、模块组成

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

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

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

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: KNI实现浅析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/228173

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一