基于之前实现的UDP/TCP Server代码,直接添加KNI部分
前言
DPDK提供了一系列对网卡的操作,包括rte_ether_hdr 、rte_ipv4_hdr 、rte_udp_hdr 等,需要自己进行协议栈的解析处理。但是在应用开发的过程中,有很多部分是不需要DPDK做这种处理的,比如:DNS仅处理所有协议中UDP部分的数据,而其他类型的数据DPDK不做处理,还是写回内核,交给内核协议栈去做。对于需要写回内核的数据,DPDK提供了一组接口——Kernel Network Interface,简称KNI。
DPDK开启了一个线程,不断地取数据,通过KNI往内核里面送,内核协议栈处理完后,再交给应用程序。这个过程中,KNI会在内核中生成一个net device,例如:veth0。与eth0、eth1一样,DPDK通过这个虚拟的veth0送数据,对应内核协议栈接管这个数据。DPDK能够通过修改生成的映射文件dev/kni对它进行操作。
一、KNI启动
在运行DPDK时,需要选择“45”选项,插入KNI模块。
1.先初始化KNI
在初始化端口时,初始化KNI并分配内存。
struct rte_kni *global_kni = NULL;
if (-1 == rte_kni_init(gDpdkPortId)) {
rte_exit(EXIT_FAILURE, "kni init failed\n");
}
init_port(mbuf_pool);
global_kni = alloc_kni(mbuf_pool);
2.分配一个KNI
创建kni函数定义:
static struct rte_kni *alloc_kni(struct rte_mempool *mbuf_pool);
所需的参数:
- mbuf_pool,dpdk为kni提供了一个线程,线程分配了一块内存,接收数据就需要内存池;
- conf:配置kni的一些参数,包括配置名字、绑定的ID、包的大小等;
- ops:表示操作,比如 :更改IP地址,更改MAC地址,down掉网卡等。
1)初始化conf
struct rte_kni_conf conf;
memset(&conf, 0, sizeof(conf));
snprintf(conf.name, RTE_KNI_NAMESIZE, "vEth%u", gDpdkPortId);
conf.group_id = gDpdkPortId;
conf.mbuf_size = MAX_PACKET_SIZE;
rte_eth_macaddr_get(gDpdkPortId, (struct rte_ether_addr *)conf.mac_addr);
rte_eth_dev_get_mtu(gDpdkPortId, &conf.mtu);
print_ethaddr("alloc_kni: ", (struct rte_ether_addr *)conf.mac_addr);
2)初始化ops
struct rte_kni_ops ops;
memset(&ops, 0, sizeof(ops));
ops.port_id = gDpdkPortId;
ops.config_network_if = config_network_if;
这里ops.config_network_if 设置的处理函数如下:
static int config_network_if(uint16_t port_id, uint8_t if_up) {
if (!rte_eth_dev_is_valid_port(port_id)) {
return -EINVAL;
}
int ret = 0;
if (if_up) {
rte_eth_dev_stop(port_id);
ret = rte_eth_dev_start(port_id);
} else {
rte_eth_dev_stop(port_id);
}
if (ret < 0) {
printf("Failed to start port : %d\n", port_id);
}
return 0;
}
3)初始化ops
创建kni_hanlder 后,作为函数返回值
kni_hanlder = rte_kni_alloc(mbuf_pool, &conf, &ops);
if (!kni_hanlder) {
rte_exit(EXIT_FAILURE, "Failed to create kni for port : %d\n", gDpdkPortId);
}
return kni_hanlder;
3.开启混杂模式
在init_port 函数中开启混杂模式
rte_eth_promiscuous_enable(gDpdkPortId);
二、重构网络协议分发流程
将UDP/TCP以外的所有协议,交给KNI处理。其中,保留一部分ARP功能,在协议栈层处理UDP/TCP报文时,判断IP与MAC信息是否在ARP表中,若没有则添加进去。
1.插入ARP信息
插入ARP信息实现函数如下:
static int arp_entry_insert(uint32_t ip, uint8_t *mac) {
struct arp_table *table = arp_table_instance();
uint8_t *hwaddr = get_dst_macaddr(ip);
if (hwaddr == NULL) {
struct arp_entry *entry = rte_malloc("arp_entry",sizeof(struct arp_entry), 0);
if (entry) {
memset(entry, 0, sizeof(struct arp_entry));
entry->ip = ip;
rte_memcpy(entry->hwaddr, mac, RTE_ETHER_ADDR_LEN);
entry->type = 0;
pthread_spin_lock(&table->spinlock);
LL_ADD(entry, table->entries);
table->count ++;
pthread_spin_unlock(&table->spinlock);
}
return 1;
}
return 0;
}
2.流程重构
将其他报文交给KNI,在pkt_process 函数中,添加以下代码: rte_kni_tx_burst 和rte_kni_handle_request 函数是配套使用的,如果不执行rte_kni_handle_request 函数,一旦执行ifconfig vEth0 192.168.0.119 up ,是不会有反应的,因为config_network_if 没有回调。
3.报错
执行ifconfig vEth0 192.168.0.119 up 出现:
SIOCSIFFLAGS: Timer expired
原因是UDP Server在处理时,定时器会发送ARP请求,ARP send时进入了死循环。
这里对ARP表优化,进行加速:
struct arp_table {
struct arp_entry *entries;
int count;
pthread_spinlock_t spinlock;
};
在每次添加ARP结点时加锁:
pthread_spin_lock(&table->spinlock);
LL_ADD(entry, table->entries);
table->count ++;
pthread_spin_unlock(&table->spinlock);
三、tcpdump抓包调试KNI
对虚拟网卡vEth0抓包:
tcpdump -i vEth0
tcpdump 是一个运行在命令行下的抓包工具。 例:
- 在ens33网卡上监听100个目标端口是443端口的数据包,并保存到
/root/1.cap ,可再用wireshark打开并分析数据包
tcpdump -i ens33 -c 100 ‘dst port 443’ -w /root/1.cap
- 在ens33网卡上监听5个目标主机是192.168.146.131的不是tcp协议的数据包:
tcpdump -i ens33 -c 5 dst host 192.168.146.131 and ‘not tcp’
四、总结
KNI主要是DPDK用于对内核的补充,不需要处理的信息,写入到KNI,让内核协议栈处理。可以让内核协议栈与DPDK用户态协议栈同时存在运行。 另外,代码运行时可能会出现内存泄漏的现象,就需要在使用完或者未生成时释放TCP/UDP数据 对应的内存。
|