DPDK 简易应用开发之路 5:实现虚拟自学习交换机
本机环境为 Ubuntu20.04 ,dpdk-stable-20.11.10
使用scapy和wireshark发包抓包分析结果
完整代码见:github
自学习交换机
以太网交换机的任务是将接收到的数据包根据其目标地址转发到合适的端口。交换机通常具有多个端口,每个端口连接到不同的设备,数据包在这些端口之间传输。其核心功能包括:
- 地址学习:记录数据包源地址和接收端口之间的映射关系。
- 转发与过滤:根据目标MAC地址,决定将数据包发送到哪个端口。
- 广播与泛洪:当目标MAC地址未知时,数据包会广播到所有端口。
传统交换机是通过人工配置的MAC地址表来决定数据包的转发路径,而自学习交换机能够自动记录和更新MAC地址表,不需要人工干预。这使得交换机在拓扑变化时能自动适应,提高了灵活性和扩展性。
自学习交换机之所以能自动识别设备,是因为它会动态更新一个MAC地址表,记录设备的MAC地址和端口之间的映射关系。
MAC地址表的工作机制
- 学习源地址:每当交换机接收到一个数据包时,它会查看数据包的源MAC地址和接收端口。如果MAC地址不在表中,则添加一条记录;如果已经存在,但端口信息有变化,则更新该记录。
- 转发数据包:交换机接收到数据包后,根据目标MAC地址在表中查找对应的端口。如果找到,则转发到指定端口;如果没有找到,则执行广播,将数据包发送到除接收端口之外的所有端口。
- 表项老化:为了适应网络拓扑的变化,MAC地址表会定期清理未使用的表项。这一过程通常称为“老化”,可以防止内存资源耗尽。
本文基于vhost-user实现,其是一种DPDK与QEMU虚拟机之间通信的协议,允许虚拟机通过虚拟网卡直接访问DPDK管理的内存。交换机将虚拟设备视为特殊的端口,这样无论是物理设备还是虚拟设备,都可以通过同样的方式进行数据包接收和发送。
代码实现
大概分为以下四个主要模块,也是DPDK开发的一些通用步骤。
初始化和配置
命令行参数解析
#define NUM_MBUFS 8192
#define MBUF_CACHE_SIZE 250
#define MAX_PKT_BURST 32
#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024
#define NUM_HASH_ENTRIES 256static struct rte_mempool *mbuf_pool; // 数据包内存池
static int nb_ports = 0; // 物理端口数量
static int nb_sockets = 0; // 虚拟设备(vhost)套接字数量
static char *socket_files = NULL; // 存储套接字文件路径static int
parse_socket_paths(int argc, char **argv) {int opt;static struct option long_option[] = {{ "help", no_argument, NULL, 'h'},{ "socket-file", required_argument, NULL, 's'},{ NULL, 0, 0, 0},};while((opt = getopt_long(argc, argv, "hs:", long_option, NULL)) != -1) {switch (opt) {case 'h':print_usage();exit(0);break;case 's': socket_files = realloc(socket_files, PATH_MAX * (nb_sockets + 1));snprintf(socket_files + nb_sockets * PATH_MAX, PATH_MAX, "%s", optarg);nb_sockets++;break;default:print_usage();return -1;}}return 0;
}
使用getopt_long解析命令行参数,如果检测到-s或–socket-file选项,将套接字路径存储到socket_files,并增加虚拟设备数量nb_sockets。
DPDK环境初始化
int ret = rte_eal_init(argc, argv);
if (ret < 0)rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");argc -= ret;
argv += ret;
rte_eal_init()函数初始化DPDK的环境抽象层(EAL),使DPDK资源(如内存、设备接口等)可用。argc和argv偏移调整,准备解析自定义的命令行参数。
内存池创建
mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL",NUM_MBUFS * (nb_ports + nb_sockets),MBUF_CACHE_SIZE, 0,RTE_MBUF_DEFAULT_BUF_SIZE,rte_socket_id());
if (mbuf_pool == NULL)rte_exit(EXIT_FAILURE, "Cannot create mbuf pool\n");
MAC地址到端口的哈希表
const struct rte_hash_parameters mac_output_map_params = {.name = "mac_output_map",.entries = NUM_HASH_ENTRIES,.key_len = sizeof(struct rte_ether_addr),.hash_func = mac_to_uchar,.hash_func_init_val = 0,
};
mac_output_map = rte_hash_create(&mac_output_map_params);
if (mac_output_map == NULL)rte_exit(EXIT_FAILURE, "Cannot create MAC-output hash table\n");
设备管理
主要处理虚拟和物理设备的添加、删除以及设备状态更新。这个模块通过注册回调函数管理设备生命周期,以实现虚拟交换机的设备管理功能。
使用了一个dev_list(设备列表)来保存当前的设备信息,包括物理和虚拟设备。设备列表中每个设备的状态(是否为虚拟设备、是否就绪等)可以用来控制数据包的转发。
dev_list_mutex互斥锁用于确保设备列表的线程安全访问,防止多个线程对设备列表进行同时操作。设备的添加和删除操作都在互斥锁的保护下完成。
添加设备回调函数 new_vdev_callback()
static int
new_vdev_callback(int vid)
{struct dev_info *dev;dev = (struct dev_info *)rte_zmalloc("vhost device", sizeof(*dev), RTE_CACHE_LINE_SIZE);if (dev == NULL) {rte_log(RTE_LOG_ERR, RTE_LOGTYPE_USER1, "Error allocating memory for virtual device: vid=%d\n", vid);return -1;}dev->virtual = 1; // 标记为虚拟设备dev->id = vid; // 设置虚拟设备IDdev->state = DEVICE_READY; // 设备状态设置为“已就绪”// 标记设备列表更新dev_list_update = 1;// 加锁确保线程安全pthread_mutex_lock(&dev_list_mutex);TAILQ_INSERT_TAIL(&dev_list, dev, dev_entry); // 将设备添加到列表// 解除更新标记dev_list_update = 0;pthread_mutex_unlock(&dev_list_mutex);rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1, "Virtual device %d added to the device list\n", dev->id);return 0;
}
为新虚拟设备分配内存。将virtual字段设置为1,表明这是一个虚拟设备,并赋予其ID和状态。由于设备列表可能会被其他线程访问,因此加锁后将新设备添加到设备列表末尾。设置dev_list_update为1来通知主转发线程更新设备列表。
删除设备回调函数 destroy_vdev_callback()
static void
destroy_vdev_callback(int vid)
{struct dev_info *dev = NULL;struct rte_ether_addr *key;int ret;// 在设备列表中找到要删除的虚拟设备TAILQ_FOREACH(dev, &dev_list, dev_entry) {if (dev->virtual && dev->id == vid)break;}if (dev == NULL) {rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1, "Device with ID %d not found in the list.\n", vid);return;}dev->state = DEVICE_CLOSING; // 设置设备状态为关闭dev_list_update = 1; // 标记设备列表更新pthread_mutex_lock(&dev_list_mutex);// 从设备列表中移除该设备TAILQ_REMOVE(&dev_list, dev, dev_entry);// 从哈希表中删除所有与该设备关联的MAC映射for (uint32_t i = 0; i < NUM_HASH_ENTRIES; i++) {if (output_table[i] && output_table[i]->virtual && output_table[i]->id == vid) {ret = rte_hash_get_key_with_position(mac_output_map, i, (void **)&key);if (ret >= 0 && key != NULL) {rte_hash_del_key(mac_output_map, key);output_table[i] = NULL;}}}dev_list_update = 0;pthread_mutex_unlock(&dev_list_mutex);rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1, "Virtual device %d deleted from the device list\n", dev->id);rte_free(dev);
}
遍历设备列表,查找与要删除的虚拟设备ID匹配的设备。将设备状态设置为DEVICE_CLOSING,通知主转发线程停止对该设备的包转发。在哈希表中,删除与该设备ID相关联的所有MAC地址映射。加锁后将设备从列表中移除,并释放设备结构体的内存。
数据包处理与转发
主要实现了数据包的MAC地址学习和转发逻辑。这部分代码负责接收数据包,学习源MAC地址并更新映射关系,然后根据目的MAC地址决定数据包是进行单播还是广播,以实现虚拟交换机的核心功能。
MAC地址学习函数 learn_mac_address()
static int
learn_mac_address(struct rte_mbuf *mbuf, struct dev_info *s_dev)
{struct rte_ether_hdr *pkt_hdr;int ret;// 获取数据包的以太网头部并提取源MAC地址pkt_hdr = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *);// 将源MAC地址添加到mac-output哈希表中ret = rte_hash_add_key(mac_output_map, &pkt_hdr->s_addr);if (ret < 0) {rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1,"Cannot add entry to mac-output lookup table (error %d)\n", ret);return -1;}// 更新output_table中与MAC地址关联的设备信息if (output_table[ret] == NULL ||output_table[ret]->virtual != s_dev->virtual ||output_table[ret]->id != s_dev->id) {output_table[ret] = s_dev; // 将设备信息更新至output_tablerte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1, "Learned MAC: ""%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8 ":%02" PRIx8" mapped to %s device with id %d\n",pkt_hdr->s_addr.addr_bytes[0], pkt_hdr->s_addr.addr_bytes[1],pkt_hdr->s_addr.addr_bytes[2], pkt_hdr->s_addr.addr_bytes[3],pkt_hdr->s_addr.addr_bytes[4], pkt_hdr->s_addr.addr_bytes[5],(s_dev->virtual) ? "virtual" : "physical", s_dev->id);}return 0;
}
通过访问数据包的以太网头部,获取源MAC地址。将源MAC地址存储到哈希表mac_output_map中,将其与设备信息关联。如果源MAC地址在表中不存在,或其设备信息有更新,则将新的设备信息存储到output_table中。
数据包转发函数 forward_packet()
static void
forward_packet(struct rte_mbuf *mbuf, struct dev_info *s_dev)
{struct rte_ether_hdr *pkt_hdr;struct dev_info *dev;struct rte_mbuf *tbuf;int ret;// 获取以太网头部信息,并查找目的地址的输出设备pkt_hdr = rte_pktmbuf_mtod(mbuf, struct rte_ether_hdr *);ret = rte_hash_lookup(mac_output_map, &pkt_hdr->d_addr);// 广播处理:当目的地址不可识别时if (ret < 0) {TAILQ_FOREACH(dev, &dev_list, dev_entry) {if (dev == s_dev) continue; // 跳过数据包的来源设备if (dev->virtual) {// 虚拟设备未关闭时发送数据包if (unlikely(dev->state != DEVICE_CLOSING))rte_vhost_enqueue_burst(dev->id, 0, &mbuf, 1);} else {// 克隆数据包以便发送给物理设备tbuf = rte_pktmbuf_clone(mbuf, mbuf_pool);if (tbuf == NULL) continue;ret = rte_eth_tx_burst(dev->id, 0, &tbuf, 1);if (unlikely(ret == 0)) rte_pktmbuf_free(tbuf); // 发送失败则释放包}}rte_pktmbuf_free(mbuf); // 广播完成后释放原始数据包return;}// 单播处理:找到目标设备并转发dev = output_table[ret];if (dev->virtual) {if (unlikely(dev->state != DEVICE_CLOSING))rte_vhost_enqueue_burst(dev->id, 0, &mbuf, 1);rte_pktmbuf_free(mbuf);} else {ret = rte_eth_tx_burst(dev->id, 0, &mbuf, 1);if (unlikely(ret == 0)) rte_pktmbuf_free(mbuf);}
}
首先,从数据包的以太网头部中提取目的MAC地址,并在哈希表中查找对应的设备。
广播处理:如果目的地址在哈希表中未找到,意味着是未知设备。将数据包复制并广播到除来源设备外的所有设备。对于虚拟设备,调用rte_vhost_enqueue_burst()将数据包放入其接收队列;对于物理设备,使用rte_eth_tx_burst()发送。
单播处理:若查找到目的地址对应的设备,则执行单播转发。如果目标是虚拟设备,直接发送至虚拟设备;如果是物理设备,则通过物理端口发送数据包。
资源清理和退出
主要处理程序的清理和退出流程,包括释放内存、注销设备、销毁互斥锁等。这一部分确保程序可以正常终止,避免资源泄漏和设备未正常关闭的情况。
资源清理函数 cleanup()
static void cleanup(void)
{// 注销每个vhost套接字文件for (int vid = 0; vid < nb_sockets; vid++) {int ret = rte_vhost_driver_unregister(&socket_files[vid * PATH_MAX]);if (ret < 0) {rte_log(RTE_LOG_ERR, RTE_LOGTYPE_USER1,"Failed to unregister vhost socket file: %s\n",&socket_files[vid * PATH_MAX]);} else {rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1,"Successfully unregistered vhost socket file: %s\n",&socket_files[vid * PATH_MAX]);}}// 停止每个物理设备for (int portid = 0; portid < nb_ports; portid++) {rte_eth_dev_stop(portid);rte_eth_dev_close(portid);rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1,"Stopped and closed physical device with port id: %d\n", portid);}// 销毁设备列表的互斥锁pthread_mutex_destroy(&dev_list_mutex);rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1, "Destroyed device list mutex\n");// 打印日志并退出程序rte_log(RTE_LOG_INFO, RTE_LOGTYPE_USER1, "Cleanup completed. Exiting...\n");exit(0);
}
- 注销虚拟设备套接字:遍历每个虚拟设备套接字文件,使用rte_vhost_driver_unregister()函数注销设备。
- 停止物理设备:遍历每个物理端口,调用rte_eth_dev_stop()停止端口,随后使用rte_eth_dev_close()释放端口资源。
- 销毁互斥锁:清理dev_list_mutex互斥锁,避免内存泄漏。
退出信号处理函数 force_exit_handling()
static void force_exit_handling(__rte_unused int signum)
{force_exit = 1;
}
- 捕获SIGINT信号:当用户按下Ctrl+C时,系统会发送SIGINT信号,触发此函数的执行。
- 设置退出标志:将force_exit标志置为1,通知主转发线程结束包处理循环并进入清理阶段。
主转发线程中的退出逻辑
static int learning_switch_main(void *arg __rte_unused)
{struct rte_mbuf *mbufs[MAX_PKT_BURST];uint16_t nb_rcv;struct dev_info *dev;// 主循环:检查force_exit标志用于退出while (!force_exit) {pthread_mutex_lock(&dev_list_mutex);while (!dev_list_update && !force_exit) {TAILQ_FOREACH(dev, &dev_list, dev_entry) {if (dev->state == DEVICE_CLOSING) continue;// 接收并处理数据包if (dev->virtual) {nb_rcv = rte_vhost_dequeue_burst(dev->id, 0, mbuf_pool, mbufs, MAX_PKT_BURST);} else {nb_rcv = rte_eth_rx_burst(dev->id, 0, mbufs, MAX_PKT_BURST);}// 学习MAC地址并转发数据包for (int n = 0; n < nb_rcv; n++) {learn_mac_address(mbufs[n], dev);forward_packet(mbufs[n], dev);}}}pthread_mutex_unlock(&dev_list_mutex);}// 清理资源并退出cleanup();return 0;
}
测试结果
python发包代码:
from scapy.all import Ether, IP, sendp# 设置目标 IP 和 MAC 地址
target_ip = "192.168.131.152"
target_mac = "00:0c:29:00:04:43"# 源 IP 和 MAC 地址(VMnet8 网卡的地址)
source_ip = "192.168.131.1"
source_mac = "00:50:56:C0:00:09" # 要学习的地址# 构造以太网帧和 IP 包
eth_packet = Ether(src=source_mac, dst=target_mac)
ip_packet = IP(src=source_ip, dst=target_ip)# 将以太网帧和 IP 包组合成完整的数据包
packet = eth_packet / ip_packet# 发送数据包,通过 VMnet8 网卡
sendp(packet, iface="VMware Network Adapter VMnet8")
Python代码通过Scapy创建了一个以太网帧和IP包,并使用sendp()函数发送这个数据包。从00:50:56:C0:00:09发送的包被接收后,程序学习到了该映射关系,具体过程:
交换机接收到通过VMnet8接口发送的以太网帧。这个帧的源MAC地址为00:50:56:C0:00:09,目标MAC地址为交换机端口的MAC(00:0c:29:00:04:43)。
交换机调用learn_mac_address()函数,通过帧中的源MAC地址00:50:56:C0:00:09和端口ID,记录了这个地址与物理端口ID的映射关系。
将此关系存储在交换机的MAC地址哈希表和输出表中,使得交换机可以在后续数据包转发时知道如何处理发往00:50:56:C0:00:09的包。