Android kernel | PCI总线的冰山一角

在完成上一篇data分区的来龙去脉之后,我准备尝试将data分区对应的块设备重新初始化为zoned设备,这件事情听起来是一件事,做起来却是许许多多件事,难免让刚刚接触的我一头雾水。

从上一篇文章中我们知道,在virtio_bus这个总线类型上,当设备和驱动通过id_table完成匹配之后,会去执行相应的初始化函数,也就是这个函数:

static int virtio_dev_probe(struct device *_d)

但是渐渐的我发现一个问题,这个函数的入参是一个device,这个device已经完成了和一个driver设备的配对,对于相应的功能会去调用driver,实现需要的功能。

所以按照之前的想法,我如果想要在这个函数中修改对该设备的初始化方式,使得它能够去支持zoned,其实是不实际的,因为它此时已经和virtio_blk绑定,我们不应该让它进入到这个初始化函数中,而是应该另起一个新的驱动,完成类似于null_blk这种可以初始化zoned设备的驱动的功能。

那我们回忆一下就知道,一个设备能不能和一个驱动匹配,是由这个设备中的id决定的,这个id分为两个部分,一个是device_id一个是vendor_id,那这个id是由什么东西定义的呢?倘若我们能去修改这两个id,我们就有可能再设计一个驱动挂到virtio_bus上,和我们新修改的id完成匹配,进行初始化动作。

看上面函数的定义,既然device *_d 能够作为一个参数传入到virtio_dev_probe函数中,那就说明这个device设备在这之前已经完成了初始化,说明一定有某一个机制,识别出了这个设备然后初始化了device结构体。

PCI子系统

PCI是外围设备互连的简称,是一种通用总线接口标准。PCI总线是一种常见的主机IO总线,用于连接高速的外部设备。从1992年创立规范至今,PCI逐渐取代之前的技术,成为事实上的计算机的标准总线。

PCI总线体系结构是一种层次式的体系结构:

d463c3967506789bd2e23694223cbc3f.jpeg

  • PCI设备:遵循PCI规范,工作在PCI局部总线环境下的设备。PCI局部总线规范指出,每个PCI设备可以包含最多8个PCI功能,每个PCI功能是一个逻辑设备。

  • PCI桥设备:由于电子负载限制,每条PCI总线上可以挂载的设备数目是有限的因此使用了一种特殊的设备,即PCI-PCI桥设备将两条独立的PCI总线连接起来,PCI-PCI桥设备简称PCI桥。

  • 主桥设备:和CPU以及内存连在一起的Host-PCI桥设备为主桥设备,主桥设备引出的总线也称为PCI根总线。

PCI桥设备所在的总线称为主总线,引出的设备称为次总线,主总线称为次总线的父总线,次总线称为主总线的子总线。

要使得PCI设备能正常工作,CPU必须和PCI设备进行通讯,也就是说它们需要有共享的地址空间。同CPU一样,PCI设备也有自己内存空间和IO空间,这些空间被映射到CPU的内存空间和IO空间,在映射之后,PCI设备上的物理资源“变成”了CPU的本地资源。

PCI设备的地址空间映射在操作系统启动过程中自动完成。为了完成这样一个过程,在操作系统中,对于总线、桥设备、PCI设备都需要准备相应的数据结构,最后通过某种方式,确定总线的拓扑结构,也就是将硬件化的PCI总线虚拟化,CPU知道了PCI上有哪些东西,才能用起来得心应手啦。

PCI设备和PCI桥设备的配置空间

PCI设备的配置空间也称为配置寄存器组,其中前面64个字节(对应16个32位寄存器)的用途和格式是标准的,被称为配置寄存器的“头部”,对于标准PCI设备其头部为类型0,格式如:

5edb41e30b30f480320c97cab4f90c10.jpeg

可以看到我们苦苦寻找的device_id和vendor_id是存在于PCI设备的配置空间里面的。这两个ID组合在一起标识了PCI设备,通常被称为PCI ID。

