跳到主要内容
P小二 P小二
← 返回文章 • DPU • • 约 12 分钟

11.DPU网络开发SDK——DPDK(十)

DPU专栏: 11.DPU网络开发SDK——DPDK(十)

接上一次内容继续分析pci总线的scan()方法。

pci_scan_one()会完成如下工作:

static int pci_scan_one(const char *dirname, const struct rte_pci_addr *addr) {
   char filename[PATH_MAX];
   unsigned long tmp;
   struct rte_pci_device *dev;
   char driver[PATH_MAX];
   int ret;
   dev = malloc(sizeof(*dev)); //创建
   ……
   memset(dev, 0, sizeof(*dev)); //关联总线
   dev->device.
bus = &rte_pci_bus.bus;
   dev->
addr = *addr;
   snprintf(filename, sizeof(filename), "%s/vendor", dirname); //设备的vendor和device id
   if (eal_parse_sysfs_value(filename, &tmp) id.
vendor_id = (uint16_t)tmp;
   snprintf(filename, sizeof(filename), "%s/device", dirname);
   if (eal_parse_sysfs_value(filename, &tmp) id.
device_id = (uint16_t)tmp;
   snprintf(filename, sizeof(filename), "%s/subsystem_vendor”, dirname); //子系统的vendor和device id
   ……
   dev->id.
subsystem_vendor_id = (uint16_t)tmp;
   snprintf(filename, sizeof(filename), "%s/subsystem_device”, dirname);
   ……
   dev->id.
subsystem_device_id = (uint16_t)tmp;
   snprintf(filename, sizeof(filename), "%s/class”, dirname); //设备类的id
   ……
   /* the least 24 bits are valid: class, subclass, program interface */
   dev->id.
class_id = (uint32_t)tmp & RTE_CLASS_ANY_ID;
   dev->
max_vfs = 0;
   snprintf(filename, sizeof(filename), "%s/max_vfs", dirname); //最大的vf数量
   if (!access(filename, F_OK) && eal_parse_sysfs_value(filename, &tmp) == 0)
      dev->
max_vfs = (uint16_t)tmp;
   else {
      /* for non igb_uio driver, need kernel version >= 3.8 */
      snprintf(filename, sizeof(filename), "%s/sriov_numvfs", dirname);
      if (!access(filename, F_OK) && eal_parse_sysfs_value(filename, &tmp) == 0)
         dev->
max_vfs = (uint16_t)tmp;
   }
   snprintf(filename, sizeof(filename), "%s/numa_node", dirname); //获取到设备所属的numa节点
   if (access(filename, F_OK) != -1) {
       ……
   }
   pci_name_set(dev);
   snprintf(filename, sizeof(filename), "%s/resource", dirname);
   if (pci_parse_sysfs_resource(filename, dev) kdrv = RTE_PCI_KDRV_VFIO;
      else if (!strcmp(driver, "igb_uio"))
         dev->
kdrv = RTE_PCI_KDRV_IGB_UIO;
      else if (!strcmp(driver, "uio_pci_generic"))
         dev->
kdrv = RTE_PCI_KDRV_UIO_GENERIC;
      else
         dev->
kdrv = RTE_PCI_KDRV_UNKNOWN;
   } else {
      dev->
kdrv = RTE_PCI_KDRV_NONE;
      return 0;
   }
   if (TAILQ_EMPTY(&rte_pci_bus.device_list)) { //空列表直接插入
      rte_pci_add_device(dev);
   } else {
      struct rte_pci_device *dev2;
      int ret;
      TAILQ_FOREACH(dev2, &rte_pci_bus.device_list, next) {
         ret = rte_pci_addr_cmp(&dev->addr, &dev2->addr); //比较设备名称
         if (ret > 0)
            continue;
         if (ret device)) { //设备没有绑定驱动
               dev2->
kdrv = dev->kdrv;
               dev2->
max_vfs = dev->max_vfs;
               dev2->
id = dev->id;
               pci_name_set(dev2);
               memmove(dev2->mem_resource, dev->mem_resource, sizeof(dev->mem_resource));
            } else { //设备绑定了驱动,
               if (dev2->kdrv != dev->kdrv || dev2->max_vfs != dev->max_vfs || memcmp(&dev2->id, &dev->id, sizeof(dev2->id)))
                  RTE_LOG(ERR, EAL, "Unexpected device scan at %s!\n", filename);
               else if (dev2->device.devargs != dev->device.devargs) {
                  rte_devargs_remove(dev2->device.devargs);
                  pci_name_set(dev2);
               }
            }
            free(dev);
         }
         return 0;
      }
      rte_pci_add_device(dev);
   }
   return 0;}

首先,他会创建一个rte_pci_device类型的实例,结构如下,可以认为是PCI总线设备的数据结构,他是rte_device设备的封装,从类角度上讲,rte_pci_device可以认为是rte_device的派生类。

struct rte_pci_device {
   TAILQ_ENTRY(rte_pci_device) next;
   /**addr, dev->name, sizeof(dev->name)); //设备名称即为设备的总线编号
   devargs = pci_devargs_lookup(&dev->addr);
   dev->device.
devargs = devargs;
   ……}

设置名称的过程中,还会设置设备的参数,调用pci_devargs_lookup()来实现,在分析该func前,需要对init过程中第11步的内容进行细化。这个步骤中,eal_option_device_parse()会遍历devopt_list中的每个元素,并调用rte_devargs_add()添加到devargs_list中。

int eal_option_device_parse(void) {
   struct device_option *devopt;
   ……
   TAILQ_FOREACH_SAFE(devopt, &devopt_list, next, tmp) {
      ……
      ret = rte_devargs_add(devopt->type, devopt->arg);
      ……
      TAILQ_REMOVE(&devopt_list, devopt, next);
      free(devopt);
   }
      ……}
intrte_devargs_add(enum rte_devtype devtype, const char *devargs_str){
   struct rte_devargs *
devargs = NULL;
   struct rte_bus *
bus = NULL;
   const char *
dev = devargs_str;
   ……
   devargs = calloc(1, sizeof(*devargs));
   if (devargs == NULL)
      goto fail;
   ……
   if (rte_devargs_parse(devargs, dev))
      goto fail;
   devargs->
type = devtype;
   bus = devargs->bus;
   if (devargs->type == RTE_DEVTYPE_BLOCKED)
      devargs->
policy = RTE_DEV_BLOCKED;
   if (bus->conf.scan_mode == RTE_BUS_SCAN_UNDEFINED) {
      if (devargs->policy == RTE_DEV_ALLOWED)
         bus->conf.
scan_mode = RTE_BUS_SCAN_ALLOWLIST;
      else if (devargs->policy == RTE_DEV_BLOCKED)
         bus->conf.
scan_mode = RTE_BUS_SCAN_BLOCKLIST;
   }
   TAILQ_INSERT_TAIL(&devargs_list, devargs, next);
   return 0;
   ……}

rte_devargs_add()会创建一个rte_devargs类型的结构,并调用rte_devargs_parse()对传入的devargs字符串进行解析。传入的devargs字符串的最开始会指明总线类型,据此查找对应的rte_bus结构(通过遍历rte_bus_list结构并比较bus的名称);指明总线类型之后,devargs字符串指明了设备的名称,将设备名称存入rte_devargs->name中;名称之后,devargs字符串的内容就是额外的参数了,存入rte_devargs->args即可。调用rte_devargs_parse()之后,rte_devargs_add()如果检测到设备绑定的总线未指明scan模式,则根据设备的策略指定总线的scan模式。

int rte_devargs_parse(struct rte_devargs *da, const char *dev) {
   struct rte_bus *
bus = NULL;
   const char *devname;
   const size_t maxlen = sizeof(da->name);
   size_t i;
   ……
   do {
  //查找总线
      devname = dev;
      bus = rte_bus_find(bus, bus_name_cmp, dev);
      if (bus == NULL)
         break;
      devname = dev + strlen(bus->name) + 1;
      if (rte_bus_find_by_device_name(devname) == bus) //设备名称经过解析之后,是该总线的设备名称的格式
         break;
   } while (1);
   i = 0;
   while (devname[i] != '\0' && devname[i] != ',') { //设备名称
      da->name[i] = devname[i];
      i++;
      ……
   }
   ……
   da->
bus = bus;
   if (devname[i] == ',')
      da->
args = strdup(&devname[i + 1]);
   else
      da->
args = strdup("");
   ……
   return 0;}

回到pci_devargs_lookup(),该func根据设备名称查找devargs_list中的元素,找到设备对应的rte_devargs数据结构,将该数据结构中存入的devargs和设备名称填充到rte_pci_device->rte_device中的相应成员。

继续分析pci_scan_one()所做工作,通过sys文件系统读取完vendor和device id这些内容之后接下来分析设备的resource文件当中的内容,调用pci_parse_sysfs_resource()来实现。该过程主要是解析PCI设备的bar空间,bar空间是记录计算机在BIOS加载过程中,分配给每个设备的物理内存的信息,每个设备最多可以分配到6个连续的物理内存段,这些信息经过解析之后,会存放在rte_pci_device->mem_resource这个数组当中。解析完resource文件之后,会解析该设备绑定的驱动设备,调用pci_get_kernel_driver_by_path()来实现,获取到设备绑定的驱动程序后,将其记录在rte_pci_device->rte_device->kdrv中,目前DPDK能够识别的驱动有vfio-pci,igb_uio和uio_pci_generic。最后,调用rte_pci_add_device()将设备插入到rte_pci_bus->device_list中,插入过程需要检查是否已经有同名设备已经存在于列表当中,且插入后设备的名称是有序的;插入过程中如果设备存在,那么要么做替换处理,要么删除旧设备再插入新的。

至此PCI总线的scan方法分析完毕,PCI总线的scan方法是在DPDK的初始化过程中被调用的,通过上面的分析,可以看到scan方法主要是完成了设备的检索,并创建出相应的数据结构添加到设备列表当中。DPDK初始化过程中对每一个总线都会调用其scan方法,将各设备存储在各自的总线的列表当中。

在DPDK初始化过程的第33步调用rte_bus_probe()来处理设备探测,和rte_bus_scan()同理,每个总线的probe方法会被调用,以PCI的probe方法pci_probe()为例,该方法对于rte_pci_bus->device_list中的所有设备,依次调用pci_probe_all_drivers()方法。该func中,会依次遍历rte_pci_bus.driver_list中的PCI设备的驱动,并调用rte_pci_probe_one_driver()将驱动和设备进行匹配,建立PCI设备与PCI设备驱动之间的联系。

static int pci_probe_all_drivers(struct rte_pci_device *dev) {
   struct rte_pci_driver *
dr = NULL;
   int rc = 0;
   ……
   FOREACH_DRIVER_ON_PCIBUS(dr) {
      rc = rte_pci_probe_one_driver(dr, dev);
      if (rc
  0)
         /* positive value means driver doesn't support it */
         continue;
      return 0;
   }
   return 1;}
#define FOREACH_DRIVER_ON_PCIBUS(p) \
      TAILQ_FOREACH(p, &(rte_pci_bus.driver_list), next)

那么PCI设备的驱动是如何插入到rte_pci_bus.driver_list当中的呢,还是通过如下宏定义实现:

void rte_pci_register(struct rte_pci_driver *driver) {
   TAILQ_INSERT_TAIL(&rte_pci_bus.driver_list, driver, next);
   driver->
bus = &rte_pci_bus;}
#define RTE_PMD_REGISTER_PCI(nm, pci_drv) \RTE_INIT(pciinitfn_ ##nm) \{\
   (pci_drv).driver.
name = RTE_STR(nm);\
   rte_pci_register(&pci_drv); \} \RTE_PMD_EXPORT_NAME(nm, __COUNTER__)
#define RTE_INIT(func) \
   RTE_INIT_PRIO(func, LAST)
#define RTE_INIT_PRIO(func, prio) \static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void)

可以看出,RTE_PMD_REGISTER_PCI这个宏定义同样是实现了一个构造函数,和总线注册时基本相同,不同之处在于其注册时的priority是LAST级别的,意思是在其他构造函数执行完之后才会执行,因为必须等到rte_bus_pci注册完毕之后,PCI设备的驱动才可以注册到rte_bus_pci.driver_list当中。

后续分析rte_pci_probe_one_driver()的实现。

往期回顾:

10.DPU网络开发SDK——DPDK(九)

09.DPU网络开发SDK—DPDK(八)