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负责处理中断,形成用户态程序和内核中断的一个桥梁。
  • 确实可以确定如下内容:
  1. igb_uio负责创建uio设备并加载igb_uio驱动,负责将内核驱动接管的网卡抢过来,以此来先屏蔽掉内核驱动以及内核协议栈;
  2. igb_uio负责一个桥梁的作用,衔接中断信号以及用户态应用,因为中断只能在内核态处理,所以igb_uio相当于提供了一个接口,衔接用户态与内核态的驱动,关于驱动,后续会开文章专门讲解DPDK的中断;

 转载自 谈谈DPDK如何实现bypass内核的原理 其一 PCI设备与UIO驱动

 

posted @ 2021-12-10 20:55  codestacklinuxer  阅读(181)  评论(0编辑  收藏  举报