这里的基地址寄存器用来向系统软件展示PCI设备内部资源的类型、属性和大小。0型配置头包含6个基址寄存器。

96d9a2c233229b1ba16bdd7b67acc2ef.jpeg

每个基址寄存器的第0位用来标识设备内部资源类型,它是只读不可修改的,设备制造商在生产该设备时直接配置该值。

  • bit0 = 0,系统需要在内存空间为该资源分配地址;

  • bit0 = 1,系统需要在IO空间为该资源分配地址;

  • bit1 = 1,表示预留,反之为不预留;

  • 若bit0 = 0,bit2 = 1表示定位64位地址空间,反之为32位,bit0 = 1总是32位;

  • 若bit0 = 0,bit3 = 1表示资源可预取,反之不可预取;

  • PCIe协议规定,bit4~6都是不可更改的,默认为0;

  • bit7~31或7~63(bit0 = 0,bit2 = 1)位用于确定资源大小,设备厂商会根据资源大小,将若干低位限制为0,不允许修改。

那操作系统如何通过基址寄存器确定资源大小呢?操作系统通过向基址寄存器中全写1,来完成这个任务↓

  • 系统软件找到PCIe设备后,向该设备的6个基址寄存器写入0xffffffff(全写1),然后回读;

  • 根据低4位判断该资源类型和属性(内存或IO空间、预留、地址空间长度);

  • 根据第4位后第一个1出现的位置,可以判断该资源大小,例如,如果在第11个bit(即bit10)才出现第一个1,说明该设备可以寻址10位(0~9)那么它的资源大小为2的10次方;

  • 知道各个设备资源大小后,操作系统通过特定机制,确保地址空间不冲突地映射PCI基地址,写入基地址寄存器,实现偏移。

所以,根据以上描述,可知,能够制定的最小资源大小为2的7次方即128字节。

类型1的头部:

0cddc8d9e898f7d6f29721cc4e22a153.jpeg

类型1只有两个基地址寄存器,因为它需要一部分空间来保存其他属性。这里特别指出三个总线编号,主总线编号表示该桥位于的总线编号,次总线编号表示该桥引出的总线编号,附属总线编号表示位于该桥次总线下的最大总线编号(因为次总线可能还有次次总线)。

操作系统启动过程中,必须完成所有PCI设备的配置,未成功配置的PCI设备不能正常工作,设计者通过在IO地址空间中保留两个32位的寄存器0xCF8和0xCFC来配置以上配置空间头中的寄存器。

那我们想要定位到一个设备的配置空间头的确定的一个寄存器应该如何去设计编号寻址呢?比如我们要找到上图中的10h基地址寄存器?

74ee2757ac70a80d311850663b6326f0.jpeg

我们如果能确定设备所在的总线号,在总线上的设备号,在设备中的功能号(逻辑设备号),以及寄存器编号和字节编号,就可以唯一确定出PCI配置空间里的任何一个字节。

因为我们每次都编辑4个字节,所以配置地址寄存器0xCF8的字节编号始终为00,这样,我们要修改或读出某一个寄存器的值,CPU就会在0xCF8中写入要修改或读出的地址,在0xCFC中写入或读出数据。

CPU对以上两个寄存器的访问,将被接收到的主桥转换为配置事务发到PCI总线上。主桥首先检查配置事务中的总线编号,若为0,则这个配置事务是针对主桥自身或者PCI总线0上的PCI设备的,主桥将配置事务转换为类型0的配置事务并在总线0上广播:

91811dd4bec1f89df93d2b72fb90e875.jpeg

通过这样的形式,CPU就可以完成对PCI设备的配置。这些配置在linux内核中driver/pci/access.c或者include/linux/pci.h中提供了相应的访问接口。

PCI总线扫描

