dpdk igbuio基础信息 转载
pci设备的基地址
上图为pci配置空间的分布图,在图中,0x0010 ~ 0x0028这24个字节中,分布着6个PCI BAR(base address register),也就是最最重要的“基地址”,那这里有人可能会想问“这个图和我们有关系么?这个图中的空间在哪?我们该怎么解析?”,答案是“无关”,这些图中的信息事实上在系统启动时,就已经被解析完成了,以文件系统的方式供用户态程序取读取。但是这里其实有这样的一个问题:
- PCI设备为啥有6个BAR,而不是3个、8个?这些BAR都有啥区别?实际访问寄存器的时候以哪一个BAR为基准呢?
因为pci设备规定就是有6个bar空间,而不简单是因为不知道为什么规定6个bar空间。那么这些BAR又有什么区别呢?
6个槽(BAR)允许设备以不同的目的提供不同的区域;来看一下intel 82599这款经典的10G网卡的datasheet中9.3.6中的解释。见下图
可以看到这款经典网卡(其实intel的卡基本都是这么分的)主要将6个pci bar分成了三块区域:
- Memory BAR : 内存BAR,Memory BAR标志着这块BAR空间位于内存空间,通过mmap映射后可以直接访问。
- I/O BAR : IO BAR空间,I/O BAR标志着这块BAR空间位于IO空间,对其的访问不能像Memory BAR那样映射之后就可以随心所欲访问,IO BAR必须通过专门的操作来进行读写。
- MSI-X BAR : 这个BAR空间主要是用来配置MSI -X 中断向量。
一共不是6个BAR空间么?这里只分了3个区域,那么每个区域分多少呢?这里请注意的是关于图3中6个PCI BAR,每个PCI BAR都是32位的,但是像82599这种工作在64位的网卡,其实就只有三个BAR。BAR0 BAR1为Memory BAR,BAR2 BAR3为I/O BAR,BAR4 BAR5为MSI-X BAR
intel 82599的datasheet来得知intel的64bit网卡的bar分布是长什么样子的,如下图
非x86体系架构下,例如ARM、PowerPC这些架构下,所有的外设和主存(RAM)都会进行统一的编址,所以kernel可以像访问正常的内存空间一样访问内设。而x86体系架构下,外设是进行独立编址的,如图8所示,因此也就出现了IO空间和Memory空间的区别。(其实可以将RAM看成一种”专门用来内存映射的IO设备“)。另外我们从图8还可一看到另外一个信息,那就是访问外设其实可以有两种方式,一种是通过I/O空间用专有的指令进行访问,另外一种便是访问内存空间,而访问内存空间就相对而言容易的多,也随便的多,那么为什么外设会同时拥有两个空间呢?这里是由于外设通常会自带“存储器”。另外宝华叔还特地提到了如下一句话:
访问外设可以通过访问内存空间,而访问外设其实可以不必通过IO空间,也间接说明了IO空间实际上不是访问设备所必要的,而内存空间才是必要的
x86中访问I/O空间是必须通过一些专有指令进行访问的,通过独特的in、out指令进行访问,端口号表示了外设的寄存器地址。
DPDK如何拿到BAR
#define PCI_MAX_RESOURCE 6 /* * pci扫描文件系统下的resource文件 * @param filename 通常为/sys/bus/pci/devices/[pci_addr]/resource文件 * @param dev[out] dpdk中对一个pci设备的抽象 */ static int pci_parse_sysfs_resource(const char *filename, struct rte_pci_device *dev) { FILE *f; char buf[BUFSIZ]; int i; uint64_t phys_addr, end_addr, flags; f = fopen(filename, "r"); //先打开resource文件,resource文件是一个只读文件,任何的写操作都会被忽略掉 if (f == NULL) { RTE_LOG(ERR, EAL, "Cannot open sysfs resource\n"); return -1; } //扫描6次,为什么是6次,在之前已经提到,PCI最多有6个BAR for (i = 0; i<PCI_MAX_RESOURCE; i++) { if (fgets(buf, sizeof(buf), f) == NULL) { RTE_LOG(ERR, EAL, "%s(): cannot read resource\n", __func__); goto error; } //扫描resource文件拿到BAR if (pci_parse_one_sysfs_resource(buf, sizeof(buf), &phys_addr, &end_addr, &flags) < 0) goto error; //如果是Memory BAR,则进行记录 if (flags & IORESOURCE_MEM) { dev->mem_resource[i].phys_addr = phys_addr; dev->mem_resource[i].len = end_addr - phys_addr + 1; /* not mapped for now */ dev->mem_resource[i].addr = NULL; } } fclose(f); return 0; error: fclose(f); return -1; } /* * 扫描pci resource文件中的某一行 * @param line 某一行 * @param len 长度,为第一个参数字符串的长度 * @param phys_addr[out] PCI BAR的起始地址,这个地址要mmap才能用 * @param end_addr[out] PCI BAR的结束地址 * @param flags[out] PCI BAR的标志 */ int pci_parse_one_sysfs_resource(char *line, size_t len, uint64_t *phys_addr, uint64_t *end_addr, uint64_t *flags) { union pci_resource_info { struct { char *phys_addr; char *end_addr; char *flags; }; char *ptrs[PCI_RESOURCE_FMT_NVAL]; } res_info; //字符串处理 if (rte_strsplit(line, len, res_info.ptrs, 3, ' ') != 3) { RTE_LOG(ERR, EAL, "%s(): bad resource format\n", __func__); return -1; } errno = 0; //字符串处理,拿到PCI BAR起始地址、PCI BAR结束地址、PCI BAR标志 *phys_addr = strtoull(res_info.phys_addr, NULL, 16); *end_addr = strtoull(res_info.end_addr, NULL, 16); *flags = strtoull(res_info.flags, NULL, 16); if (errno != 0) { RTE_LOG(ERR, EAL, "%s(): bad resource format\n", __func__); return -1; } return 0; }
扫描某个pci设备的resource文件获得PCI BAR。也就是/sys/bus/pci/[pci_addr]/resource这个文件
resource文件内部的特点,前6行为PCI设备的6个BAR,每行共3列,其中第1列为PCI BAR的起始地址,第2列为PCI BAR的终止地址,第3列为PCI BAR的标识
- config: PCI配置空间,二进制,可读写;
- device: PCI设备ID,只读。很重要;
- driver: 为PCI设备采用的驱动目录的软连接,真正的目录位于/sys/bus/pci/drivers/目录下,可以看图10中显示这个PCI设备采用的是内核ixgbe驱动;
- enable: 设备是否正常使能,可读写;
- irq: 被分到的中断号,只读;
- local_cpulist: 这个网卡的内存空间位于和同处于一个NUMA节点上的cpu有哪些,列表方式呈现,只读。举个例子,比如网卡的内存空间位于numa node 0,cpu 1-6同样位于numa node0,那么读取这个文件的内容便是:1-6。重要,因为跨numa节点访问内存会带来极大的性能开销。
- local_cpu: 与local_cpulist的作用相同,不过是以掩码的方式给出,例如1-6号cpu和pci设备处于同一个numa节点,那么掩码便是0x7E(0111 1110)。重要,重要程度等价于local_cpulist。
- numa_node: 只读,告诉这个PCI设备属于哪一个numa节点。重要,会影响性能。
- resource: BAR空间记录文件,只读,任何写操作将会被忽略,通常有三列组成,第一列为PCI BAR起始地址,第二列为PCI BAR终止地址,第三列为这个PCI BAR的标识,见图9.
- resource0..N: 某一个PCI BAR空间,二进制,只读,可以映射,如果用户态程序向操作PCI设备必须通过mmap这个resource0..N,也就意味着这个文件是可以mmap的。重要。
- sriov_numfs: 只读,虚拟化常用的技术,sriov透传技术,可以理解在这个网卡上可以虚拟出多个虚拟网卡,这些虚拟网卡可以直接透传到qemu中的客户机,并且网卡内部会有一个小的交换机实现VM客户机数据包的收发,可以极大的减少时延,这个numvfs便是告诉这个pci设备目前虚拟出多少个虚拟网卡(vf)。重要,主要应用在虚拟化场合。
- sriov_totalvfs: 只读,作用与sriov_numfs相同,不过是总数,揭示这个PCI设备一共可以申请多少个vf。
- subsystem_device: PCI子系统设备ID,只读。
- subsystem_vendor: PCI子系统生产商ID,只读。
- vendor:PCI生产商ID,比如intel便是0x8086.重要。
DPDK真的是通过读取resource文件来拿到BAR的么?答案其实是否定的...DPDK获取PCI BAR并不是这么获取的
/* * 映射resource资源获取PCI BAR * @param DPDK中关于某一个PCI设备的抽象实例 * @param res_id下标,说白了就是获取第几个BAR * @param uio_res用来存放PCI BAR资源的结构 * @param map_idx uio_res数组的计数器 */ int pci_uio_map_resource_by_index(struct rte_pci_device *dev, int res_idx, struct mapped_pci_resource *uio_res, int map_idx) { ..... //省略 //打开/dev/bus/pci/devices/[pci_addr]/resource0..N文件 if (!wc_activate || fd < 0) { snprintf(devname, sizeof(devname), "%s/" PCI_PRI_FMT "/resource%d", rte_pci_get_sysfs_path(), loc->domain, loc->bus, loc->devid, loc->function, res_idx); /* then try to map resource file */ fd = open(devname, O_RDWR); if (fd < 0) { RTE_LOG(ERR, EAL, "Cannot open %s: %s\n", devname, strerror(errno)); goto error; } } /* try mapping somewhere close to the end of hugepages */ if (pci_map_addr == NULL) pci_map_addr = pci_find_max_end_va(); //进行mmap映射,拿到PCI BAR在进程虚拟空间下的地址 mapaddr = pci_map_resource(pci_map_addr, fd, 0, (size_t)dev->mem_resource[res_idx].len, 0); close(fd); if (mapaddr == MAP_FAILED) goto error; pci_map_addr = RTE_PTR_ADD(mapaddr, (size_t)dev->mem_resource[res_idx].len); //将拿到的PCI BAR映射至进程虚拟空间内的地址存起来 maps[map_idx].phaddr = dev->mem_resource[res_idx].phys_addr; maps[map_idx].size = dev->mem_resource[res_idx].len; maps[map_idx].addr = mapaddr; maps[map_idx].offset = 0; strcpy(maps[map_idx].path, devname); dev->mem_resource[res_idx].addr = mapaddr; return 0; error: rte_free(maps[map_idx].path); return -1; } /* * 对pci/resource0..N进行mmap,将PCI BAR空间通过mmap的方式映射到进程内部的虚拟空间,供用户态应用来操作设备 */ void * pci_map_resource(void *requested_addr, int fd, off_t offset, size_t size, int additional_flags) { void *mapaddr; //核心便是这句mmap,其中要注意的是,offset必须为0 mapaddr = mmap(requested_addr, size, PROT_READ | PROT_WRITE, MAP_SHARED | additional_flags, fd, offset); if (mapaddr == MAP_FAILED) { RTE_LOG(ERR, EAL, "%s(): cannot mmap(%d, %p, 0x%zx, 0x%llx): %s (%p)\n", __func__, fd, requested_addr, size, (unsigned long long)offset, strerror(errno), mapaddr); } else RTE_LOG(DEBUG, EAL, " PCI memory mapped at %p\n", mapaddr); return mapaddr; }
DPDK是直接mmap resource0..N就做到获取了PCI BAR,至于resource0..N则是内核自带的一个供用户态程序通过mmap的方式访问PCI BAR。网上很多的文章提到igb_uio的作用,基本都是以下两点:
- igb_uio负责将PCI BAR提供给用户态应用,也就是DPDK;但是这个没有使用igb_uio驱动提供访问PCI BAR 接口!
UIO提供了(PCI BAR)访问方式,但是DPDK直接mmap了resource,Kernel对resource实现的mmap跟在igb_uio中实现一个mmap是一样的实现,没有区别,用kernel自己的方式不是更好么?
- igb_uio负责处理中断,形成用户态程序和内核中断的一个桥梁。
- 确实可以确定如下内容:
- igb_uio负责创建uio设备并加载igb_uio驱动,负责将内核驱动接管的网卡抢过来,以此来先屏蔽掉内核驱动以及内核协议栈;
- igb_uio负责一个桥梁的作用,衔接中断信号以及用户态应用,因为中断只能在内核态处理,所以igb_uio相当于提供了一个接口,衔接用户态与内核态的驱动,关于驱动,后续会开文章专门讲解DPDK的中断;
转载自 谈谈DPDK如何实现bypass内核的原理 其一 PCI设备与UIO驱动