首页
社区
课程
招聘
PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)
发表于: 2023-7-13 19:07 17353

PCI总线初始化过程(linux-2.4.0内核中的pci_init()函数分析)

2023-7-13 19:07
17353

  PCI设备(包括PCI桥),与主板之间的连接关系为:设备-[(PCI次层总线)-(PCI-PCI桥)]*-{(PCI主总线)-(宿主-PCI桥)}-主板(*表示0或多层)。总之,所有PCI设备,都直接或间接的连接到了主板上,并且每个PCI设备的内部,必然存在一些存储单元,服务于设备功能,包括寄存器、RAM,甚至ROM,本文将它们称为”功能存储区间”,以便与”配置寄存器组”(用于设备配置的存储区间)区分。


  “配置寄存器组”,一方面提供设备的出厂信息,另一方面,用于系统软件对设备进行配置。其中,前64字节,必须按照PCI标准使用,称为”配置寄存器组”的头部,头部又分为”0型”、”1型”和”2型”,不过,不管哪种头部,前16字节的格式都是一样的(头部类型就包含在其中),只是后48字节格式不同。头部之后的192字节,由具体设备自行使用,如果不需要,没有也是可以的。

  根据PCI规范说明,可以通过”0xCF8”和”0xCFC”两个32位寄存器,读写PCI设备的”配置寄存器组”(对于内核开发,直接按照规范使用即可,不需要关心PCI规范的硬件实现,比如为什么是”0xCF8”和”0xCFC”这两个I/O端口号,以及根据其中内容寻找并读写PCI设备的具体过程)

  地址寄存器:用于指定配置寄存器地址;
  数据寄存器:对于读操作,用于从中获取读取结果,对于写操作,用于往里填充写入内容。
  由于目标地址包含总线号和设备号,所以读写前,所有PCI总线和设备,都要有自己的编号。其中,对于总线编号, PCI桥提供了PCI_PRIMARY_BUS和PCI_SECONDARY_BUS配置寄存器,由软件进行设置,而设备编号,却没有对应的配置寄存器。个人理解,这是因为,总线编号,受总线在树中的位置决定,是PCI厂商无法预测的,而设备编号,只要能够表明设备在单条总线上的局部位置即可,所以可以与PCI插槽位置等效,这在PCI总线出厂时就能确定,所以不需要软件执行枚举编号。
  至此,还可以看出PCI设备中,配置寄存器与”功能存储区间”读写渠道的区别:通过”0xCF8”和”0xCFC”寄存器读写”配置寄存器”->通过”配置寄存器”,配置”功能存储区间”地址->直接根据配置地址访问”功能存储区间”。

  BIOS程序烧刻在ROM(只读/不挥发内存),其中,有些厂商的主板,BIOS提供了PCI操作功能,就称为”PCI BIOS”。对于CPU来说,主板加电后,BIOS程序立即就可以执行,而内核程序,必须先由BIOS从MBR(主引导扇区),加载引导程序,再由引导程序加载到RAM(内存),才可以执行。

  “PCI BIOS”包含的很多PCI操作功能,在加电自检阶段就会被执行,本意是为内核省事情。但是,由于一些厂商的BIOS,存在这样或那样的问题,以及嵌入式主板中,甚至没有BIOS,所以Linux内核自己实现了一套独立的PCI操作接口,可以完全不依赖BIOS,不过对于已经在自检阶段完成的操作,Linux内核会尽量保持BIOS的设置,有些情况,会调用自己实现的接口,对其进行补充或修正,有些情况,甚至不再调用自己实现的接口,完全接受BIOS的处理结果。

  “PCI BIOS”还有一些PCI操作功能,作为接口提供给内核程序使用,当内核希望自己完成一些PCI操作,又不想过分关注PCI硬件spec时,就可以通过lcall指令,直接调用“PCI BIOS”提供的功能。不过,同样为了避免BIOS中的各种问题,以及没有BIOS的情况,Linux内核对这些接口,也自己实现了一份,并且,Linux内核中的PCI操作函数,分别通过CONFIG_PCI_BIOS和CONFIG_PCI_DIRECT宏,选择调用哪套接口。显然,这两个宏至少要有一个是打开的,并且,两者互不相斥,都打开时,内核通常优先使用自己的接口,或用自己的接口,对BIOS接口的操作结果进行修正。

  《Linux内核源代码情景分析》,p1025:

  《Linux内核源代码情景分析》,p1030:

  对于书上的说法,我一开始误解为:不管什么情况,即使没有BIOS,内核也能自己完成PCI操作,而是否依赖,可以通过CONFIG_PCI_BIOS宏,进行控制。
  带着这个误解,分析完i386架构下的pci_init()函数后,我产生了一个困惑:没有看到设置PCI设备PCI_IO_BASE和PCI_MEMORY_BASE寄存器的代码。然而,为各个PCI设备配置内存和I/O空间,可是PCI初始化的根本目标之一,所以既然内核没有设置,那就一定依赖了BIOS的设置,这就跟我的误解冲突了。
  为了重新理解书上的说法,我首先想到的是,不定义CONFIG_PCI_BIOS宏,只会影响内核代码的编译结果,关闭内核函数对BIOS接口的调用,并不能影响加电自检时BIOS执行哪些PCI操作,因为BIOS程序在主板出厂时,就已经固定了。
  i386架构下,内核代码所有对BIOS接口的调用,都在pcibios_init()或它调用的函数中:

  另外,为防止看漏了,我对内核设置PCI_MEMORY_BASE寄存器的语句,进行了搜索:

  结果发现,i386架构下,内核代码确实没有设置过PCI_IO_BASE和PCI_MEMORY_BASE寄存器,但在arm架构下(arch/arm/kernel/bios32.c),是由内核自己配置:

  所以,结合以上两点可知,判断pcibios_init()是否依赖BIOS,还要看它是否依赖BIOS在加电自检阶段的操作结果,并且i386架构下的pcibios_init()函数,确实是依赖的。
  那就是说,虽然Linux内核独立实现了一套PCI操作接口,使得Linux内核可以完全不依赖BIOS,实现任何PCI操作,不过,这只是为最终的PCI操作是否完全不依赖BIOS,提供了选择,比如arm CPU常用于嵌入式系统,通常根本没有BIOS,所以arm架构下实现PCI操作,必须选择完全使用内核接口,而对于i386 CPU,可以确定加电自检阶段,有些操作必然已由BIOS完成,并且完成的没问题时,就可以选择仍然”依赖”BIOS(可以依赖时,不依赖白不依赖)。
  搞清楚这一点很重要,要不然分析代码时,就可能会期待在pci_init()函数中,看到根据所有PCI设备汇总信息,分配PCI地址的过程,却看不到,有些配置也会让人觉得”来历不明”,还没见到设置,内核就认为已经设置了。

8
 |- 0x02:网络设备
 |  |- 8== 0 && PCI_CLASS_PROG == 0
 |     |- Ethernet网卡
 |- 0x07:简单通信控制器
 |  |- 8== 0x01:并行口
 |     |- PCI_CLASS_PROG
 |        |- 0:单向
 |        |- 1:双向
 |        |- 2:符合ECP 1.0规定
 |- 0x06:PCI桥
    |- 8
        |- 0x00"宿主-PCI"
        |- 0x01"PCI-ISA"
        |- 0x04"PCI-PCI"
        |- 0x07"PCI-CARDBUS"
8
 |- 0x02:网络设备
 |  |- 8== 0 && PCI_CLASS_PROG == 0
 |     |- Ethernet网卡
 |- 0x07:简单通信控制器
 |  |- 8== 0x01:并行口
 |     |- PCI_CLASS_PROG
 |        |- 0:单向
 |        |- 1:双向
 |        |- 2:符合ECP 1.0规定
 |- 0x06:PCI桥
    |- 8
        |- 0x00"宿主-PCI"
        |- 0x01"PCI-ISA"
        |- 0x04"PCI-PCI"
        |- 0x07"PCI-CARDBUS"
pcibios_init()
 |- pci_find_bios()                          // #ifdef CONFIG_PCI_BIOS
 |   |- check_pcibios()
 |   |   |- lcall &pci_indirect              // 通过call指令,进入BIOS调用入口
 |   |- return &pci_bios_access
 |       |- pci_bios_OP_config_SIZE()        // OP: read/write; SISE: byte/word/dword
 |           |- lcall &pci_indirect
 |- pcibios_irq_init()
 |   |- pcibios_get_irq_routing_table()      // #ifdef CONFIG_PCI_BIOS
 |   |   |- lcall &pci_indirect
 |   |- pirq_find_router()
 |       |- pirq_router = &pirq_bios_router  // #ifdef CONFIG_PCI_BIOS
 |           |- pcibios_set_irq_routing()
 |               |- lcall &pci_indirect
 |- pcibios_sort()                           // #ifdef CONFIG_PCI_BIOS
     |- pci_bios_find_device()
     |- lcall &pci_indirect
pcibios_init()
 |- pci_find_bios()                          // #ifdef CONFIG_PCI_BIOS
 |   |- check_pcibios()
 |   |   |- lcall &pci_indirect              // 通过call指令,进入BIOS调用入口
 |   |- return &pci_bios_access
 |       |- pci_bios_OP_config_SIZE()        // OP: read/write; SISE: byte/word/dword
 |           |- lcall &pci_indirect
 |- pcibios_irq_init()
 |   |- pcibios_get_irq_routing_table()      // #ifdef CONFIG_PCI_BIOS
 |   |   |- lcall &pci_indirect
 |   |- pirq_find_router()
 |       |- pirq_router = &pirq_bios_router  // #ifdef CONFIG_PCI_BIOS
 |           |- pcibios_set_irq_routing()
 |               |- lcall &pci_indirect
 |- pcibios_sort()                           // #ifdef CONFIG_PCI_BIOS
     |- pci_bios_find_device()
     |- lcall &pci_indirect
pcibios_init()
 |- pci_assign_unassigned_resources()
     |- pbus_assign_resources()
         |- pci_setup_bridge()
             |- pci_write_config_dword(bridge, PCI_IO_BASE, l)
             |- pci_write_config_dword(bridge, PCI_MEMORY_BASE, l)
pcibios_init()
 |- pci_assign_unassigned_resources()
     |- pbus_assign_resources()
         |- pci_setup_bridge()
             |- pci_write_config_dword(bridge, PCI_IO_BASE, l)
             |- pci_write_config_dword(bridge, PCI_MEMORY_BASE, l)