我们先对上面的PCI配置空间的访问做一个总结:每个PCI桥设备在接收到配置事务后,判断配置目标是否在它的下游总线。若不是,则忽略该事务;若在它引出的总线上,则在这条局部总线上广播事务,否则向下游传播。而每个PCI事务决定是否认领这个配置事务,通常最终会有一个PCI设备会对这个配置事务做出响应。

有了上面的知识,下面我们从Android代码角度来分析一下这个过程。PCI总线扫描的目的的枚举系统中的PCI设备,配置其配置空间,在内核中建立描述符结构,包括为总线编号。

为总线编号是一个深度优先算法,有点类似于对树的前序遍历。

75168636def393d4d489032118f170e7.jpeg

bb706427c7231482f6b4504afe1dae39.jpeg

092802653531e5e8bfbe98ad07a862e4.jpeg

b407759f90c6038524302512b21b97d7.jpeg

第一轮在bus0上扫描到bridge1,进行bridge1配置(主0次1配不确定)并新定义bus1,按照深度优先的顺序,进入bus1扫描到bridge2(主1次2配不确定)定义bus2,进入bus2完成扫描,返回bridge2(主1次2配2),继续扫描bus1,发现bridge3(主1次3配不确定)定义bus3,扫描bus3将bus4和bridge4配置完成后完成bridge3配置(主1次3配4),最后配置bridge1(主0次1配4)。

pci总线扫描流程在subsys_initcall初始化阶段被调用,以函数pci_subsys_init为起点:

//common/arch/x86/pci/legacy.c:58
static int __init pci_subsys_init(void)
{
  /*
   * The init function returns an non zero value when
   * pci_legacy_init should be invoked.
   */
  if (x86_init.pci.init()) {
    if (pci_legacy_init()) {
      pr_info("PCI: System does not support PCI\n");
      return -ENODEV;
    }
  }


  pcibios_fixup_peer_bridges();
  x86_init.pci.init_irq();
  pcibios_init();


  return 0;
}
subsys_initcall(pci_subsys_init);

第8行进行了宏定义配置相关的回调检查,第9行为主要部分:

//common/arch/x86/pci/legacy.c:27
int __init pci_legacy_init(void)
{
  if (!raw_pci_ops)
    return 1;


  pr_info("PCI: Probing PCI hardware\n");
  pcibios_scan_root(0);
  return 0;
}

第4,5行用于检查原生的pci操作是否已经配置好,只有配置好了,才能去真正的配置PCI设备,接着进入第8行,从根总线开始扫描。下图是Linux2.6中的函数调用流程图,跟在现在的Android内核中有差异,但不多。

78e4c1d14f9ebf1bb9fd01c9c9e8db4d.jpeg

总体关键函数调用顺序如下:
pci_subsys_init
  pci_legacy_init
    pcibios_scan_root(0)
      pci_scan_root_bus
        pci_create_root_bus // 创建根总线
        pci_scan_child_bus // 探测当前总线设备以及子总线、子总线设备
          pci_scan_child_bus_extend  
            pci_scan_slot // 探测当前总线插槽
              pci_scan_single_device(bus, devfn) // 探测单功能设备
              pci_scan_single_device(bus, devfn + fn) // 探测多功能设备
                pci_scan_device // 通过配置空间定步枚举设备
                  pci_setup_device // 修改基址、数据寄存器,实际下发配置修改
                pci_device_add
                  device_initialize // 设备注册第一步
                  list_add_tail(&dev->bus_list, &bus->devices) // 将设备加入到bus设备链表
                  device_add // 设备注册第二步,完成驱动模型中设备注册流程 
            pci_scan_bridge_extend // 如果扫描到的PCI设备有桥,进入下一级
              pci_add_new_bus // 添加一条总线
                list_add_tail(&child->node, &parent->children) // 添加到子总线关系的链表
              pci_scan_child_bus  -> 递归
      pci_bus_add_devices
        pci_bus_add_device
          device_attach // 与driver配对,配对成功有uevent消息
        pci_bus_add_devices -> 递归