// 声明:以下分析的代码,是针对i386 CPU的实现!!
pci_init()
 |
 |- pcibios_init()  // arch/i386/kernel/pci-pc.c !!
 |   |              // 早期,该函数完全通过调用BIOS接口实现,所以函数名中包含了"bios"
 |   |              // 后来,该函数演变为,可以通过CONFIG_PCI_DIRECT和CONFIG_PCI_BIOS宏,选择调用BIOS接口,还是调用内核自己实现的接口
 |   |              // 而且两者不互斥,如果2个宏都打开,内核通常优先使用自己的接口,或用自己的接口,对BIOS接口的操作结果进行修正
 |   |
 |   |  // 获取BIOS提供的PCI操作接口 (#ifdef CONFIG_PCI_BIOS)
 |   |- pci_root_ops = pci_find_bios()
 |   |   |
 |   |   |  // 0xe0000~0xffff0为BIOS占用的物理内存
 |   |   |- for (check = (union bios32 *) __va(0xe0000); check <= (union bios32 *) __va(0xffff0); ++check)
 |   |       |
 |   |       |  // 每个bios32对象,都包含16字节头部,其中包含signature和checksum字段
 |   |       |- if (check->fields.signature != BIOS32_SIGNATURE)
 |   |       |   |- continue
 |   |       |
 |   |       |  // 构造bios32对象时,通过设置checksum字段值,保证了整个对象占用的字节相加为0
 |   |       |- for (i = 0; i < length ; ++i)
 |   |       |   |- sum += check->chars[i]
 |   |       |- if (sum != 0)
 |   |       |   |- continue
 |   |       |
 |   |       |  // 进一步检查
 |   |       |- if (check_pcibios())
 |   |           |- if ((pcibios_entry = bios32_service(PCI_SERVICE)))
 |   |           |   |- pci_indirect.address = pcibios_entry + PAGE_OFFSET  // 设置BIOS的PCI服务接口的段内偏移
 |   |           |   |- ...                                                 // 设置参数:PCIBIOS_PCI_BIOS_PRESENT
 |   |           |   |- lcall &pci_indirect                                 // 调用接口
 |   |           |             |- pci_indirect = { 0, __KERNEL_CS }         // 初始化时,已将段基址设置为__KERNEL_CS
 |   |           |- return &pci_bios_access
 |   |
 |   |  // 获取内核自己实现的PCI操作接口 (#ifdef CONFIG_PCI_DIRECT)
 |   |- pci_root_ops = pci_check_direct()
 |   |   |
 |   |   |  // 探测主总线是否为PCI总线
 |   |   |- if (pci_probe & PCI_PROBE_CONF1)
 |   |   |   |- outb (0x01, 0xCFB)            // 32位地址寄存器最高字节0xCFB,称为"PMC寄存器",它的bit1~7保留,bit0用于选择"PCI配置访问机制"
 |   |   |   |                                // 参考:https://html.datasheetbank.com/datasheet-html/251876/Intel/34page/290479-004.html (PCI datasheet, page 34)
 |   |   |   |                                //       bit0=0 | PCI配置访问机制0 | 2型PCI设备
 |   |   |   |                                //      --------+------------------+------------
 |   |   |   |                                //       bit0=1 | PCI配置访问机制1 | 1型PCI设备
 |   |   |   |- tmp = inl (0xCF8)             // 从地址寄存器,读取原始值 (对于常规存储单元,先向a写入1(a=1),再读取a(b=a),结果会为1,但是PCI配置寄存器往往不是这样)
 |   |   |   |- outl (0x80000000, 0xCF8)      // 向地址寄存器,写入0x80000000
 |   |   |   |                                // 至于为什么非要写入0x80000000进行检查,估计也是PCI datasheet规定,后续分析不会过于纠结这种细节,建议尝试根据PCI datasheet查找
 |   |   |   |
 |   |   |   |  // 读到的还是0x80000000(),并且通过pci_sanity_check()检查
 |   |   |   |- if (inl (0xCF8) == 0x80000000 && pci_sanity_check(&pci_direct_conf1))
 |   |   |   |   |                                |
 |   |   |   |   |                                |- for(dev.devfn=0; dev.devfn < 0x100; dev.devfn++// 一条PCI总线,最多可以有256个逻辑设备
 |   |   |   |   |                                    |- if ((!o->read_word(&dev, PCI_CLASS_DEVICE, &x) &&
 |   |   |   |   |                                        |   (x == PCI_CLASS_BRIDGE_HOST || x == PCI_CLASS_DISPLAY_VGA)) ||
 |   |   |   |   |                                        |  (!o->read_word(&dev, PCI_VENDOR_ID, &x) &&
 |   |   |   |   |                                        |   (x == PCI_VENDOR_ID_INTEL || x == PCI_VENDOR_ID_COMPAQ)))  // 连接了PCI_CLASS_BRIDGE_HOST或PCI_CLASS_DISPLAY_VGA设备,认为是PCI总线
 |   |   |   |   |                                        |                                                              // 连接的设备由Intel或Compaq厂商制造,也认为是PCI总线
 |   |   |   |   |                                        |- return 1
 |   |   |   |   |
 |   |   |   |   |- outl (tmp, 0xCF8)                      // 为地址寄存器恢复原始值
 |   |   |   |   |- request_region(0xCF8, 8, "PCI conf1"// 在内核I/O设备资源树增加一个节点,用于记录"宿主-PCI桥"占用的I/O地址区间
 |   |   |   |   |- return &pci_direct_conf1               // 返回pci_direct_conf1对象地址,包含read_byte()、write_byte()等函数/PCI操作列表
 |   |   |   |
 |   |   |   |- outl (tmp, 0xCF8)             // 为地址寄存器恢复原始值
 |   |   |
 |   |   |  // 探测主总线是否为ISA总线 (现已不用)
 |   |   |- if (pci_probe & PCI_PROBE_CONF2)
 |   |       |- outb (0x00, 0xCFB)            // PMC寄存器=0,选择机制0,访问2型PCI设备的配置寄存器
 |   |       |- ...
 |   |       |- return &pci_direct_conf2
 |   |
 |   |  // 扫描主总线,探测与枚举连接在主总线上的PCI设备,如果设备为"PCI-PCI"桥,则递归扫描其连接的次层总线
 |   |  // 对于i386 CPU,必定已由BIOS在加电自检阶段枚举过一遍了,pci_scan_bus()相当于直接读取BIOS的设置成果,并分配管理对象进行记录
 |   |- pci_root_bus = pci_scan_bus(0, pci_root_ops, NULL)
 |   |   |
 |   |   |  // 分配pci_bus对象,用于记录总线信息
 |   |   |- struct pci_bus *b = pci_alloc_primary_bus()
 |   |   |   |- if (pci_bus_exists(&pci_root_buses, bus))  // 检查pci_root_buses全局链表,是否已经存在总线号为bus的pci_bus对象
 |   |   |   |   |- return NULL
 |   |   |   |- b = pci_alloc_bus()                        // 分配pci_bus对象
 |   |   |   |- list_add_tail(&b->node, &pci_root_buses)   // 添加到pci_root_buses全局链表
 |   |   |   |- b->number = b->secondary = bus             // 记录总线号
 |   |   |   |                                             // number表示当前总线的编号
 |   |   |   |                                             // primary表示当前总线的上层总线编号
 |   |   |   |                                             // secondary表示总线所在PCI桥的次层总线编号(也就是当前总线自己),所以总是等于number (额外搞个成员变量,估计是为了提高代码可读性)
 |   |   |   |  /* pci_dev对象的resource[DEVICE_COUNT_RESOURCE]:
 |   |   |   |      PCI设备本身占用:
 |   |   |   |       resource[0~5]:设备上可能有的内存地址区间
 |   |   |   |       resource[6]:设备上可能有的扩充ROM区间
 |   |   |   |      PCI桥是一种特殊的PCI设备,额外包含次层总线占用的4个区间:
 |   |   |   |       resource[7]:I/O地址窗口
 |   |   |   |       resource[8]:内存地址窗口
 |   |   |   |       resource[9]:"可预取"内存地址窗口
 |   |   |   |       resource[10]:扩充ROM区间窗口 */
 |   |   |   |  // pci_bus对象的*resource[4],分别指向所在PCI桥的&resource[7~10]
 |   |   |   |- b->resource[0] = &ioport_resource   // resource[0]:可由当前总线使用的I/O地址区间 (由于是主总线,所以指向整个&ioport_resource)
 |   |   |   |- b->resource[1] = &iomem_resource    // resource[1]:可由当前总线使用的内存地址区间 (由于是主总线,所以指向整个&iomem_resource)
 |   |   |
 |   |   |  // 开始扫描
 |   |   |- if (b)
 |   |       |- b->ops = ops  // 次层总线的ops,同样设置为pci_root_ops
 |   |       |- b->subordinate = pci_do_scan_bus(b)  // 逐个发现连接在总线上的设备,为其建立pci_dev对象
 |   |           |- for (devfn = 0; devfn < 0x100; devfn += 8// 每次扫描1个PCI插槽
 |   |           |   |- dev0.devfn = devfn
 |   |           |   |- pci_scan_slot(&dev0)
 |   |           |       |- for (func = 0; func < 8; func++, temp->devfn++// 每个PCI设备,最多可以有8个功能,相当于8个逻辑设备 (逻辑设备号 = 5位设备号+3位功能号)
 |   |           |           |
 |   |           |           |- if (func && !is_multi)  // 0功能号,只在多功能设备中才需要处理
 |   |           |           |   |- continue            // 疑问:这里为什么不直接break??
 |   |           |           |
 |   |           |           |  // 读取"配置寄存器组"头部中的PCI_HEADER_TYPE字段,根据它的最高位是否为1,判断设备是否为单功能,从而决定下一轮循环是否需要继续
 |   |           |           |- pci_read_config_byte(temp, PCI_HEADER_TYPE, &hdr_type)  // pci_read_config_byte()函数,根据PCI_OP(read, byte, u8 *)宏展开定义
 |   |           |           |   |- dev->bus->ops->read_byte()  // ops通常为pci_direct_conf1 (见pcibios_init()入口,对pci_root_ops的设置)
 |   |           |           |       |
 |   |           |           |       |- pci_conf1_read_config_byte()            // 如果ops=&pci_direct_conf1
 |   |           |           |       '   |- outl(CONFIG_CMD(dev,where), 0xCF8// 按照PCI规范,向地址寄存器(0xCF8),写入"总线号,逻辑设备号,寄存器偏移(where)"
 |   |           |           |       '   |        |- (0x80000000 | (dev->bus->number << 16) | (dev->devfn << 8) | (where & ~3))
 |   |           |           |       '   |- *value = inb(0xCFC + (where&2))     // 从数据寄存器(0xCFC),读取设备的PCI_HEADER_TYPE寄存器值
 |   |           |           |       '
 |   |           |           |       '- pci_bios_read_config_byte()             // 如果ops=&pci_bios_access
 |   |           |           |           |- ...                                 // 设置参数:PCIBIOS_READ_CONFIG_WORD
 |   |           |           |           |- lcall &pci_indirect                 // 调用BIOS的PCI服务接口 (段基址、偏移,已分别由初始化和check_pcibios()函数设置)
 |   |           |           |
 |   |           |           |- temp->hdr_type = hdr_type & 0x7f  // 7位代表设备类型:
 |   |           |           |                                    // PCI_HEADER_TYPE_NORMAL(普通PCI设备)、PCI_HEADER_TYPE_BRIDGE(PCI-PCI桥)、PCI_HEADER_TYPE_CARDBUS(PCI-CardBus桥)
 |   |           |           |
 |   |           |           |- dev = pci_scan_device(temp)       // 读取逻辑设备的出厂信息
 |   |           |           |   |                                // "配置寄存器组",一方面提供软件对设备的配置界面,另一方面,携带设备的出厂信息
 |   |           |           |   |  // 读取"配置寄存器组"头部中的PCI_HEADER_TYPE字段,低16位为厂商编号,高16位为设备编号
 |   |           |           |   |- pci_read_config_dword(temp, PCI_VENDOR_ID, &l)
 |   |           |           |   |- if (l == 0xffffffff || l == 0x00000000 || l == 0x0000ffff || l == 0xffff0000// 0或全1,都为无效的厂商编号、备编号
 |   |           |           |   |   |- return NULL
 |   |           |           |   |- dev = kmalloc(sizeof(*dev), GFP_KERNEL)  // 分配pci_dev对象
 |   |           |           |   |- ...                                      // 初始化pci_dev对象,设置厂商编号、设备编号
 |   |           |           |   |- pci_setup_device(dev)                    // 根据设备类型,进一步填充pci_dev对象
 |   |           |           |       |
 |   |           |           |       |  // 读取"配置寄存器组"头部中的PCI_CLASS_REVISION字段
 |   |           |           |       |- pci_read_config_dword(dev, PCI_CLASS_REVISION, &class// 相比表示大类的hdr_type,class对设备类型进一步细分,其中包含:
 |   |           |           |       |                                                          // PCI_CLASS_DEVICE(高16位)、PCI_CLASS_PROG(次低8位)、PCI_REVISION_ID(低8位)
 |   |           |           |       |- switch (dev->hdr_type)
 |   |           |           |           |
 |   |           |           |           |- case PCI_HEADER_TYPE_NORMAL      // 普通PCI设备
 |   |           |           |           |   |
 |   |           |           |           |   |- if (class == PCI_CLASS_BRIDGE_PCI)
 |   |           |           |           |   |   |- goto bad                 // 可以看出,只要不是"PCI-PCI"桥,都属于0型设备
 |   |           |           |           |   |
 |   |           |           |           |   |  // 读取"配置寄存器组"头部中的PCI_INTERRUPT_PIN和PCI_INTERRUPT_LINE字段
 |   |           |           |           |   |- pci_read_irq(dev)
 |   |           |           |           |   |   |  // PCI_INTERRUPT_PIN字段,表示设备与PCI插槽之间,中断请求信号线的连接关系 (由硬件决定,只读)
 |   |           |           |           |   |   |  // 0:表示设备不产生中断请求; 1~4:分别表示设备与PCI插槽的INTA~INTD中断请求信号线连接
 |   |           |           |           |   |   |- pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &irq)
 |   |           |           |           |   |   |- if (irq)
 |   |           |           |           |   |   |   |  // PCI_INTERRUPT_LINE字段,表示设备经过PCI插槽,最终连接了中断控制器的哪根中断请求信号线 (由软件设置)
 |   |           |           |           |   |   |   |  // PCI_INTERRUPT_LINE字段只起记录作用,不起控制作用,修改其值,并不会修改连接关系,不过连接关系通常也可以由软件设置
 |   |           |           |           |   |   |   |  // 如果这时读取的irq不为0,一定是因为已由BIOS设置,内核后续不会对其进行修改
 |   |           |           |           |   |   |   |- pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &irq)
 |   |           |           |           |   |   |
 |   |           |           |           |   |   |- dev->irq = irq  // 记录到pci_dev对象 (对于irq为0的情况,会在后续调用pcibios_lookup_irq()函数时,进行处理)
 |   |           |           |           |   |
 |   |           |           |           |   |  // 读取"配置寄存器组"头部中的PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5和PCI_ROM_ADDRESS字段
 |   |           |           |           |   |- pci_read_bases(dev, 6, PCI_ROM_ADDRESS)
 |   |           |           |           |   |   |
 |   |           |           |           |   |   |  // PCI设备中,通常包含寄存器或RAM"(可挥发)功能存储区间"(最多6个),PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5寄存器,用于记录它们的出厂信息,以及配置它们的映射地址
 |   |           |           |           |   |   |- for(pos=0; pos<howmany; pos = next)
 |   |           |           |           |   |   |   |
 |   |           |           |           |   |   |   |  // 对于已由BIOS配置过的寄存器,内核后续不会对其进行修改,只会在检查到配置错误的情况下,对其进行修正
 |   |           |           |           |   |   |   |- reg = PCI_BASE_ADDRESS_0 + (pos << 2) // PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5寄存器,长度都为4字节
 |   |           |           |           |   |   |   |- pci_read_config_dword(dev, reg, &l)   // 读取区间起始地址
 |   |           |           |           |   |   |   |- pci_write_config_dword(dev, reg, ~0// 写入全1 (根据PCI规范)
 |   |           |           |           |   |   |   |- pci_read_config_dword(dev, reg, &sz)  // 读取区间大小
 |   |           |           |           |   |   |   |- pci_write_config_dword(dev, reg, l)   // 恢复原始值
 |   |           |           |           |   |   |   |
 |   |           |           |           |   |   |   |  // 起始地址,bit0=0,表示内存地址区间,bit0=1表示I/O地址区间
 |   |           |           |           |   |   |   |  /* 区间类型由映射到什么区间决定,而跟存储单元是什么介质(内存/寄存器),以及所在的硬件设备无关
 |   |           |           |           |   |   |   |     Linux内核通常优先选择使用内存空间:
 |   |           |           |           |   |   |   |     1. 有些CPU没有I/O指令,因而没有I/O空间,寄存器也就都在存储器地址空间中
 |   |           |           |           |   |   |   |     2. 有些CPU有I/O指令,但是指令的寻址空间很小,比如i386 CPU的I/O指令,操作数最多16位,即I/O空间为64KB,非常拥挤
 |   |           |           |           |   |   |   |     3. C语言没有访问I/O地址空间的语言成分,需要调用汇编子过程,不方便而且效率也稍低 */
 |   |           |           |           |   |   |   |- if ((l & PCI_BASE_ADDRESS_SPACE) == PCI_BASE_ADDRESS_SPACE_MEMORY)
 |   |           |           |           |   |   |   |   |
 |   |           |           |           |   |   |   |   |  // 内存区间,起始地址低4位为控制信息:
 |   |           |           |           |   |   |   |   |  // bit0:区间类型 (0: 内存区间; 1: I/O区间)
 |   |           |           |           |   |   |   |   |  // bit1:区间大小是否超过1MB (0: 未超过; 1: 超过)
 |   |           |           |           |   |   |   |   |  // bit2: 区间地址位数 (0: 32位; 1: 64位)
 |   |           |           |           |   |   |   |   |  // bit3: 区间是否"可预取" (0: 否; 1:是)
 |   |           |           |           |   |   |   |   |  //       对于普通的内存单元,读操作是不会修改其内容的,但是如果内存区间是由寄存器区间映射的,内容就有可能被读操作修改:
 |   |           |           |           |   |   |   |   |  //       1. 比如FIFO寄存器,读走队列头的元素后,下一个元素就成为首个元素,存入FIFO寄存器了
 |   |           |           |           |   |   |   |   |  //       2. 有些状态寄存器,状态被读出之后,寄存器就会硬件自动清0
 |   |           |           |           |   |   |   |   |  //       "预读"是指,程序在读第N个单元时,CPU会把第N+1个单元也读入缓存:
 |   |           |           |           |   |   |   |   |  //       对内容可能被读操作修改的存储单元,进行"预读",就会存在问题:
 |   |           |           |           |   |   |   |   |  //       比如,程序在读第N个单元时,CPU会把第N+1个单元也读入缓存,但在缓存被冲刷掉之前,程序并没有取走第N+1个单元,程序后面再想重新读取时,内存单元却已经被"预读"修改了
 |   |           |           |           |   |   |   |   |  //       所以,通常将映射到内存空间的寄存器区间,"可预取"标志置为0
 |   |           |           |           |   |   |   |   |- res->start = l & PCI_BASE_ADDRESS_MEM_MASK
 |   |           |           |           |   |   |   |   |
 |   |           |           |           |   |   |   |   |  // PCI规范规定,高28位中,只有位置最低的1,用于表示区间大小,而出厂值还有其它位为1,是为了表示哪些位可以由软件设置,所以不能忽略
 |   |           |           |           |   |   |   |   |- sz = pci_size(sz, PCI_BASE_ADDRESS_MEM_MASK)
 |   |           |           |           |   |   |   |       |
 |   |           |           |           |   |   |   |       |  // 假设:size = x100
 |   |           |           |           |   |   |   |       |  // 则:size-1 = x100-1 = x011
 |   |           |           |           |   |   |   |       |  //     => ~(size-1) = X100  (其中:X & x = 0)
 |   |           |           |           |   |   |   |       |  //                  & x100  (size)
 |   |           |           |           |   |   |   |       |  //                 --------
 |   |           |           |           |   |   |   |       |  //                  = 0100  (高位x不管为多少,都转换为0了)
 |   |           |           |           |   |   |   |       |- size = size & ~(size-1)
 |   |           |           |           |   |   |   |       |- return size-1
 |   |           |           |           |   |   |   |- else
 |   |           |           |           |   |   |   |   |
 |   |           |           |           |   |   |   |   |  // I/O区间,起始地址低3位为控制信息
 |   |           |           |           |   |   |   |   |- res->start = l & PCI_BASE_ADDRESS_IO_MASK
 |   |           |           |           |   |   |   |   |- sz = pci_size(sz, PCI_BASE_ADDRESS_IO_MASK & 0xffff)
 |   |           |           |           |   |   |   |
 |   |           |           |           |   |   |   |- res->end = res->start + (unsigned long) sz
 |   |           |           |           |   |   |   |- ... // 暂不关心64位地址的情况
 |   |           |           |           |   |   |
 |   |           |           |           |   |   |  // PCI设备中,有时也会包含ROM"(不可挥发)功能存储区间"(最多1个),PCI_ROM_ADDRESS寄存器,用于记录它的出厂信息,以及配置它的映射地址
 |   |           |           |           |   |   |- if (rom)
 |   |           |           |           |   |       |- ... // 和PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5处理过程类似
 |   |           |           |           |   |
 |   |           |           |           |   |  // 读取其它字段
 |   |           |           |           |   |- pci_read_config_word(dev, PCI_SUBSYSTEM_VENDOR_ID, &dev->subsystem_vendor)
 |   |           |           |           |   |- pci_read_config_word(dev, PCI_SUBSYSTEM_ID, &dev->subsystem_device)
 |   |           |           |           |
 |   |           |           |           |- case PCI_HEADER_TYPE_BRIDGE      // PCI-PCI桥
 |   |           |           |           |   |
 |   |           |           |           |   |- if (class != PCI_CLASS_BRIDGE_PCI)
 |   |           |           |           |   |   |- goto bad                 // 可以看出,仅"PCI-PCI"桥,属于1型设备
 |   |           |           |           |   |
 |   |           |           |           |   |  // 相比普通PCI设备,很多配置寄存器在PCI-PCI桥中不存在,并且PCI-PCI桥上,最多只有2个可挥发存储区间
 |   |           |           |           |   |- pci_read_bases(dev, 2, PCI_ROM_ADDRESS1)  // 不过次层PCI总线,需要后续进行递归扫描
 |   |           |           |           |
 |   |           |           |           |- case PCI_HEADER_TYPE_CARDBUS     // PCI-CardBus桥 (专用于笔记本,暂不关心)
 |   |           |           |               |- ...
 |   |           |           |
 |   |           |           |- pci_name_device(dev)  // 根据厂商和设备编号,获取厂商和设备名称 (转换表存储于drivers/pci/pci.ids文件)
 |   |           |           |
 |   |           |           |- if (!func)
 |   |           |           |   |- is_multi = hdr_type & 0x80                  // 是否多功能
 |   |           |           |- list_add_tail(&dev->global_list, &pci_devices)  // 将pci_dev对象添加到pci_devices全局链表
 |   |           |           |- list_add_tail(&dev->bus_list, &bus->devices)    // 将pci_dev对象添加到所属pci_bus的devices链表
 |   |           |           |
 |   |           |           |  // 有些设备出售后,厂商却发现固化在"配置寄存器组"中的初始化信息有问题,只好提供修正代码,在内核层面进行修改
 |   |           |           |- pci_fixup_device(PCI_FIXUP_HEADER, dev)
 |   |           |               |
 |   |           |               |  // 数组元素表示:对于由vendor厂商提供的device设备,在pass阶段,执行hook()修正函数
 |   |           |               |  /* pcibios_fixups[]:不同CPU架构,使用各自的
 |   |           |               |     pci_fixups[]:所有CPU架构共用
 |   |           |               |     都是pci_fixup对象全局数组,以{0}元素结束 */
 |   |           |               |  // 疑问:如果存在PCI BIOS,加电自检阶段,就已经基于错误的出厂信息,枚举过设备了,到内核执行阶段才修复,来得及吗??
 |   |           |               |- pci_do_fixups(dev, pass, pcibios_fixups)
 |   |           |               |- pci_do_fixups(dev, pass, pci_fixups)
 |   |           |                   |
 |   |           |                   |  // dev包含vendor和device信息
 |   |           |                   |  // pass为PCI_FIXUP_HEADER,表示"配置寄存器组"头部信息读出以后,PCI_FIXUP_FINAL表示全部信息读出以后
 |   |           |                   |- while (f->pass)
 |   |           |                       |- if (f->pass == pass &&
 |   |           |                       |   |  (f->vendor == dev->vendor || f->vendor == (u16) PCI_ANY_ID) &&
 |   |           |                       |   |  (f->device == dev->device || f->device == (u16) PCI_ANY_ID))
 |   |           |                       |   |
 |   |           |                       |   |- f->hook(dev)  // pass,vendor,device全部命中,执行对应的修正函数
 |   |           |                       |- f++
 |   |           |
 |   |           |  // 不光PCI设备中的出厂信息会有错,有些母板也存在设计问题
 |   |           |- pcibios_fixup_bus(bus)
 |   |           |   |
 |   |           |   |  // 去除"幻影" (有些母板上,会在两个不同的综合地址中,读出同一设备的头部信息,从而造成重复枚举,在形成的PCI树中引入"幻影")
 |   |           |   |- pcibios_fixup_ghosts(b)
 |   |           |   |
 |   |           |   |  // pci_scan_slot()函数,只读取了PCI设备"功能存储区间"的信息,还没读取PCI桥为次层总线保存的地址窗口
 |   |           |   |- pci_read_bridge_bases(b)
 |   |           |       |
 |   |           |       |  /* PCI桥是一种特殊的PCI设备,本身并不一定有RAM和ROM,但是要有3个地址窗口 (也用resource对象描述):
 |   |           |       |     1. I/O地址窗口 (窗口起始地址和大小,都以4KB对齐,PCI_IO_BASE和PCI_IO_LIMIT低4位全0时,表示I/O地址为16位,全1表示I/O地址为32位)
 |   |           |       |        PCI_IO_BASE(8位): I/O起始地址(16位) = PCI_IO_BASE高4+ 12*0
 |   |           |       |        PCI_IO_LIMIT(8位): I/O结束地址(16位) = PCI_IO_LIMIT高4+ 12*1
 |   |           |       |        PCI_IO_BASE_UPPER16(16位): I/O起始地址(32位) = PCI_IO_BASE_UPPER16 + PCI_IO_BASE高4+ 12*0
 |   |           |       |        PCI_IO_LIMIT_UPPER16(16位): I/O结束地址(32位) = PCI_IO_LIMIT_UPPER16 + PCI_IO_LIMIT高4+ 12*1
 |   |           |       |     2. 存储器地址窗口 (窗口起始地址和大小,都以1MB对齐,主要用于映射在存储器地址空间的I/O寄存器,最低4位固定为0)
 |   |           |       |        PCI_MEMORY_BASE(16位): 存储器起始地址(32位) = PCI_MEMORY_BASE高12+ 20*0
 |   |           |       |        PCI_MEMORY_LIMIT(16位): 存储器结束地址(32位) = PCI_MEMORY_BASE高12+ 20*1
 |   |           |       |     3. "可预取"存储器地址窗口 (窗口起始地址和大小,都以1MB对齐,最低4位为0表示地址为32位,为1表示地址为64位)
 |   |           |       |        PCI_PREF_MEMORY_BASE(16位): "可预取"存储器起始地址(32位) = PCI_PREF_MEMORY_BASE高12+ 20*0
 |   |           |       |        PCI_PREF_MEMORY_LIMIT(16位): "可预取"存储器结束地址(32位) = PCI_PREF_MEMORY_LIMIT高12+ 20*1
 |   |           |       |        PCI_PREF_BASE_UPPER32(32位): "可预取"存储器起始地址(64位) = PCI_PREF_BASE_UPPER32 + PCI_PREF_MEMORY_BASE高12+ 20*0
 |   |           |       |        PCI_PREF_LIMIT_UPPER32(32位): "可预取"存储器结束地址(64位) = PCI_PREF_BASE_UPPER32 + PCI_PREF_MEMORY_LIMIT高12+ 20*1
 |   |           |       |     CPU一侧访问的地址,必须在某个窗口之内(位于当前总线),才能到达下游,设备一侧访问的地址,必须在所有窗口之外(不在当前总线),才能到达上游
 |   |           |       |     此外,PCI命令寄存器("配置寄存器组"头部中的PCI_COMMAND字段),包含"memory access enable""I/O access enable"两个控制位
 |   |           |       |     控制位为0时,窗口对2个方向都关闭,用于保证还没为PCI设备各个区间分配合适的总线地址时,CPU和设备两侧不会相互干扰 */
 |   |           |       |- for(i=0; i<3; i++)
 |   |           |       |   |- child->resource[i] = &dev->resource[PCI_BRIDGE_RESOURCES+i]  // pci_bus对象中的resource只是指针,指向所在PCI桥的pci_dev对象的resource
 |   |           |       |
 |   |           |       |  // 读取PCI_IO_BASE,PCI_MEMORY_BASE,PCI_PREF_MEMORY_BASE等寄存器,记录到pci_bus对象的resource[0~2]
 |   |           |       |  // 对于i386 CPU,PCI桥的地址窗口,必然已在加电自检阶段,就由BIOS根据次层总线的需要,设置好了!!
 |   |           |       |- ...
 |   |           |
 |   |           |  // 递归扫描PCI-PCI桥或PCI-CardBus桥上的次层总线
 |   |           |- for (pass=0; pass < 2; pass++// 第一趟针对已由BIOS处理过的PCI桥
 |   |               |                              // 第二趟针对未经BIOS处理过的PCI桥 (疑问:对于i386 CPU,为什么会存在这种情况??)
 |   |               |- for (ln=bus->devices.next; ln != &bus->devices; ln=ln->next// 遍历上层总线上面的PCI设备
 |   |                   |- dev = pci_dev_b(ln)
 |   |                   |- if (dev->hdr_type == PCI_HEADER_TYPE_BRIDGE || dev->hdr_type == PCI_HEADER_TYPE_CARDBUS)  // 设备为"PCI-PCI""PCI-CardBus"
 |   |                       |
 |   |                       |  //  pci_scan_bridge()函数写的有点绕:
 |   |                       |  //                       | if ((buses & 0xffff00)) | pass | run    | 目的
 |   |                       |  // ----------------------+-------------------------+------+--------+------------------------------------------------------
 |   |                       |  //  已由BIOS处理,第一趟 |            Y            |  0   | 772行  | 第一趟仅为已由BIOS枚举的总线分配pci_bus对象,并统计最大总线号max
 |   |                       |  //  未经BIOS处理,第一趟 |            N            |  0   | return | 等待第一趟扫描完毕,max值完全确认后,在第二趟扫描中处理
 |   |                       |  //  已由BIOS处理,第二趟 |            Y            |  1   | return | 已经在第一趟扫描中处理过了
 |   |                       |  //  未经BIOS处理,第二趟 |            N            |  1   | 792行  | 基于已由BIOS处理的最大总线号max,处理未经BIOS处理的总线
 |   |                       |- max = pci_scan_bridge(bus, dev, max, pass)
 |   |                           |
 |   |                           |  // 读取"配置寄存器组"头部中的PCI_PRIMARY_BUS字段 (byte0:主总线号; byte1:次总线号; byte2:子树中最大总线号)
 |   |                           |- pci_read_config_dword(dev, PCI_PRIMARY_BUS, &buses)
 |   |                           |
 |   |                           |  // 次总线号和子树中最大总线号不全为0,表示加电自检阶段,BIOS已经枚举过当前总线,在此之前就已经设置过了
 |   |                           |- if ((buses & 0xffff00) && !pcibios_assign_all_busses())
 |   |                           |   |                          |- 空操作
 |   |                           |   |- if (pass)   // 已由BIOS枚举的总线,第二趟扫描不需要处理
 |   |                           |   |   |- return max
 |   |                           |   |
 |   |                           |   |- child = pci_add_new_bus(bus, dev, 0// 为新探测到的总线,分配和设置pci_bus对象 (逻辑简单,不再展开)
 |   |                           |   |- ...                                   // 继续设置pci_bus对象
 |   |                           |   |- if (!is_cardbus)
 |   |                           |   |   |- cmax = pci_do_scan_bus(child)     // 递归调用 (遇到次层总线,立即递归扫描,而不是探测到上层总线的所有次层总线后,再依次扫描次层总线,所以是深度优先)
 |   |                           |   |   |- if (cmax > max) max = cmax
 |   |                           |   |- else
 |   |                           |       |- cmax = child->subordinate         // PCI-CardBus桥不支持次层总线
 |   |                           |       |- if (cmax > max) max = cmax
 |   |                           |- else
 |   |                           |   |- if (!pass// 未经BIOS处理的总线,等待第一趟扫描完毕
 |   |                           |   |   |- return max
 |   |                           |   |
 |   |                           |   |  // 未经BIOS处理的PCI桥,相关配置寄存器,就得由内核设置 (读写过程涉及PCI规范)
 |   |                           |   |- pci_read_config_word(dev, PCI_COMMAND, &cr)
 |   |                           |   |- pci_write_config_word(dev, PCI_COMMAND, 0x0000)
 |   |                           |   |- pci_write_config_word(dev, PCI_STATUS, 0xffff)
 |   |                           |   |- child = pci_add_new_bus(bus, dev, ++max)             // 为新探测到的总线,分配和设置pci_bus对象
 |   |                           |   |- buses = (buses & 0xff000000)
 |   |                           |   |        | ((unsigned int)(child->primary)     <<  0)
 |   |                           |   |        | ((unsigned int)(child->secondary)   <<  8)
 |   |                           |   |        | ((unsigned int)(child->subordinate) << 16)   // 按照PCI_PRIMARY_BUS寄存器的组成规范,进行拼接
 |   |                           |   |- pci_write_config_dword(dev, PCI_PRIMARY_BUS, buses)  // 将总线号写入PCI_PRIMARY_BUS寄存器
 |   |                           |   |- if (!is_cardbus)
 |   |                           |   |   |- max = pci_do_scan_bus(child)                     // 递归调用
 |   |                           |   |- else
 |   |                           |   |   |- max += 3
 |   |                           |   |- child->subordinate = max
 |   |                           |   |- pci_write_config_byte(dev, PCI_SUBORDINATE_BUS, max)
 |   |                           |   |- pci_write_config_word(dev, PCI_COMMAND, cr)
 |   |                           |
 |   |                           |- return max
 |   |
 |   |  /* 到此为止,已经为0号主总线及其所有子总线分配了管理对象,并且获取了出厂信息,接下来继续完成如下操作:
 |   |     1. 设备中断请求线与中断控制器的连接
 |   |     2. 设备各个存储器区间,与总线地址的映射 (对于i386 CPU,一定已在加电自检阶段,由BIOS完成了映射,接下来只需检查并修正)
 |   |     另外,母板可以有多个"宿主-PCI桥",从而可以接多条PCI主总线,然而以上过程只完成了0号PCI主总线树的枚举,因此,接下来还要完成其它主总线树的枚举和设置 */
 |   |
 |   |  // 确认PCI设备与中断控制器之间的连接,记录到PCI_INTERRUPT_LINE寄存器 (将来为设备设置中断处理函数时,需要知道登记到哪个中断服务队列)
 |   |  /* 参照"外设-PCI插槽-路径互连器-中断控制器"之间的中断请求线连接关系图:
 |   |     "外设-PCI插槽"之间:由硬件决定,记录在PCI设备的PCI_INTERRUPT_PIN寄存器,以上过程已经读取了 (1~4分别表示与INTA~INTD连接)
 |   |     "PCI插槽-路径互连器"之间:由母板设计决定,为此,BIOS提供了一个"中断请求路径表"(irq_routing_table对象),记录每个PCI插槽的INTA~INTD中断请求线,分别与互连器的哪根请求线连接
 |   |     "路径互连器-中断控制器"之间:部分由硬件设计决定,其余由软件设置 (使得内核可以将中断设备,均匀分布到中断控制器的引脚上) */
 |   |- pcibios_irq_init()
 |   |   |
 |   |   |  // irq_routing_table对象的slots[]数组,记录了各个设备所在PCI插槽的INTA~INTD,与互连器的哪根中断请求线连接,以及互连器的这根请求线,可以连接中断控制器的哪些线
 |   |   |- pirq_table = pirq_find_routing_table()  // 利用内核自身接口,定位BIOS中的"中断请求路径表"
 |   |   |   |- for(addr = (u8 *) __va(0xf0000); addr < (u8 *) __va(0x100000); addr += 16)
 |   |   |       |- ...      // 根据signature和checksum等特征,寻找irq_routing_table对象位置
 |   |   |- if (!pirq_table && (pci_probe & PCI_BIOS_IRQ_SCAN))
 |   |   |   |- pirq_table = pcibios_get_irq_routing_table()  // #ifdef CONFIG_PCI_BIOS (利用BIOS接口,定位BIOS中的"中断请求路径表")
 |   |   |
 |   |   |  // 根据"中断请求路径表"信息,查询"PCI设备-中断控制器"连接关系:
 |   |   |  //         硬件决定         中断请求路径表            部分由硬件决定,其余由软件设置
 |   |   |  // PCI设备----------PCI插槽----------------路径互连器--------------------------------中断控制器
 |   |   |- if (pirq_table)  // 疑问:如果BIOS中找不到"中断请求路径表",为什么不返回错误??
 |   |       |               // 这种情况一定意味着,不需要内核设置"路径互连器-中断控制器"之间的连接(已完全由硬件决定),相应的,BIOS阶段就能完全确定"PCI设备-中断控制器"之间的连接关系。
 |   |       |
 |   |       |  // 先额外搞点别的事
 |   |       |  // 调用pcibios_irq_init()之前,只完成了0号PCI主总线树的枚举,pirq_peer_trick()根据irq_routing_table对象中出现的其它总线号,进行补充枚举
 |   |       |- pirq_peer_trick()
 |   |       |   |
 |   |       |   |  // 每个中断路径表项对应PCI插槽所在的总线,一定是经过BIOS确认存在的,并且每一条都有可能是主总线
 |   |       |   |- for(i=0; i < (rt->size - sizeof(struct irq_routing_table)) / sizeof(struct irq_info); i++)
 |   |       |   |   |- e = &rt->slots[i]
 |   |       |   |   |- busmap[e->bus] = 1
 |   |       |   |
 |   |       |   |  /* 疑问1:busmap[]数组,不是0~pcibios_last_bus下标处必然为1吗??
 |   |       |   |     个人猜测,BIOS并不会为空PCI插槽分配slots[i]对象,此处是为了只对插了PCI设备的总线,调用pci_scan_bus(),进而分配总线管理对象
 |   |       |   |     后面调用的pcibios_fixup_peer_bridges()函数(穷举式扫描总线),相比此处,正是直接通过pcibios_last_bus遍历,但是多了"总线上是否有PCI设备"的判断
 |   |       |   |    
 |   |       |   |     疑问2:根据《Linux内核源代码情景分析》说明,以下过程是为了补充扫描其它PCI主总线树,但是《PCI Express体系结构导读》(王齐),2.4.3节提到:
 |   |       |   |            "当处理器系统中存在多个HOST主桥时,将有多个编号为0的PCI总线,但是这些编号为0的PCI总线分属不同的PCI总线域"
 |   |       |   |     然而从此处看,linux-2.4.0内核,并没有对PCI总线分域,并且期望的是:第N+1条PCI主总线的编号 = 第N条PCI主总线树中最大总线编号 + 1
 |   |       |   |     为了求证这一点,我又粗略看了《存储技术原理分析:基于Linux_2.6内核源代码》,从linux-2.6.34内核源码中,看到如下片段:
 |   |       |   |            pirq_peer_trick()  // arch/x86/pci/irq.c
 |   |       |   |             |- ...
 |   |       |   |             |- for (i = 1; i < 256; i++)
 |   |       |   |                 |- if (!busmap[i] || pci_find_bus(0, i))  // 如果已经存在于domain0,不再重复扫描
 |   |       |   |                 |   |- continue                           // 推测:linux-2.6.34内核即使对PCI总线进行了分域,也没有期望每条PCI主总线,从0开始编号
 |   |       |   |                 |- pci_scan_bus_on_node()
 |   |       |   |                     |- pci_scan_bus()
 |   |       |   |                         |- pci_scan_bus_parented()
 |   |       |   |                             |- pci_create_bus()
 |   |       |   |                                 |- pci_find_bus(pci_domain_nr(b), bus)
 |   |       |   |                                                  |- struct pci_sysdata *sd = bus->sysdata
 |   |       |   |                                                  |- return sd->domain
 |   |       |   |     结论:linux-2.4.0内核非常老,有些实现细节会跟较新的设计思路不一致,不用过于纠结。
 |   |       |   |
 |   |       |   |     疑问30号总线除外,总线要先编号,才能被访问(可以认为0号总线在一个固定位置,不需要编号),既然编号前不能访问,又怎么为它编号呢??
 |   |       |   |     对总线进行编号,只是设置总线所在PCI桥的PCI_SECONDARY_BUS寄存器,并不需要访问总线本身(总线本身也没有寄存器啥的)
 |   |       |   |     所以,0号之外的主总线,虽然编号不是固定的,但是所在"宿主-PCI"桥在母板的位置是固定的,可以随时对它编号并访问  */
 |   |       |   |
 |   |       |   |  // 扫描其它PCI主总线树 (根据明确的总线清单进行补漏,否则后期需要执行pcibios_fixup_peer_bridges(),进行低效的穷举式扫描)
 |   |       |   |- for(i=1; i<256; i++)
 |   |       |       |
 |   |       |       |- if (busmap[i])
 |   |       |       |   |
 |   |       |       |   |  // 对源码中注释的解释:
 |   |       |       |   |  // 由于总线编号,采用的是深度优先遍历方式,那么次层总线号必然大于所在主总线号,所以如果i是次层总线,则所在主总线一定会早于i被扫描,i总线也在那时就已经扫描了
 |   |       |       |   |  // 所以pci_scan_bus()返回不为空时,i一定为PCI主总线
 |   |       |       |   |- pci_scan_bus(i, pci_root_bus->ops, NULL)  // 如果i总线之前已经扫描,pci_scan_bus()会保证不对其重复分配pci_dev对象并扫描
 |   |       |       |
 |   |       |       |- pcibios_last_bus = -1  // pcibios_last_bus表示加电阶段,BIOS设置的最大总线编号,此处设置为-1,表示这些总线都已扫描,后期不再需要扫描
 |   |       |
 |   |       |  // 查找互连器的irq_router对象,赋值到全局变量pirq_router,供后期使用
 |   |       |  //(互连器作为一种特殊的PCI设备,除了要有pci_dev对象记录其设备信息,还要有irq_router对象记录其"编程接口" (互连器是可编程的))
 |   |       |- pirq_find_router()
 |   |       |   |
 |   |       |   |- struct irq_routing_table *rt = pirq_table  // "中断请求路径表"
 |   |       |   |- pirq_router = pirq_routers + sizeof(pirq_routers) / sizeof(pirq_routers[0]) - 1  // pirq_routers[]:不同厂商的互连器信息表
 |   |       |   |
 |   |       |   |  // 在pci_devices全局链表中,找到互连器的pci_dev对象
 |   |       |   |  // irq_routing_table对象的rtr_bus(所在总线)和rtr_devfn(设备+功能号)成员,记录了互连器的位置 (互连器通常与PCI-ISA桥集成在同一芯片,作为PCI设备连接在PCI总线上)
 |   |       |   |- pirq_router_dev = pci_find_slot(rt->rtr_bus, rt->rtr_devfn)
 |   |       |   |   |- pci_for_each_dev(dev)
 |   |       |   |       |- if (dev->bus->number == bus && dev->devfn == devfn)
 |   |       |   |           |- return dev
 |   |       |   |
 |   |       |   |  // "中断请求路径表"和互连器的pci_dev对象,都包含互连器的生产厂商和设备ID,优先以"中断请求路径表"提供的为准
 |   |       |   |  // 如果找不到对应的irq_router对象,表示互连器与中断控制器之间的连接,不需要软件设置 (比如全部为"硬连接")
 |   |       |   |- for(r=pirq_routers; r->vendor; r++)
 |   |       |       |- if (r->vendor == rt->rtr_vendor && r->device == rt->rtr_device)
 |   |       |       |   |- pirq_router = r
 |   |       |       |   |- break
 |   |       |       |- if (r->vendor == pirq_router_dev->vendor && r->device == pirq_router_dev->device)
 |   |       |           |- pirq_router = r
 |   |       |
 |   |       |  // "中断请求路径表"还有一个exclusive_irqs字段,是一个位图,用于表示中断控制器上,应该避免共用的中断请求线 (arch/i386/kernel/pci-irq.c, 27行):
 |   |       |  /*
 |   |       |   * Never use: 0, 1, 2 (timer, keyboard, and cascade)  (0号中断请求线,由时钟中断专用)
 |   |       |   * Avoid using: 13, 14 and 15 (FP error and IDE).
 |   |       |   * Penalize: 3, 4, 6, 7, 12 (known ISA uses: serial, floppy, parallel and mouse)
 |   |       |   */
 |   |       |- if (pirq_table->exclusive_irqs)
 |   |           |- for (i=0; i<16; i++)
 |   |               |- if (!(pirq_table->exclusive_irqs & (1 << i)))
 |   |                   |- pirq_penalty[i] += 100  // 内核使用"惩罚量",对应需要避免的程度
 |   |
 |   |  // 穷举式扫描还未枚举的总线
 |   |- pcibios_fixup_peer_bridges()
 |   |   |- if (pcibios_last_bus <= 0 || pcibios_last_bus >= 0xff// 如果根据"中断请求路径表"补过漏了,不用再扫描
 |   |   |   |- return
 |   |   |- for (n=0; n <= pcibios_last_bus; n++)
 |   |       |- for(dev.devfn=0; dev.devfn<256; dev.devfn += 8)
 |   |           |- if (!pci_read_config_word(&dev, PCI_VENDOR_ID, &l) && l != 0x0000 && l != 0xffff// 总线上有PCI设备,才会被扫描
 |   |               |- pci_scan_bus(n, pci_root_ops, NULL)
 |   |
 |   |  // pirq_peer_trick()和pcibios_fixup_peer_bridges()属于顺带任务,pcibios_irq_init()之后的主要任务,是继续处理中断请求线的连接问题
 |   |- pcibios_fixup_irqs()
 |   |   |
 |   |   |  // 累积惩罚值 (有些dev->irq,在加电自检阶段,就已经被BIOS设置成非0值了,并由前面的pci_setup_device()读到了pci_dev对象中)
 |   |   |- pci_for_each_dev(dev)  // 遍历pci_devices全局链表上的pci_dev对象
 |   |   |   |- if (pirq_penalty[dev->irq] >= 100 && pirq_penalty[dev->irq] < 100000)
 |   |   |   |   |- pirq_penalty[dev->irq] = 0
 |   |   |   |- pirq_penalty[dev->irq]++  // 有了pirq_penalty[]参考,内核就可以尽量保证,将剩余未接的请求线,接在中断控制器相对空闲的请求线上
 |   |   |
 |   |   |  // 扫描所有pci设备,对于已经连接到中断控制器的设备,获取其连接的请求线,记录到PCI_INTERRUPT_LINE寄存器
 |   |   |- pci_for_each_dev(dev)
 |   |       |- pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin)
 |   |       |- ...                       // #ifdef CONFIG_X86_IO_APIC (暂不关心)
 |   |       |- if (pin && !dev->irq)     // PCI_INTERRUPT_PIN寄存器不为0(表示设备可以产生中断),但是dev->irq为0(表示还不知道设备连接中断控制器的哪根请求线)
 |   |           |
 |   |           |  // 根据"外设-PCI插槽-路径互连器-中断控制器",逐层查找
 |   |           |  // 不过dev设备连接的那根互连器请求线,可能还未与中断控制器连接,第2个参数传0,正是表示只查找、不连接,让pcibios_lookup_irq()先别管未连接的情况
 |   |           |- pcibios_lookup_irq(dev, 0// 疑问1:对于i386 CPU,此时就已连接的情况,必定也由BIOS设置好了PCI_INTERRUPT_LINE,那么为0就表示还未连接,还有必要查找吗??
 |   |               |                          // 疑问2:pcibios_lookup_irq()找到连接的请求线后,为什么并没有补充记录到PCI_INTERRUPT_LINE寄存器??
 |   |               |
 |   |               |- struct irq_router *r = pirq_router
 |   |               |- pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin)
 |   |               |- pin = pin - 1                 // pin=1~4时,表示dev设备中断请求线,与PCI插槽INTA~INTD连接,此处减1,用于后续作为数组下标
 |   |               |- info = pirq_get_info(dev)     // 获取dev设备所在插槽的irq_info对象 (用于描述PCI插槽上的INTA~INTD请求线,与互连器的连接信息)
 |   |               |   |- struct irq_routing_table *rt = pirq_table
 |   |               |   |- int entries = (rt->size - sizeof(struct irq_routing_table)) / sizeof(struct irq_info)    // "中断请求路径表"中的irq_info对象个数
 |   |               |   |- for (info = rt->slots; entries--; info++)
 |   |               |       |- if (info->bus == dev->bus->number && PCI_SLOT(info->devfn) == PCI_SLOT(dev->devfn))  // 匹配dev设备,与各个插槽的irq_info对象
 |   |               |           |- return info
 |   |               |- pirq = info->irq[pin].link    // link:PCI插槽第pin根请求线,连接的互连器请求线号
 |   |               |                                // 4位:是否"硬连接"; 低4位(仅"硬连接"时有意义):连接的互连器请求线号
 |   |               |- mask = info->irq[pin].bitmap  // 与PCI插槽第pin根请求线连接的互连器请求线,可以连接的中断控制器请求线位图
 |   |               |- mask &= pcibios_irq_mask      // 中断控制器0~2请求线,禁止共用
 |   |               |
 |   |               |  // 运行至此处,dev->irq一定为0,assign一定为0 (见pcibios_lookup_irq()函数的调用条件,和第二个参数值)
 |   |               |- newirq = dev->irq       // dev->irq为0,表示还不知道设备连接中断控制器的哪根请求线,一种可能是连接了,但还没查询,另一种可能是还没连接
 |   |               |- if (!newirq && assign)  // assign=1时(一定是后期,此时assign为0),如果dev->irq还是0,那就不得不进行连接了
 |   |               |   |- for (i = 0; i < 16; i++)
 |   |               |       |- if (pirq_penalty[i] < pirq_penalty[newirq])
 |   |               |           |- request_irq(i, pcibios_test_irq_handler, SA_SHIRQ, "pci-test", dev)
 |   |               |           |- newirq = i
 |   |               |
 |   |               |  // 当前场景,一定会执行到这里,并且进入以下第1或第2个分支,最终获取设备连接的中断控制器请求线 (assign=1时,newirq才能非0,才能进入第3个分支)
 |   |               |- if ((pirq & 0xf0) == 0xf0)
 |   |               |   |- irq = pirq & 0xf
 |   |               |- else if (r->get)
 |   |               |   |- irq = r->get(pirq_router_dev, dev, pirq)
 |   |               |- else if (newirq && r->set && (dev->class >> 8) != PCI_CLASS_DISPLAY_VGA)
 |   |               |   |- r->set(pirq_router_dev, dev, pirq, newirq)
 |   |               |
 |   |               |  // 连接到互连器同一根请求线的设备,一定也连接到中断控制器的同一根请求线 (pci_for_each_dev(dev)后续遍历到这些设备时,就不需要再调用pcibios_lookup_irq())
 |   |               |- pci_for_each_dev(dev2)
 |   |                   |- pci_read_config_byte(dev2, PCI_INTERRUPT_PIN, &pin)
 |   |                   |- info = pirq_get_info(dev2)
 |   |                   |- if (info->irq[pin].link == pirq)
 |   |                       |- dev2->irq = irq
 |   |                       |- pirq_penalty[irq]++
 |   |
 |   |  // "配置寄存器组"头部中保存的地址区间起始地址:
 |   |  //  出厂时,为设备内部的局部地址,对于i386 CPU,已在加电阶段,由BIOS设置为映射地址了
 |   |  //  内核为之建立resource对象,并加以验证和确认:过低的物理地址区间(被内核本身使用)、与其它区间冲突的地址区间,都要另行分配
 |   |
 |   |  /* resource结构:
 |   |      start和end表示区间范围,flags包含表示区间属性的标志位,parent,sibling和child用于按照位置关系,将所有resource对象组织在一起
 |   |
 |   |     总线地址空间:(pci_init()之前,已经被"批发"走了一部分)
 |   |      struct resource ioport_resource = { "PCI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO };
 |   |      struct resource iomem_resource = { "PCI mem", 0x00000000, 0xffffffff, IORESOURCE_MEM }; */
 |   |- pcibios_resource_survey()
 |       |
 |       |  // 为每条PCI总线分配resource对象 (事后追认!!)
 |       |  // 每条PCI总线占用的地址资源,实际就是所在PCI桥的3个地址窗口:
 |       |  //  设备上,由所在PCI桥的PCI_IO_BASE,PCI_MEMORY_BASE,PCI_PREF_MEMORY_BASE寄存器记录
 |       |  //  内存中,由所在PCI桥pci_dev对象的resource[7~9]记录
 |       |  // 实际上,pcibios_allocate_bus_resources()并不需要分配什么:
 |       |  //  总线地址早已在加电阶段由BIOS分配,并设置到PCI_IO_BASE,PCI_MEMORY_BASE,PCI_PREF_MEMORY_BASE寄存器 (很多设置在加电自检阶段就已经完成了,pci_init()其实只是修复和补充)
 |       |  //  pci_bus对象的*resource[0~2],也已经在前面调用pci_read_bridge_bases()时,直接指向所在PCI桥pci_dev对象的&resource[7~9],因此也不需要分配
 |       |- pcibios_allocate_bus_resources(&pci_root_buses)           // 建议仔细看一下函数定义上方的注释
 |       |   |- for (ln=bus_list->next; ln != bus_list; ln=ln->next// 被pcibios_resource_survey()调用时,bus_list=&pci_root_buses,递归调用时,bus_list=上层总线的children链表
 |       |       |
 |       |       |- bus = pci_bus_b(ln)     // list_entry()的封装
 |       |       |- if ((dev = bus->self))  // bus->self:总线自身所在的设备(PCI桥) (bus->devices:连接在总线上的设备)
 |       |       |   |  // 仅为窗口分配resource对象 (PCI桥本身具有的RAM、ROM区间,将在后面同普通PCI设备一起,调用pcibios_allocate_resources()函数进行分配)
 |       |       |   |- for (idx = PCI_BRIDGE_RESOURCES; idx < PCI_NUM_RESOURCES; idx++)
 |       |       |       |
 |       |       |       |- r = &dev->resource[idx]
 |       |       |       |- if (!r->start)  // 当前区间没有对地址资源的需求 (比如子设备不需要IO空间,则PCI桥就不需要IO窗口)
 |       |       |       |   |- continue
 |       |       |       |
 |       |       |       |  // "父节点"批发地址资源 (对于i386 CPU,"父节点"需要占用多少地址资源,已经由BIOS计算好了,内核只是"追加"resource对象)
 |       |       |       |- pr = pci_find_parent_resource(dev, r)   // dev:总线所在PCI桥的pci_dev对象
 |       |       |       |   |                                      // r:区间原始信息 (在设备内的局部地址、大小等)
 |       |       |       |   |- for(i=0; i<4; i++)
 |       |       |       |   |   |- struct resource *r = bus->resource[i]
 |       |       |       |   |   |- if (res->start && !(res->start >= r->start && res->end <= r->end))
 |       |       |       |   |   |   |- continue
 |       |       |       |   |   |- if ((res->flags ^ r->flags) & (IORESOURCE_IO | IORESOURCE_MEM))  // I/O区间,必须从I/O空间分配,存储器区间,必须从存储器区间分配
 |       |       |       |   |   |   |- continue
 |       |       |       |   |   |- if (!((res->flags ^ r->flags) & IORESOURCE_PREFETCH))  //     分配区间(res)要求"可预取",但"父节点"区间(r)不支持"可预取"  ("可预取"区间,不能从不支持"可预取"的空间分配)
 |       |       |       |   |   |   |                                                     // 或:分配区间(res)不要求"可预取",但"父节点"区间(r)支持"可预取"  ("可预取"空间,尽量留给要求"可预取"的区间)
 |       |       |       |   |   |   |                                                     //    |-------------------|
 |       |       |       |   |   |   |                                                     // !((res->flags ^ r->flags) & IORESOURCE_PREFETCH)
 |       |       |       |   |   |   |                                                     // |----------------------------------------------|
 |       |       |       |   |   |   |                                                     //  只要不是IORESOURCE_PREFETCH,直接返回"父节点"的这块空间
 |       |       |       |   |   |   |- return r
 |       |       |       |   |   |- if ((res->flags & IORESOURCE_PREFETCH) && !(r->flags & IORESOURCE_PREFETCH))  // 不要求"可预取"的区间,迫不得已时也可以从"可预取"空间分配
 |       |       |       |   |   |   |- best = r
 |       |       |       |   |- return best
 |       |       |       |
 |       |       |       |  // 从批发的地址资源中,分配需要的地址区间
 |       |       |       |- if (!pr)
 |       |       |           |- request_resource(pr, r)  // 从pr上面,切割出一个r
 |       |       |               |- __request_resource(root, new)
 |       |       |                   |- if (end < start)          // 冲突:new->end < new->start
 |       |       |                   |   |-return root;
 |       |       |                   |- if (start < root->start)  // 冲突:new->start < root->start
 |       |       |                   |   |- return root;
 |       |       |                   |- if (end > root->end)      // 冲突:new->end > root->end
 |       |       |                   |   |- return root;
 |       |       |                   |- p = &root->child
 |       |       |                   |- for (;;)
 |       |       |                       |- tmp = *p
 |       |       |                       |- if (!tmp || tmp->start > end)  // 如果tmp是第一个child,只需要tmp->start > end即可
 |       |       |                       |   |                             // 否则上次循环,一定是从if (tmp->end < start)分支中,continue的,保证了上个child->end < start
 |       |       |                       |   |- new->sibling = tmp
 |       |       |                       |   |- *p = new
 |       |       |                       |   |- new->parent = root
 |       |       |                       |   |- return NULL         // 没有冲突区间,返回NULL
 |       |       |                       |- p = &tmp->sibling
 |       |       |                       |- if (tmp->end < start)
 |       |       |                       |   |- continue
 |       |       |                       |- return tmp              // tmp->start < end && tmp->end > start,说明new与tmp有重叠部分
 |       |       |
 |       |       |- pcibios_allocate_bus_resources(&bus->children)  // 递归
 |       |
 |       |  // 为PCI设备分配resource对象 (第一趟,处理已经生效的地址区间)
 |       |- pcibios_allocate_resources(0)
 |       |   |- pci_for_each_dev(dev)  // 遍历所有PCI设备
 |       |       |- pci_read_config_word(dev, PCI_COMMAND, &command)
 |       |       |- for(idx = 0; idx < 6; idx++// PCI设备本身的RAM区间 (最多6个)
 |       |       |   |- r = &dev->resource[idx]
 |       |       |   |- if (r->parent)  // parent已经指向"父节点",表示已经分配了resource
 |       |       |   |   |- continue
 |       |       |   |- if (!r->start)  // 区间起始地址为0,表示不需要分配地址资源的区间,或者暂时无法从"父节点"分配
 |       |       |   |   |- continue
 |       |       |   |  // 需要分配地址资源的区间
 |       |       |   |- if (r->flags & IORESOURCE_IO)
 |       |       |   |   |- disabled = !(command & PCI_COMMAND_IO)
 |       |       |   |- else
 |       |       |   |   |- disabled = !(command & PCI_COMMAND_MEMORY)
 |       |       |   |- if (pass == disabled)  // 第一趟,pass=0
 |       |       |       |- pr = pci_find_parent_resource(dev, r)    // "父节点"批发地址资源
 |       |       |       |- if (!pr || request_resource(pr, r) < 0// 从批发的地址资源中,分配需要的地址区间
 |       |       |           |  // 无法从"父节点"分配,先将该区间起始地址平移到0,后续由pcibios_assign_resources()统一变更到合适的位置
 |       |       |           |- r->end -= r->start
 |       |       |           |- r->start = 0
 |       |       |
 |       |       |- if (!pass// 仅第一趟执行,关闭ROM区间:
 |       |           |          // ROM区间一般只在初始化时由BIOS或具体的驱动程序使用,但地址资源还保留,如果需要,还可以在驱动程序中再打开
 |       |           |- r = &dev->resource[PCI_ROM_RESOURCE]
 |       |           |- if (r->flags & PCI_ROM_ADDRESS_ENABLE)
 |       |               |- r->flags &= ~PCI_ROM_ADDRESS_ENABLE
 |       |               |- pci_read_config_dword(dev, dev->rom_base_reg, &reg)
 |       |               |- pci_write_config_dword(dev, dev->rom_base_reg, reg & ~PCI_ROM_ADDRESS_ENABLE)
 |       |
 |       |  // 为PCI设备分配resource对象 (第二趟,基于第一趟剩余的地址区间,处理未生效地址区间)
 |       |- pcibios_allocate_resources(1)
 |       |   |- pci_for_each_dev(dev)
 |       |       |- ...
 |       |
 |       |  // 处理未能从"父节点"分配到地址资源(即起始地址为0)的区间 (真实意义上的"分配",而不是"追认")
 |       |- pcibios_assign_resources()
 |           |- pci_for_each_dev(dev)  // 遍历所有PCI设备
 |               |- int class = dev->class >> 8  // "配置寄存器组"PCI_CLASS_DEVICE字段,高8位代表设备类型
 |               |- if (!class || class == PCI_CLASS_BRIDGE_HOST)  // 设备无效(类型字段为0),或者是PCI桥,不处理
 |               |   |- continue
 |               |
 |               |- for(idx=0; idx<6; idx++// PCI设备本身的RAM区间 (最多6个)
 |               |   |- r = &dev->resource[idx]
 |               |   |- if ((class == PCI_CLASS_STORAGE_IDE && idx < 4) ||
 |               |   |   |  (class == PCI_CLASS_DISPLAY_VGA && (r->flags & IORESOURCE_IO)))  // IDE存储设备(硬盘)的前4个区间,和VGA设备的I/O区间是特例,这些区间既不需要分配,也不能改变
 |               |   |   |- continue
 |               |   |- if (!r->start && r->end)  // 起始地址为0,但结束地址不为0,表示需要分配地址资源的区间 (比如之前分配失败,经过"平移"的区间)
 |               |       |- pci_assign_resource(dev, idx)  // 分配总线地址,并设置"配置寄存器组"的相应字段
 |               |           |- const struct pci_bus *bus = dev->bus
 |               |           |- struct resource *res = dev->resource + i
 |               |           |- size = res->end - res->start + 1                                       // 区间大小
 |               |           |- min = (res->flags & IORESOURCE_IO) ? PCIBIOS_MIN_IO : PCIBIOS_MIN_MEM  // I/O地址区间位置不得低于4KB,内存地址区间位置不得低于256MB
 |               |           |
 |               |           |  // 为PCI设备从所在总线分配地址资源 (IORESOURCE_PREFETCH参数表示:如果设备区间"可预取",优先要求分配的地址区间也"可预取")
 |               |           |- if (pci_assign_bus_resource(bus, dev, res, size, min, IORESOURCE_PREFETCH, i) < 0)
 |               |               |   |- for (i = 0 ; i < 4; i++)
 |               |               |       |
 |               |               |       |- struct resource *r = bus->resource[i]     // r:所在总线的地址窗口
 |               |               |       |- if ((res->flags ^ r->flags) & type_mask)  // res:待分配的地址区间
 |               |               |       |   |- continue
 |               |               |       |- if ((r->flags & IORESOURCE_PREFETCH) && !(res->flags & IORESOURCE_PREFETCH))
 |               |               |       |   |- continue
 |               |               |       |
 |               |               |       |  // 分配总线地址
 |               |               |       |- allocate_resource(r, res, size, min, -1, size, pcibios_align_resource, dev)
 |               |               |       |   |                                              |
 |               |               |       |   |                                              |  // 避免使用bit8或bit9为非0的I/O地址:
 |               |               |       |   |                                              |  //     0000,0000B ~     1111,1111B:保留给母板使用
 |               |               |       |   |                                              |  //  01,0000,0000B 11,1111,1111B:有些外设只解析I/O地址低10
 |               |               |       |   |                                              |  // X01,0000,0000B ~ X11,1111,1111B:比如将001,0000,0000B~011,1111,1111B分配给外设A,将101,0000,0000B~111,1111,1111B分配给外设B
 |               |               |       |   |                                              |  //                                  并且外设A、B正好都只解析低10位,则实际上它们都分配到01,0000,0000B~11,1111,1111B地址区间,导致冲突
 |               |               |       |   |  // 寻找符合要求的区间                       |- if (res->flags & IORESOURCE_IO)
 |               |               |       |   |  // root:   总线上的地址窗口                     |- if (start & 0x300)
 |               |               |       |   |  // new:    待分配的地址区间                     |   |- start = (start + 0x3ff) & ~0x3ff
 |               |               |       |   |  // size:   待分配区间的大小 (必为2的次幂)       |- res->start = start
 |               |               |       |   |  // align:  待分配区间地址地址对齐值 (=size)
 |               |               |       |   |  // alignf: 边界对齐函数指针 (pcibios_align_resource())
 |               |               |       |   |  // alignf_data: alignf()参数 (pci_dev对象)
 |               |               |       |   |- err = find_resource(root, new, size, min, max, align, alignf, alignf_data)
 |               |               |       |   |         |- for(;;)
 |               |               |       |   |             |- struct resource *this = root->child
 |               |               |       |   |             |- new->start = root->start
 |               |               |       |   |             |- if (this)                   // root有子节点
 |               |               |       |   |             |   |- new->end = this->start  // 扫描子区间
 |               |               |       |   |             |- else                        // root为叶子节点,没有子节点
 |               |               |       |   |             |   |- new->end = root->end    // root起点,作为待分配区间的起点
 |               |               |       |   |             |- if (new->start < min)
 |               |               |       |   |             |   |- new->start = min
 |               |               |       |   |             |- if (new->end > max)
 |               |               |       |   |             |   |- new->end = max
 |               |               |       |   |             |- new->start = (new->start + align - 1) & ~(align - 1// 先按照align值对齐
 |               |               |       |   |             |- if (alignf)
 |               |               |       |   |             |   |- alignf(alignf_data, new, size)  // 再通过alignf()额外调整
 |               |               |       |   |             |- if (new->start < new->end && new->end - new->start + 1 >= size)  // 找到大小符合要求的区间
 |               |               |       |   |             |   |- new->end = new->start + size - 1
 |               |               |       |   |             |   |- return 0
 |               |               |       |   |             |- if (!this)
 |               |               |       |   |             |   |- break
 |               |               |       |   |             |- new->start = this->end + 1
 |               |               |       |   |             |- this = this->sibling
 |               |               |       |   |
 |               |               |       |   |  // 将分配到的区间,添加到root->child队列
 |               |               |       |   |- __request_resource(root, new)
 |               |               |       |
 |               |               |       |  // 将分配地址设置到PCI设备的"配置寄存器组"
 |               |               |       |- pcibios_update_resource(dev, r, res, resno)
 |               |               |           |- new = res->start | (res->flags & PCI_REGION_FLAG_MASK)
 |               |               |           |- if (resource < 6)
 |               |               |           |   |- reg = PCI_BASE_ADDRESS_0 + 4*resource  // PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5地址 (每个寄存器占4字节)
 |               |               |           |- else if (resource == PCI_ROM_RESOURCE)
 |               |               |           |   |- res->flags |= PCI_ROM_ADDRESS_ENABLE
 |               |               |           |   |- new |= PCI_ROM_ADDRESS_ENABLE
 |               |               |           |   |- reg = dev->rom_base_reg
 |               |               |           |- else
 |               |               |           |   |- return
 |               |               |           |- pci_write_config_dword(dev, reg, new)
 |               |               |           |- pci_read_config_dword(dev, reg, &check)
 |               |               |
 |               |               |  // 为PCI设备从所在总线分配地址资源 (情非得已并且设备区间"可预取"的情况下,分配到的地址区间与其不符(即不"可预取"),也是可以的,相当于放弃使用设备的"可预取"特性)
 |               |               |- if (!(res->flags & IORESOURCE_PREFETCH) || pci_assign_bus_resource(bus, dev, res, size, min, 0, i) < 0)
 |               |                   |- return -EBUSY
 |               |
 |               |- if (pci_probe & PCI_ASSIGN_ROMS)
 |                   |- r = &dev->resource[PCI_ROM_RESOURCE]
 |                   |- r->end -= r->start
 |                   |- r->start = 0
 |                   |- if (r->end)
 |                       |- pci_assign_resource(dev, PCI_ROM_RESOURCE)
 |
 // 出厂信息修正 (PCI初始化结束阶段)
 |- pci_for_each_dev(dev)
     |- pci_fixup_device(PCI_FIXUP_FINAL, dev)
// 声明:以下分析的代码,是针对i386 CPU的实现!!
pci_init()
 |
 |- pcibios_init()  // arch/i386/kernel/pci-pc.c !!
 |   |              // 早期,该函数完全通过调用BIOS接口实现,所以函数名中包含了"bios"
 |   |              // 后来,该函数演变为,可以通过CONFIG_PCI_DIRECT和CONFIG_PCI_BIOS宏,选择调用BIOS接口,还是调用内核自己实现的接口
 |   |              // 而且两者不互斥,如果2个宏都打开,内核通常优先使用自己的接口,或用自己的接口,对BIOS接口的操作结果进行修正
 |   |
 |   |  // 获取BIOS提供的PCI操作接口 (#ifdef CONFIG_PCI_BIOS)
 |   |- pci_root_ops = pci_find_bios()
 |   |   |
 |   |   |  // 0xe0000~0xffff0为BIOS占用的物理内存
 |   |   |- for (check = (union bios32 *) __va(0xe0000); check <= (union bios32 *) __va(0xffff0); ++check)
 |   |       |
 |   |       |  // 每个bios32对象,都包含16字节头部,其中包含signature和checksum字段
 |   |       |- if (check->fields.signature != BIOS32_SIGNATURE)
 |   |       |   |- continue
 |   |       |
 |   |       |  // 构造bios32对象时,通过设置checksum字段值,保证了整个对象占用的字节相加为0
 |   |       |- for (i = 0; i < length ; ++i)
 |   |       |   |- sum += check->chars[i]
 |   |       |- if (sum != 0)
 |   |       |   |- continue
 |   |       |
 |   |       |  // 进一步检查
 |   |       |- if (check_pcibios())
 |   |           |- if ((pcibios_entry = bios32_service(PCI_SERVICE)))
 |   |           |   |- pci_indirect.address = pcibios_entry + PAGE_OFFSET  // 设置BIOS的PCI服务接口的段内偏移
 |   |           |   |- ...                                                 // 设置参数:PCIBIOS_PCI_BIOS_PRESENT
 |   |           |   |- lcall &pci_indirect                                 // 调用接口
 |   |           |             |- pci_indirect = { 0, __KERNEL_CS }         // 初始化时,已将段基址设置为__KERNEL_CS
 |   |           |- return &pci_bios_access
 |   |
 |   |  // 获取内核自己实现的PCI操作接口 (#ifdef CONFIG_PCI_DIRECT)
 |   |- pci_root_ops = pci_check_direct()
 |   |   |
 |   |   |  // 探测主总线是否为PCI总线
 |   |   |- if (pci_probe & PCI_PROBE_CONF1)
 |   |   |   |- outb (0x01, 0xCFB)            // 32位地址寄存器最高字节0xCFB,称为"PMC寄存器",它的bit1~7保留,bit0用于选择"PCI配置访问机制"
 |   |   |   |                                // 参考:https://html.datasheetbank.com/datasheet-html/251876/Intel/34page/290479-004.html (PCI datasheet, page 34)
 |   |   |   |                                //       bit0=0 | PCI配置访问机制0 | 2型PCI设备
 |   |   |   |                                //      --------+------------------+------------
 |   |   |   |                                //       bit0=1 | PCI配置访问机制1 | 1型PCI设备
 |   |   |   |- tmp = inl (0xCF8)             // 从地址寄存器,读取原始值 (对于常规存储单元,先向a写入1(a=1),再读取a(b=a),结果会为1,但是PCI配置寄存器往往不是这样)
 |   |   |   |- outl (0x80000000, 0xCF8)      // 向地址寄存器,写入0x80000000
 |   |   |   |                                // 至于为什么非要写入0x80000000进行检查,估计也是PCI datasheet规定,后续分析不会过于纠结这种细节,建议尝试根据PCI datasheet查找
 |   |   |   |
 |   |   |   |  // 读到的还是0x80000000(),并且通过pci_sanity_check()检查
 |   |   |   |- if (inl (0xCF8) == 0x80000000 && pci_sanity_check(&pci_direct_conf1))
 |   |   |   |   |                                |
 |   |   |   |   |                                |- for(dev.devfn=0; dev.devfn < 0x100; dev.devfn++// 一条PCI总线,最多可以有256个逻辑设备
 |   |   |   |   |                                    |- if ((!o->read_word(&dev, PCI_CLASS_DEVICE, &x) &&
 |   |   |   |   |                                        |   (x == PCI_CLASS_BRIDGE_HOST || x == PCI_CLASS_DISPLAY_VGA)) ||
 |   |   |   |   |                                        |  (!o->read_word(&dev, PCI_VENDOR_ID, &x) &&
 |   |   |   |   |                                        |   (x == PCI_VENDOR_ID_INTEL || x == PCI_VENDOR_ID_COMPAQ)))  // 连接了PCI_CLASS_BRIDGE_HOST或PCI_CLASS_DISPLAY_VGA设备,认为是PCI总线
 |   |   |   |   |                                        |                                                              // 连接的设备由Intel或Compaq厂商制造,也认为是PCI总线
 |   |   |   |   |                                        |- return 1
 |   |   |   |   |
 |   |   |   |   |- outl (tmp, 0xCF8)                      // 为地址寄存器恢复原始值
 |   |   |   |   |- request_region(0xCF8, 8, "PCI conf1"// 在内核I/O设备资源树增加一个节点,用于记录"宿主-PCI桥"占用的I/O地址区间
 |   |   |   |   |- return &pci_direct_conf1               // 返回pci_direct_conf1对象地址,包含read_byte()、write_byte()等函数/PCI操作列表
 |   |   |   |
 |   |   |   |- outl (tmp, 0xCF8)             // 为地址寄存器恢复原始值
 |   |   |
 |   |   |  // 探测主总线是否为ISA总线 (现已不用)
 |   |   |- if (pci_probe & PCI_PROBE_CONF2)
 |   |       |- outb (0x00, 0xCFB)            // PMC寄存器=0,选择机制0,访问2型PCI设备的配置寄存器
 |   |       |- ...
 |   |       |- return &pci_direct_conf2
 |   |
 |   |  // 扫描主总线,探测与枚举连接在主总线上的PCI设备,如果设备为"PCI-PCI"桥,则递归扫描其连接的次层总线
 |   |  // 对于i386 CPU,必定已由BIOS在加电自检阶段枚举过一遍了,pci_scan_bus()相当于直接读取BIOS的设置成果,并分配管理对象进行记录
 |   |- pci_root_bus = pci_scan_bus(0, pci_root_ops, NULL)
 |   |   |
 |   |   |  // 分配pci_bus对象,用于记录总线信息
 |   |   |- struct pci_bus *b = pci_alloc_primary_bus()
 |   |   |   |- if (pci_bus_exists(&pci_root_buses, bus))  // 检查pci_root_buses全局链表,是否已经存在总线号为bus的pci_bus对象
 |   |   |   |   |- return NULL
 |   |   |   |- b = pci_alloc_bus()                        // 分配pci_bus对象
 |   |   |   |- list_add_tail(&b->node, &pci_root_buses)   // 添加到pci_root_buses全局链表
 |   |   |   |- b->number = b->secondary = bus             // 记录总线号
 |   |   |   |                                             // number表示当前总线的编号
 |   |   |   |                                             // primary表示当前总线的上层总线编号
 |   |   |   |                                             // secondary表示总线所在PCI桥的次层总线编号(也就是当前总线自己),所以总是等于number (额外搞个成员变量,估计是为了提高代码可读性)
 |   |   |   |  /* pci_dev对象的resource[DEVICE_COUNT_RESOURCE]:
 |   |   |   |      PCI设备本身占用:
 |   |   |   |       resource[0~5]:设备上可能有的内存地址区间
 |   |   |   |       resource[6]:设备上可能有的扩充ROM区间
 |   |   |   |      PCI桥是一种特殊的PCI设备,额外包含次层总线占用的4个区间:
 |   |   |   |       resource[7]:I/O地址窗口
 |   |   |   |       resource[8]:内存地址窗口
 |   |   |   |       resource[9]:"可预取"内存地址窗口
 |   |   |   |       resource[10]:扩充ROM区间窗口 */
 |   |   |   |  // pci_bus对象的*resource[4],分别指向所在PCI桥的&resource[7~10]
 |   |   |   |- b->resource[0] = &ioport_resource   // resource[0]:可由当前总线使用的I/O地址区间 (由于是主总线,所以指向整个&ioport_resource)
 |   |   |   |- b->resource[1] = &iomem_resource    // resource[1]:可由当前总线使用的内存地址区间 (由于是主总线,所以指向整个&iomem_resource)
 |   |   |
 |   |   |  // 开始扫描
 |   |   |- if (b)
 |   |       |- b->ops = ops  // 次层总线的ops,同样设置为pci_root_ops
 |   |       |- b->subordinate = pci_do_scan_bus(b)  // 逐个发现连接在总线上的设备,为其建立pci_dev对象
 |   |           |- for (devfn = 0; devfn < 0x100; devfn += 8// 每次扫描1个PCI插槽
 |   |           |   |- dev0.devfn = devfn
 |   |           |   |- pci_scan_slot(&dev0)
 |   |           |       |- for (func = 0; func < 8; func++, temp->devfn++// 每个PCI设备,最多可以有8个功能,相当于8个逻辑设备 (逻辑设备号 = 5位设备号+3位功能号)
 |   |           |           |
 |   |           |           |- if (func && !is_multi)  // 0功能号,只在多功能设备中才需要处理
 |   |           |           |   |- continue            // 疑问:这里为什么不直接break??
 |   |           |           |
 |   |           |           |  // 读取"配置寄存器组"头部中的PCI_HEADER_TYPE字段,根据它的最高位是否为1,判断设备是否为单功能,从而决定下一轮循环是否需要继续
 |   |           |           |- pci_read_config_byte(temp, PCI_HEADER_TYPE, &hdr_type)  // pci_read_config_byte()函数,根据PCI_OP(read, byte, u8 *)宏展开定义
 |   |           |           |   |- dev->bus->ops->read_byte()  // ops通常为pci_direct_conf1 (见pcibios_init()入口,对pci_root_ops的设置)
 |   |           |           |       |
 |   |           |           |       |- pci_conf1_read_config_byte()            // 如果ops=&pci_direct_conf1
 |   |           |           |       '   |- outl(CONFIG_CMD(dev,where), 0xCF8// 按照PCI规范,向地址寄存器(0xCF8),写入"总线号,逻辑设备号,寄存器偏移(where)"
 |   |           |           |       '   |        |- (0x80000000 | (dev->bus->number << 16) | (dev->devfn << 8) | (where & ~3))
 |   |           |           |       '   |- *value = inb(0xCFC + (where&2))     // 从数据寄存器(0xCFC),读取设备的PCI_HEADER_TYPE寄存器值
 |   |           |           |       '
 |   |           |           |       '- pci_bios_read_config_byte()             // 如果ops=&pci_bios_access
 |   |           |           |           |- ...                                 // 设置参数:PCIBIOS_READ_CONFIG_WORD
 |   |           |           |           |- lcall &pci_indirect                 // 调用BIOS的PCI服务接口 (段基址、偏移,已分别由初始化和check_pcibios()函数设置)
 |   |           |           |
 |   |           |           |- temp->hdr_type = hdr_type & 0x7f  // 7位代表设备类型:
 |   |           |           |                                    // PCI_HEADER_TYPE_NORMAL(普通PCI设备)、PCI_HEADER_TYPE_BRIDGE(PCI-PCI桥)、PCI_HEADER_TYPE_CARDBUS(PCI-CardBus桥)
 |   |           |           |
 |   |           |           |- dev = pci_scan_device(temp)       // 读取逻辑设备的出厂信息
 |   |           |           |   |                                // "配置寄存器组",一方面提供软件对设备的配置界面,另一方面,携带设备的出厂信息
 |   |           |           |   |  // 读取"配置寄存器组"头部中的PCI_HEADER_TYPE字段,低16位为厂商编号,高16位为设备编号
 |   |           |           |   |- pci_read_config_dword(temp, PCI_VENDOR_ID, &l)
 |   |           |           |   |- if (l == 0xffffffff || l == 0x00000000 || l == 0x0000ffff || l == 0xffff0000// 0或全1,都为无效的厂商编号、备编号
 |   |           |           |   |   |- return NULL
 |   |           |           |   |- dev = kmalloc(sizeof(*dev), GFP_KERNEL)  // 分配pci_dev对象
 |   |           |           |   |- ...                                      // 初始化pci_dev对象,设置厂商编号、设备编号
 |   |           |           |   |- pci_setup_device(dev)                    // 根据设备类型,进一步填充pci_dev对象
 |   |           |           |       |
 |   |           |           |       |  // 读取"配置寄存器组"头部中的PCI_CLASS_REVISION字段
 |   |           |           |       |- pci_read_config_dword(dev, PCI_CLASS_REVISION, &class// 相比表示大类的hdr_type,class对设备类型进一步细分,其中包含:
 |   |           |           |       |                                                          // PCI_CLASS_DEVICE(高16位)、PCI_CLASS_PROG(次低8位)、PCI_REVISION_ID(低8位)
 |   |           |           |       |- switch (dev->hdr_type)
 |   |           |           |           |
 |   |           |           |           |- case PCI_HEADER_TYPE_NORMAL      // 普通PCI设备
 |   |           |           |           |   |
 |   |           |           |           |   |- if (class == PCI_CLASS_BRIDGE_PCI)
 |   |           |           |           |   |   |- goto bad                 // 可以看出,只要不是"PCI-PCI"桥,都属于0型设备
 |   |           |           |           |   |
 |   |           |           |           |   |  // 读取"配置寄存器组"头部中的PCI_INTERRUPT_PIN和PCI_INTERRUPT_LINE字段
 |   |           |           |           |   |- pci_read_irq(dev)
 |   |           |           |           |   |   |  // PCI_INTERRUPT_PIN字段,表示设备与PCI插槽之间,中断请求信号线的连接关系 (由硬件决定,只读)
 |   |           |           |           |   |   |  // 0:表示设备不产生中断请求; 1~4:分别表示设备与PCI插槽的INTA~INTD中断请求信号线连接
 |   |           |           |           |   |   |- pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &irq)
 |   |           |           |           |   |   |- if (irq)
 |   |           |           |           |   |   |   |  // PCI_INTERRUPT_LINE字段,表示设备经过PCI插槽,最终连接了中断控制器的哪根中断请求信号线 (由软件设置)
 |   |           |           |           |   |   |   |  // PCI_INTERRUPT_LINE字段只起记录作用,不起控制作用,修改其值,并不会修改连接关系,不过连接关系通常也可以由软件设置
 |   |           |           |           |   |   |   |  // 如果这时读取的irq不为0,一定是因为已由BIOS设置,内核后续不会对其进行修改
 |   |           |           |           |   |   |   |- pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &irq)
 |   |           |           |           |   |   |
 |   |           |           |           |   |   |- dev->irq = irq  // 记录到pci_dev对象 (对于irq为0的情况,会在后续调用pcibios_lookup_irq()函数时,进行处理)
 |   |           |           |           |   |
 |   |           |           |           |   |  // 读取"配置寄存器组"头部中的PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5和PCI_ROM_ADDRESS字段
 |   |           |           |           |   |- pci_read_bases(dev, 6, PCI_ROM_ADDRESS)
 |   |           |           |           |   |   |
 |   |           |           |           |   |   |  // PCI设备中,通常包含寄存器或RAM"(可挥发)功能存储区间"(最多6个),PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5寄存器,用于记录它们的出厂信息,以及配置它们的映射地址
 |   |           |           |           |   |   |- for(pos=0; pos<howmany; pos = next)
 |   |           |           |           |   |   |   |
 |   |           |           |           |   |   |   |  // 对于已由BIOS配置过的寄存器,内核后续不会对其进行修改,只会在检查到配置错误的情况下,对其进行修正
 |   |           |           |           |   |   |   |- reg = PCI_BASE_ADDRESS_0 + (pos << 2) // PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5寄存器,长度都为4字节
 |   |           |           |           |   |   |   |- pci_read_config_dword(dev, reg, &l)   // 读取区间起始地址
 |   |           |           |           |   |   |   |- pci_write_config_dword(dev, reg, ~0// 写入全1 (根据PCI规范)
 |   |           |           |           |   |   |   |- pci_read_config_dword(dev, reg, &sz)  // 读取区间大小
 |   |           |           |           |   |   |   |- pci_write_config_dword(dev, reg, l)   // 恢复原始值
 |   |           |           |           |   |   |   |
 |   |           |           |           |   |   |   |  // 起始地址,bit0=0,表示内存地址区间,bit0=1表示I/O地址区间
 |   |           |           |           |   |   |   |  /* 区间类型由映射到什么区间决定,而跟存储单元是什么介质(内存/寄存器),以及所在的硬件设备无关
 |   |           |           |           |   |   |   |     Linux内核通常优先选择使用内存空间:
 |   |           |           |           |   |   |   |     1. 有些CPU没有I/O指令,因而没有I/O空间,寄存器也就都在存储器地址空间中
 |   |           |           |           |   |   |   |     2. 有些CPU有I/O指令,但是指令的寻址空间很小,比如i386 CPU的I/O指令,操作数最多16位,即I/O空间为64KB,非常拥挤
 |   |           |           |           |   |   |   |     3. C语言没有访问I/O地址空间的语言成分,需要调用汇编子过程,不方便而且效率也稍低 */
 |   |           |           |           |   |   |   |- if ((l & PCI_BASE_ADDRESS_SPACE) == PCI_BASE_ADDRESS_SPACE_MEMORY)
 |   |           |           |           |   |   |   |   |
 |   |           |           |           |   |   |   |   |  // 内存区间,起始地址低4位为控制信息:
 |   |           |           |           |   |   |   |   |  // bit0:区间类型 (0: 内存区间; 1: I/O区间)
 |   |           |           |           |   |   |   |   |  // bit1:区间大小是否超过1MB (0: 未超过; 1: 超过)
 |   |           |           |           |   |   |   |   |  // bit2: 区间地址位数 (0: 32位; 1: 64位)
 |   |           |           |           |   |   |   |   |  // bit3: 区间是否"可预取" (0: 否; 1:是)
 |   |           |           |           |   |   |   |   |  //       对于普通的内存单元,读操作是不会修改其内容的,但是如果内存区间是由寄存器区间映射的,内容就有可能被读操作修改:
 |   |           |           |           |   |   |   |   |  //       1. 比如FIFO寄存器,读走队列头的元素后,下一个元素就成为首个元素,存入FIFO寄存器了
 |   |           |           |           |   |   |   |   |  //       2. 有些状态寄存器,状态被读出之后,寄存器就会硬件自动清0
 |   |           |           |           |   |   |   |   |  //       "预读"是指,程序在读第N个单元时,CPU会把第N+1个单元也读入缓存:
 |   |           |           |           |   |   |   |   |  //       对内容可能被读操作修改的存储单元,进行"预读",就会存在问题:
 |   |           |           |           |   |   |   |   |  //       比如,程序在读第N个单元时,CPU会把第N+1个单元也读入缓存,但在缓存被冲刷掉之前,程序并没有取走第N+1个单元,程序后面再想重新读取时,内存单元却已经被"预读"修改了
 |   |           |           |           |   |   |   |   |  //       所以,通常将映射到内存空间的寄存器区间,"可预取"标志置为0
 |   |           |           |           |   |   |   |   |- res->start = l & PCI_BASE_ADDRESS_MEM_MASK
 |   |           |           |           |   |   |   |   |
 |   |           |           |           |   |   |   |   |  // PCI规范规定,高28位中,只有位置最低的1,用于表示区间大小,而出厂值还有其它位为1,是为了表示哪些位可以由软件设置,所以不能忽略
 |   |           |           |           |   |   |   |   |- sz = pci_size(sz, PCI_BASE_ADDRESS_MEM_MASK)
 |   |           |           |           |   |   |   |       |
 |   |           |           |           |   |   |   |       |  // 假设:size = x100
 |   |           |           |           |   |   |   |       |  // 则:size-1 = x100-1 = x011
 |   |           |           |           |   |   |   |       |  //     => ~(size-1) = X100  (其中:X & x = 0)
 |   |           |           |           |   |   |   |       |  //                  & x100  (size)
 |   |           |           |           |   |   |   |       |  //                 --------
 |   |           |           |           |   |   |   |       |  //                  = 0100  (高位x不管为多少,都转换为0了)
 |   |           |           |           |   |   |   |       |- size = size & ~(size-1)
 |   |           |           |           |   |   |   |       |- return size-1
 |   |           |           |           |   |   |   |- else
 |   |           |           |           |   |   |   |   |
 |   |           |           |           |   |   |   |   |  // I/O区间,起始地址低3位为控制信息
 |   |           |           |           |   |   |   |   |- res->start = l & PCI_BASE_ADDRESS_IO_MASK
 |   |           |           |           |   |   |   |   |- sz = pci_size(sz, PCI_BASE_ADDRESS_IO_MASK & 0xffff)
 |   |           |           |           |   |   |   |
 |   |           |           |           |   |   |   |- res->end = res->start + (unsigned long) sz
 |   |           |           |           |   |   |   |- ... // 暂不关心64位地址的情况
 |   |           |           |           |   |   |
 |   |           |           |           |   |   |  // PCI设备中,有时也会包含ROM"(不可挥发)功能存储区间"(最多1个),PCI_ROM_ADDRESS寄存器,用于记录它的出厂信息,以及配置它的映射地址
 |   |           |           |           |   |   |- if (rom)
 |   |           |           |           |   |       |- ... // 和PCI_BASE_ADDRESS_0~PCI_BASE_ADDRESS_5处理过程类似
 |   |           |           |           |   |
 |   |           |           |           |   |  // 读取其它字段
 |   |           |           |           |   |- pci_read_config_word(dev, PCI_SUBSYSTEM_VENDOR_ID, &dev->subsystem_vendor)
 |   |           |           |           |   |- pci_read_config_word(dev, PCI_SUBSYSTEM_ID, &dev->subsystem_device)
 |   |           |           |           |
 |   |           |           |           |- case PCI_HEADER_TYPE_BRIDGE      // PCI-PCI桥
 |   |           |           |           |   |
 |   |           |           |           |   |- if (class != PCI_CLASS_BRIDGE_PCI)
 |   |           |           |           |   |   |- goto bad                 // 可以看出,仅"PCI-PCI"桥,属于1型设备
 |   |           |           |           |   |
 |   |           |           |           |   |  // 相比普通PCI设备,很多配置寄存器在PCI-PCI桥中不存在,并且PCI-PCI桥上,最多只有2个可挥发存储区间
 |   |           |           |           |   |- pci_read_bases(dev, 2, PCI_ROM_ADDRESS1)  // 不过次层PCI总线,需要后续进行递归扫描
 |   |           |           |           |
 |   |           |           |           |- case PCI_HEADER_TYPE_CARDBUS     // PCI-CardBus桥 (专用于笔记本,暂不关心)
 |   |           |           |               |- ...
 |   |           |           |
 |   |           |           |- pci_name_device(dev)  // 根据厂商和设备编号,获取厂商和设备名称 (转换表存储于drivers/pci/pci.ids文件)
 |   |           |           |
 |   |           |           |- if (!func)
 |   |           |           |   |- is_multi = hdr_type & 0x80                  // 是否多功能
 |   |           |           |- list_add_tail(&dev->global_list, &pci_devices)  // 将pci_dev对象添加到pci_devices全局链表
 |   |           |           |- list_add_tail(&dev->bus_list, &bus->devices)    // 将pci_dev对象添加到所属pci_bus的devices链表
 |   |           |           |
 |   |           |           |  // 有些设备出售后,厂商却发现固化在"配置寄存器组"中的初始化信息有问题,只好提供修正代码,在内核层面进行修改
 |   |           |           |- pci_fixup_device(PCI_FIXUP_HEADER, dev)
 |   |           |               |
 |   |           |               |  // 数组元素表示:对于由vendor厂商提供的device设备,在pass阶段,执行hook()修正函数
 |   |           |               |  /* pcibios_fixups[]:不同CPU架构,使用各自的
 |   |           |               |     pci_fixups[]:所有CPU架构共用
 |   |           |               |     都是pci_fixup对象全局数组,以{0}元素结束 */
 |   |           |               |  // 疑问:如果存在PCI BIOS,加电自检阶段,就已经基于错误的出厂信息,枚举过设备了,到内核执行阶段才修复,来得及吗??
 |   |           |               |- pci_do_fixups(dev, pass, pcibios_fixups)
 |   |           |               |- pci_do_fixups(dev, pass, pci_fixups)
 |   |           |                   |
 |   |           |                   |  // dev包含vendor和device信息
 |   |           |                   |  // pass为PCI_FIXUP_HEADER,表示"配置寄存器组"头部信息读出以后,PCI_FIXUP_FINAL表示全部信息读出以后
 |   |           |                   |- while (f->pass)
 |   |           |                       |- if (f->pass == pass &&
 |   |           |                       |   |  (f->vendor == dev->vendor || f->vendor == (u16) PCI_ANY_ID) &&
 |   |           |                       |   |  (f->device == dev->device || f->device == (u16) PCI_ANY_ID))
 |   |           |                       |   |
 |   |           |                       |   |- f->hook(dev)  // pass,vendor,device全部命中,执行对应的修正函数
 |   |           |                       |- f++
 |   |           |
 |   |           |  // 不光PCI设备中的出厂信息会有错,有些母板也存在设计问题
 |   |           |- pcibios_fixup_bus(bus)
 |   |           |   |
 |   |           |   |  // 去除"幻影" (有些母板上,会在两个不同的综合地址中,读出同一设备的头部信息,从而造成重复枚举,在形成的PCI树中引入"幻影")
 |   |           |   |- pcibios_fixup_ghosts(b)
 |   |           |   |
 |   |           |   |  // pci_scan_slot()函数,只读取了PCI设备"功能存储区间"的信息,还没读取PCI桥为次层总线保存的地址窗口
 |   |           |   |- pci_read_bridge_bases(b)
 |   |           |       |
 |   |           |       |  /* PCI桥是一种特殊的PCI设备,本身并不一定有RAM和ROM,但是要有3个地址窗口 (也用resource对象描述):
 |   |           |       |     1. I/O地址窗口 (窗口起始地址和大小,都以4KB对齐,PCI_IO_BASE和PCI_IO_LIMIT低4位全0时,表示I/O地址为16位,全1表示I/O地址为32位)
 |   |           |       |        PCI_IO_BASE(8位): I/O起始地址(16位) = PCI_IO_BASE高4+ 12*0
 |   |           |       |        PCI_IO_LIMIT(8位): I/O结束地址(16位) = PCI_IO_LIMIT高4+ 12*1
 |   |           |       |        PCI_IO_BASE_UPPER16(16位): I/O起始地址(32位) = PCI_IO_BASE_UPPER16 + PCI_IO_BASE高4+ 12*0
 |   |           |       |        PCI_IO_LIMIT_UPPER16(16位): I/O结束地址(32位) = PCI_IO_LIMIT_UPPER16 + PCI_IO_LIMIT高4+ 12*1
 |   |           |       |     2. 存储器地址窗口 (窗口起始地址和大小,都以1MB对齐,主要用于映射在存储器地址空间的I/O寄存器,最低4位固定为0)
 |   |           |       |        PCI_MEMORY_BASE(16位): 存储器起始地址(32位) = PCI_MEMORY_BASE高12+ 20*0
 |   |           |       |        PCI_MEMORY_LIMIT(16位): 存储器结束地址(32位) = PCI_MEMORY_BASE高12+ 20*1
 |   |           |       |     3. "可预取"存储器地址窗口 (窗口起始地址和大小,都以1MB对齐,最低4位为0表示地址为32位,为1表示地址为64位)
 |   |           |       |        PCI_PREF_MEMORY_BASE(16位): "可预取"存储器起始地址(32位) = PCI_PREF_MEMORY_BASE高12+ 20*0
 |   |           |       |        PCI_PREF_MEMORY_LIMIT(16位): "可预取"存储器结束地址(32位) = PCI_PREF_MEMORY_LIMIT高12+ 20*1
 |   |           |       |        PCI_PREF_BASE_UPPER32(32位): "可预取"存储器起始地址(64位) = PCI_PREF_BASE_UPPER32 + PCI_PREF_MEMORY_BASE高12+ 20*0
 |   |           |       |        PCI_PREF_LIMIT_UPPER32(32位): "可预取"存储器结束地址(64位) = PCI_PREF_BASE_UPPER32 + PCI_PREF_MEMORY_LIMIT高12+ 20*1
 |   |           |       |     CPU一侧访问的地址,必须在某个窗口之内(位于当前总线),才能到达下游,设备一侧访问的地址,必须在所有窗口之外(不在当前总线),才能到达上游
 |   |           |       |     此外,PCI命令寄存器("配置寄存器组"头部中的PCI_COMMAND字段),包含"memory access enable""I/O access enable"两个控制位
 |   |           |       |     控制位为0时,窗口对2个方向都关闭,用于保证还没为PCI设备各个区间分配合适的总线地址时,CPU和设备两侧不会相互干扰 */
 |   |           |       |- for(i=0; i<3; i++)
 |   |           |       |   |- child->resource[i] = &dev->resource[PCI_BRIDGE_RESOURCES+i]  // pci_bus对象中的resource只是指针,指向所在PCI桥的pci_dev对象的resource
 |   |           |       |
 |   |           |       |  // 读取PCI_IO_BASE,PCI_MEMORY_BASE,PCI_PREF_MEMORY_BASE等寄存器,记录到pci_bus对象的resource[0~2]
 |   |           |       |  // 对于i386 CPU,PCI桥的地址窗口,必然已在加电自检阶段,就由BIOS根据次层总线的需要,设置好了!!
 |   |           |       |- ...
 |   |           |
 |   |           |  // 递归扫描PCI-PCI桥或PCI-CardBus桥上的次层总线
 |   |           |- for (pass=0; pass < 2; pass++// 第一趟针对已由BIOS处理过的PCI桥
 |   |               |                              // 第二趟针对未经BIOS处理过的PCI桥 (疑问:对于i386 CPU,为什么会存在这种情况??)
 |   |               |- for (ln=bus->devices.next; ln != &bus->devices; ln=ln->next// 遍历上层总线上面的PCI设备
 |   |                   |- dev = pci_dev_b(ln)
 |   |                   |- if (dev->hdr_type == PCI_HEADER_TYPE_BRIDGE || dev->hdr_type == PCI_HEADER_TYPE_CARDBUS)  // 设备为"PCI-PCI""PCI-CardBus"
 |   |                       |
 |   |                       |  //  pci_scan_bridge()函数写的有点绕:
 |   |                       |  //                       | if ((buses & 0xffff00)) | pass | run    | 目的
 |   |                       |  // ----------------------+-------------------------+------+--------+------------------------------------------------------
 |   |                       |  //  已由BIOS处理,第一趟 |            Y            |  0   | 772行  | 第一趟仅为已由BIOS枚举的总线分配pci_bus对象,并统计最大总线号max
 |   |                       |  //  未经BIOS处理,第一趟 |            N            |  0   | return | 等待第一趟扫描完毕,max值完全确认后,在第二趟扫描中处理
 |   |                       |  //  已由BIOS处理,第二趟 |            Y            |  1   | return | 已经在第一趟扫描中处理过了
 |   |                       |  //  未经BIOS处理,第二趟 |            N            |  1   | 792行  | 基于已由BIOS处理的最大总线号max,处理未经BIOS处理的总线
 |   |                       |- max = pci_scan_bridge(bus, dev, max, pass)
 |   |                           |
 |   |                           |  // 读取"配置寄存器组"头部中的PCI_PRIMARY_BUS字段 (byte0:主总线号; byte1:次总线号; byte2:子树中最大总线号)
 |   |                           |- pci_read_config_dword(dev, PCI_PRIMARY_BUS, &buses)
 |   |                           |
 |   |                           |  // 次总线号和子树中最大总线号不全为0,表示加电自检阶段,BIOS已经枚举过当前总线,在此之前就已经设置过了
 |   |                           |- if ((buses & 0xffff00) && !pcibios_assign_all_busses())
 |   |                           |   |                          |- 空操作
 |   |                           |   |- if (pass)   // 已由BIOS枚举的总线,第二趟扫描不需要处理
 |   |                           |   |   |- return max
 |   |                           |   |
 |   |                           |   |- child = pci_add_new_bus(bus, dev, 0// 为新探测到的总线,分配和设置pci_bus对象 (逻辑简单,不再展开)
 |   |                           |   |- ...                                   // 继续设置pci_bus对象
 |   |                           |   |- if (!is_cardbus)
 |   |                           |   |   |- cmax = pci_do_scan_bus(child)     // 递归调用 (遇到次层总线,立即递归扫描,而不是探测到上层总线的所有次层总线后,再依次扫描次层总线,所以是深度优先)
 |   |                           |   |   |- if (cmax > max) max = cmax
 |   |                           |   |- else
 |   |                           |       |- cmax = child->subordinate         // PCI-CardBus桥不支持次层总线
 |   |                           |       |- if (cmax > max) max = cmax
 |   |                           |- else
 |   |                           |   |- if (!pass// 未经BIOS处理的总线,等待第一趟扫描完毕
 |   |                           |   |   |- return max
 |   |                           |   |
 |   |                           |   |  // 未经BIOS处理的PCI桥,相关配置寄存器,就得由内核设置 (读写过程涉及PCI规范)
 |   |                           |   |- pci_read_config_word(dev, PCI_COMMAND, &cr)
 |   |                           |   |- pci_write_config_word(dev, PCI_COMMAND, 0x0000)
 |   |                           |   |- pci_write_config_word(dev, PCI_STATUS, 0xffff)
 |   |                           |   |- child = pci_add_new_bus(bus, dev, ++max)             // 为新探测到的总线,分配和设置pci_bus对象
 |   |                           |   |- buses = (buses & 0xff000000)
 |   |                           |   |        | ((unsigned int)(child->primary)     <<  0)
 |   |                           |   |        | ((unsigned int)(child->secondary)   <<  8)
 |   |                           |   |        | ((unsigned int)(child->subordinate) << 16)   // 按照PCI_PRIMARY_BUS寄存器的组成规范,进行拼接
 |   |                           |   |- pci_write_config_dword(dev, PCI_PRIMARY_BUS, buses)  // 将总线号写入PCI_PRIMARY_BUS寄存器
 |   |                           |   |- if (!is_cardbus)
 |   |                           |   |   |- max = pci_do_scan_bus(child)                     // 递归调用
 |   |                           |   |- else
 |   |                           |   |   |- max += 3
 |   |                           |   |- child->subordinate = max
 |   |                           |   |- pci_write_config_byte(dev, PCI_SUBORDINATE_BUS, max)
 |   |                           |   |- pci_write_config_word(dev, PCI_COMMAND, cr)
 |   |                           |
 |   |                           |- return max
 |   |

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2023-7-24 17:35 被jmpcall编辑 ,原因: 大图上传后看不清了,替换为文字上传
上传的附件:
收藏
免费 8
支持
分享
最新回复 (3)
雪    币: 10
活跃值: (424)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
2023-7-15 17:34
0
雪    币: 3738
活跃值: (3872)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
感谢分享!
2023-7-15 17:59
0
雪    币: 3059
活跃值: (30876)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2023-7-15 23:05
1
游客
登录 | 注册 方可回帖
返回
//