在扫描完成后,sysfs中就会建立起以下的子目录树。

091b17e0ab8def4baaf24144a767240f.jpeg

所以当总线扫描完成之后,有一个驱动的组合过程,这个过程才是我们想要去深究的。

PCI设备驱动编程模式

采用PCI接口的主机适配器所需要的驱动称为PCI设备驱动。它们是Linux驱动模型的一种应用,其次需要和PCI核心代码交互。

pci_driver在内核中的数据结构:

//common/include/linux/pci.h:906
struct pci_driver {
  struct list_head  node;
  const char    *name;
  const struct pci_device_id *id_table;  /* Must be non-NULL for probe to be called */
  int  (*probe)(struct pci_dev *dev, const struct pci_device_id *id);  /* New device inserted */
  void (*remove)(struct pci_dev *dev);  /* Device removed (NULL if not a hot-plug capable driver) */
  int  (*suspend)(struct pci_dev *dev, pm_message_t state);  /* Device suspended */
  int  (*resume)(struct pci_dev *dev);  /* Device woken up */
  void (*shutdown)(struct pci_dev *dev);
  int  (*sriov_configure)(struct pci_dev *dev, int num_vfs); /* On PF */
  int  (*sriov_set_msix_vec_count)(struct pci_dev *vf, int msix_vec_count); /* On PF */
  u32  (*sriov_get_vf_total_msix)(struct pci_dev *pf);
  const struct pci_error_handlers *err_handler;
  const struct attribute_group **groups;
  const struct attribute_group **dev_groups;
  struct device_driver  driver;
  struct pci_dynids  dynids;
};

如果某一设备的配置空间中读出的厂商ID和设备ID与驱动支持的ID表匹配,并且还没有被其他驱动绑定,则会回调驱动的probe函数进行探测,如果探测决定“认领”(绑定)这个设备,则probe函数返回0,否则返回负的错误码。

我们发现,virtio也有相关的pci驱动:

//common/drivers/virtio/virtio_pci_common.c:626
static struct pci_driver virtio_pci_driver = {
  .name    = "virtio-pci",
  .id_table  = virtio_pci_id_table,
  .probe    = virtio_pci_probe,
  .remove    = virtio_pci_remove,
#ifdef CONFIG_PM_SLEEP
  .driver.pm  = &virtio_pci_pm_ops,
#endif
  .sriov_configure = virtio_pci_sriov_configure,
};

该驱动的id_table定义为:

static const struct pci_device_id virtio_pci_id_table[] = {
  { PCI_DEVICE(PCI_VENDOR_ID_REDHAT_QUMRANET, PCI_ANY_ID) },
  { 0 }
};
即 0x1af4, ~0(-1)

我们输入

lspci -kn

查看pci相关信息可以发现:

1|emulator_x86_64:/ # lspci -kn                                                                                                                         
00:17.0 0980: 1af4:1052  (rev 01) virtio-pci
00:08.0 0200: 1af4:1000  (rev 00) virtio-pci
00:0d.0 0380: 1af4:1050  (rev 01) virtio-pci
00:10.0 0980: 1af4:1052  (rev 01) virtio-pci
00:13.0 0980: 1af4:1052  (rev 01) virtio-pci
00:04.0 0100: 1af4:1001  (rev 00) virtio-pci
00:16.0 0980: 1af4:1052  (rev 01) virtio-pci
00:1b.0 0780: 1af4:1012  (rev 00) virtio-pci
00:07.0 0100: 1af4:1001  (rev 00) virtio-pci
00:0c.0 00ff: 1af4:1005  (rev 00) virtio-pci
00:19.0 0900: 1af4:1052  (rev 01) virtio-pci
00:0f.0 0980: 1af4:1052  (rev 01) virtio-pci
00:12.0 0980: 1af4:1052  (rev 01) virtio-pci
00:03.0 0100: 1af4:1001  (rev 00) virtio-pci
00:15.0 0980: 1af4:1052  (rev 01) virtio-pci
00:1a.0 0200: 1af4:100a  (rev 00) virtio-pci
00:06.0 0100: 1af4:1001  (rev 00) virtio-pci
00:0b.0 00ff: 607d:f153  (rev 01) goldfish_address_space
00:18.0 0980: 1af4:1052  (rev 01) virtio-pci
00:09.0 0780: 1af4:1003  (rev 00) virtio-pci
00:0e.0 0980: 1af4:1052  (rev 01) virtio-pci
00:11.0 0980: 1af4:1052  (rev 01) virtio-pci
00:02.0 0401: 1af4:1019  (rev 00) virtio-pci
00:14.0 0980: 1af4:1052  (rev 01) virtio-pci
00:05.0 0100: 1af4:1001  (rev 00) virtio-pci
00:0a.0 0780: 1af4:1003  (rev 00) virtio-pci

咱们模拟器的大部分设备都与virtio设备驱动所绑定。

再看virtio-pci所定义的probe函数:

//common/drivers/virtio/virtio_pci_common.c:512
static int virtio_pci_probe(struct pci_dev *pci_dev,
          const struct pci_device_id *id)
{
  struct virtio_pci_device *vp_dev, *reg_dev = NULL;
  int rc;


  /* allocate our structure and fill it out */
  vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);
  if (!vp_dev)
    return -ENOMEM;


  pci_set_drvdata(pci_dev, vp_dev);
  vp_dev->vdev.dev.parent = &pci_dev->dev;
  vp_dev->vdev.dev.release = virtio_pci_release_dev;
  vp_dev->pci_dev = pci_dev;
  INIT_LIST_HEAD(&vp_dev->virtqueues);
  spin_lock_init(&vp_dev->lock);


  /* enable the device */
  rc = pci_enable_device(pci_dev);
  if (rc)
    goto err_enable_device;


  if (force_legacy) {
    rc = virtio_pci_legacy_probe(vp_dev);
    /* Also try modern mode if we can't map BAR0 (no IO space). */
    if (rc == -ENODEV || rc == -ENOMEM)
      rc = virtio_pci_modern_probe(vp_dev);
    if (rc)
      goto err_probe;
  } else {
    rc = virtio_pci_modern_probe(vp_dev);
    if (rc == -ENODEV)
      rc = virtio_pci_legacy_probe(vp_dev);
    if (rc)
      goto err_probe;
  }


  pci_set_master(pci_dev);


  rc = register_virtio_device(&vp_dev->vdev);
  reg_dev = vp_dev;
  if (rc)
    goto err_register;


  return 0;


err_register:
  if (vp_dev->ioaddr)
       virtio_pci_legacy_remove(vp_dev);
  else
       virtio_pci_modern_remove(vp_dev);
err_probe:
  pci_disable_device(pci_dev);
err_enable_device:
  if (reg_dev)
    put_device(&vp_dev->vdev.dev);
  else
    kfree(vp_dev);
  return rc;
}

这个函数调用了:

//common/drivers/virtio/virtio.c:386
int register_virtio_device(struct virtio_device *dev)
{
  int err;


  dev->dev.bus = &virtio_bus;
  device_initialize(&dev->dev);


  /* Assign a unique device index and hence name. */
  err = ida_simple_get(&virtio_index_ida, 0, 0, GFP_KERNEL);
  if (err < 0)
    goto out;


  dev->index = err;
  dev_set_name(&dev->dev, "virtio%u", dev->index);


  err = virtio_device_of_init(dev);
  if (err)
    goto out_ida_remove;


  spin_lock_init(&dev->config_lock);
  dev->config_enabled = false;
  dev->config_change_pending = false;


  /* We always start by resetting the device, in case a previous
   * driver messed it up.  This also tests that code path a little. */
  dev->config->reset(dev);


  /* Acknowledge that we've seen the device. */
  virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);


  INIT_LIST_HEAD(&dev->vqs);
  spin_lock_init(&dev->vqs_list_lock);


  /*
   * device_add() causes the bus infrastructure to look for a matching
   * driver.
   */
  err = device_add(&dev->dev);
  if (err)
    goto out_of_node_put;


  return 0;


out_of_node_put:
  of_node_put(dev->dev.of_node);
out_ida_remove:
  ida_simple_remove(&virtio_index_ida, dev->index);
out:
  virtio_add_status(dev, VIRTIO_CONFIG_S_FAILED);
  return err;
}

实际上,这个函数就完成了将设备注册到virtio总线的操作,接着就快要和我们的上文链接起来了!那链接还差一环在哪呢?

还记得吗,我们virtio-blk的id_table可不是1af4而是2,说明,这当中依然存在某种转换,于是在我的不懈printk调试法寻找下原来猫腻藏在这个函数中:

//common/drivers/virtio/virtio_pci_legacy.c:212
int virtio_pci_legacy_probe(struct virtio_pci_device *vp_dev)
{
  struct pci_dev *pci_dev = vp_dev->pci_dev;
  int rc;


  /* We only own devices >= 0x1000 and <= 0x103f: leave the rest. */
  if (pci_dev->device < 0x1000 || pci_dev->device > 0x103f)
    return -ENODEV;


  if (pci_dev->revision != VIRTIO_PCI_ABI_VERSION) {
    printk(KERN_ERR "virtio_pci: expected ABI version %d, got %d\n",
           VIRTIO_PCI_ABI_VERSION, pci_dev->revision);
    return -ENODEV;
  }


  rc = dma_set_mask(&pci_dev->dev, DMA_BIT_MASK(64));
  if (rc) {
    rc = dma_set_mask_and_coherent(&pci_dev->dev, DMA_BIT_MASK(32));
  } else {
    /*
     * The virtio ring base address is expressed as a 32-bit PFN,
     * with a page size of 1 << VIRTIO_PCI_QUEUE_ADDR_SHIFT.
     */
    dma_set_coherent_mask(&pci_dev->dev,
        DMA_BIT_MASK(32 + VIRTIO_PCI_QUEUE_ADDR_SHIFT));
  }


  if (rc)
    dev_warn(&pci_dev->dev, "Failed to enable 64-bit or 32-bit DMA.  Trying to continue, but this might not work.\n");


  rc = pci_request_region(pci_dev, 0, "virtio-pci-legacy");
  if (rc)
    return rc;


  rc = -ENOMEM;
  vp_dev->ioaddr = pci_iomap(pci_dev, 0, 0);
  if (!vp_dev->ioaddr)
    goto err_iomap;


  vp_dev->isr = vp_dev->ioaddr + VIRTIO_PCI_ISR;


  /* we use the subsystem vendor/device id as the virtio vendor/device
   * id.  this allows us to use the same PCI vendor/device id for all
   * virtio devices and to identify the particular virtio driver by
   * the subsystem ids */
  vp_dev->vdev.id.vendor = pci_dev->subsystem_vendor;
  vp_dev->vdev.id.device = pci_dev->subsystem_device;


  vp_dev->vdev.config = &virtio_pci_config_ops;


  vp_dev->config_vector = vp_config_vector;
  vp_dev->setup_vq = setup_vq;
  vp_dev->del_vq = del_vq;


  return 0;


err_iomap:
  pci_release_region(pci_dev, 0);
  return rc;
}

在上面的47行可以看到,id被定义为子系统id,随后virtio-bus使用新的id去进行匹配。那么这两个id又是从哪里来的呢?

ae6acde3d90ce499cadb3e83faa81437.png

所以,我们再将之前的data分区来龙去脉完善以下,就可以得出这样的关系图:

872e1c3eb1b716c262799faa8807401a.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值