理解linux pci 扫描流程

时间:2022-12-19 20:37:30
本文主要描述linux kernel进行pci总线扫描,linux kernel代码基于版本2.6.37
1. 域
最先需要说明一个概念--域
百度百科:域之原义指地方的范围,后逐渐演变为数学、生物、科技等学科的某类单位的分类词语。
域英文叫DOMAIN (a set of websites on the Internet which end with the same group of letters,for exemple'.com','org'-------《牛津高阶英汉双解词典》)
出现在本文中,意义要窄一些,认为域是一个使用同一编址方式的范围。
比如:
pci域,使用pci地址进行寻址
x86域,使用x86物理地址进行寻址
域有主导者,访问都通过它进行
pci域,主导者是pci root,如果需要访问一个pci设备,就需要通过主导者pci root进行,它能识别的地址是pci地址
x86域,主导者是x86 cpu,如果需要访问一个内存,就需要通过主导者x86 cpu进行,它能识别的地址是x86物理地址
如果引申一下有物理地址,线性地址,逻辑地址,虚拟地址,在另一篇文章中有提到。
本文中将的是pci域,所以所有的寻址都是使用pci地址

2. pci总线
简单的介绍一下pci,有意愿的话,可以认真看看pci协议,到处都有得下,英文的。
2.1 pci设备
pci有三种设备,有一种叫cardbus没接触过不提,所以认为两种,普通设备和桥设备
普通设备的功能是系统需要的具体功能,具体功能最终也会归到数据传输,数据处理上面去。
如果一个pci设备是这么组成的
pci总线接口 -- 内存 -- 网络数据处理器 -- 网络接口
网络数据通过网络接口进到网络数据处理器,处理后放到pci设备的内存上,再通过pci总线接口将数据发送到pci总线上,于是另一个pci设备就能够获取网络数据,反方向就是发送网络数据
很明显,这个pci设备就是一个pci网卡
类似的
pci总线接口 -- 内存 -- 视频数据处理器 -- 视频接口
一个pci显卡
pci总线接口 -- 内存 -- 音频数据处理器 -- 音频接口
一个pci声卡

桥设备的功能是扩展pci总线,根据pci协议,一个总线上支持32个dev,每个dev支持8中功能,数量不够多,扩展功能是必要的。
命令lspci -t查看拓扑
-[0000:00]-+-00.0
           +-02.0
           +-1b.0
           +-1c.0-[20]--
           +-1c.1-[3f]----00.0
           +-1d.0
           +-1d.1
           +-1d.2
           +-1d.3
           +-1d.7
           +-1e.0-[05]----04.0
           +-1f.0
           +-1f.1
           \-1f.2
pci总线0下有多个设备
普通设备00.0, 02.0, 1b.0, 1d.0-7, 1f.0-2
桥设备1c.0, 1c.1, 1e.0
桥1c.0扩展总线20, 1c.1扩展总线3f, 1e.0扩展总线05
总线20下无设备,总线3f下有设备3f:00.0,总线05下有设备05:04.0

2.2 pci配置空间
pci是一种总线,特点是灵活可配置,因为它有一个专门的区域叫配置空间。
配置空间通过io总线访问,x86是0xCF8,0xCFC,一个是控制端口一个是数据端口
命令可查看
sh-4.1# cat /proc/ioports 
...
0cf8-0cff : PCI conf1
...
桥设备和普通设备的配置空间内容不一样
桥设备的配置空间需要说明pci总线的拓扑,内存映射的地址范围等等
普通设备的配置空间需要说明该设备 内存映射的地址范围,pci的中断号等等
讲配置空间的网上有很多很优秀的文章,不贴过来了

2.3 pci内存映射空间
考虑一个pci声卡
pci总线接口 -- 内存 -- 音频数据处理器 -- 音频接口
音频数据处理器处理完的数据存放到pci设备的内存中,如何在pci总线*问到这块内存从而获取其中的音频数据呢?
前面说pci配置空间说明内存映射空间地址范围,即将这块内存映射到pci总线域的哪块地址,这样在pci总线域中访问这块地址就是访问这块内存
内存映射空间一般有一个或者多个,看一个真实的显卡
00:02.0 VGA compatible controller: Intel Corporation 82945G/GZ Integrated Graphics Controller (rev 02) (prog-if 00 [VGA controller])
        Subsystem: Hewlett-Packard Company Device 3010
        Flags: bus master, fast devsel, latency 0, IRQ 16
        Memory at e0400000 (32-bit, non-prefetchable) [size=512K]
        I/O ports at 20c0 [size=8]
        Memory at d0000000 (32-bit, prefetchable) [size=256M]
        Memory at e0480000 (32-bit, non-prefetchable) [size=256K]
        Expansion ROM at <unassigned> [disabled]
        Capabilities: <access denied>
        Kernel driver in use: i915
        Kernel modules: i915
这里面的信息都是从配置空间获取的,可以看到有4个内存映射空间
        Memory at e0400000 (32-bit, non-prefetchable) [size=512K]
        I/O ports at 20c0 [size=8]
        Memory at d0000000 (32-bit, prefetchable) [size=256M]
        Memory at e0480000 (32-bit, non-prefetchable) [size=256K]
只是第二个有些特殊是io port的映射区间,另外的三个都是真实的内存映射区间,第三个对应的就是显卡上的256MB显存

3. 扫描过程
经过前面内容,可以猜想pci设备的初始化流程应该是
a. 通过io总线配置pci配置空间
b. 通过io总线读取pci配置空间信息,获取内存映射空间的映射地址信息
c. 通过读写内存映射空间实现数据传输
看上去显得很简单,但是简单的说就是这么简单
如果只有一个总线,下面又只挂了一个pci设备,那么我认为上面的流程及足够了,上面的流程也是pci驱动初始化pci设备的基本流程。
由于大量的pci桥和pci普通设备有可能出现在pci总线上,所以实际情况比这个复杂
3.1 BIOS pci 扫描
最先进行pci扫描的是x86上的BIOS,扫描的结果是各个pci普通设备和pci桥都被配置完成,但是这种配置有可能是不能用的。
原因需要回到域的概念,简单的考虑,x86系统中存在两个域,x86域和pci域,由于x86系统的特殊性,两个域被设计成对等映射的关系。
也就是说,虽然x86域中使用x86地址进行访问,pci域中使用pci地址进行访问,但是两者对同一个实体的地址是相等的。这样带来的问题是两个域的地址不能重合,需要分开。
命令cat /proc/iomem可查看
00000000-00000fff : reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : Video RAM area
000c0000-000c7fff : Video ROM
000cd400-000e7fff : pnp 00:0e
000e8000-000fffff : reserved
  000f0000-000fffff : System ROM
00100000-7f7c82ff : System RAM
  00400000-007aca75 : Kernel code
  007aca76-00a1cf6f : Kernel data
  00aa8000-00c2b8bf : Kernel bss
7f7c8300-7fffffff : reserved
80000000-801fffff : PCI Bus 0000:20
80200000-803fffff : PCI Bus 0000:20
80400000-805fffff : PCI Bus 0000:3f
80600000-806fffff : PCI Bus 0000:05
  80600000-8061ffff : 0000:05:04.0
d0000000-dfffffff : 0000:00:02.0
e0400000-e047ffff : 0000:00:02.0
e0480000-e04bffff : 0000:00:02.0
e04c0000-e04c3fff : 0000:00:1b.0
  e04c0000-e04c3fff : ICH HD audio
e04c4000-e04c43ff : 0000:00:1d.7
  e04c4000-e04c43ff : ehci_hcd
e0500000-e07fffff : PCI Bus 0000:3f
  e0500000-e050ffff : 0000:3f:00.0
    e0500000-e050ffff : tg3
e0800000-e0afffff : PCI Bus 0000:05
  e0a00000-e0a000ff : 0000:05:04.0
    e0a00000-e0a000ff : r8169
f0000000-f3ffffff : reserved
  f0000000-f3ffffff : pnp 00:0e
    f0000000-f1ffffff : PCI MMCONFIG 0000 [bus 00-1f]
fec00000-fed3ffff : reserved
  fec00000-fec003ff : IOAPIC 0
  fec01000-fed3ffff : pnp 00:0e
fed45000-ffffffff : reserved
  fee00000-fee00fff : Local APIC
后面一段基本都是pci设备,前面一段基本都是ram,两者没有重合部分
回到BIOS配置可能不可用的问题,如果linux kernel将系统内存也安排到了BIOS配置pci的地址区域,出现重合,系统就乱了。

3.2 kernel扫描
既然BIOS配置可能不可用,那么只能kernel自己重新扫一次,合理安排pci地址空间和x86空间的地址了。
内核有个配置选项与此相关
CONFIG_PCI_BIOS
CONFIG_PCI_MMCONFIG
CONFIG_PCI_DIRECT
网上有相关说明,不贴了
3.2.1 扫描流程
cat System.map | grep pci | grep __initcall 
ffffffff8170c818 t __initcall_pci_reboot_init1
ffffffff8170c8c8 t __initcall_pcibus_class_init2
ffffffff8170c8d0 t __initcall_pci_driver_init2
ffffffff8170c918 t __initcall_acpi_pci_init3
ffffffff8170c940 t __initcall_pci_arch_init3
ffffffff8170c9b8 t __initcall_pci_slot_init4
ffffffff8170c9d0 t __initcall_acpi_pci_root_init4
ffffffff8170c9d8 t __initcall_acpi_pci_link_init4
ffffffff8170ca48 t __initcall_pci_subsys_init4
ffffffff8170caf0 t __initcall_pcibios_assign_resources5
ffffffff8170cb18 t __initcall_pci_apply_final_quirks5s
ffffffff8170cb28 t __initcall_pci_iommu_initrootfs
ffffffff8170ced0 t __initcall_pci_proc_init6
ffffffff8170ced8 t __initcall_pcie_portdrv_init6
ffffffff8170cfa8 t __initcall_serial8250_pci_init6
ffffffff8170cfd0 t __initcall_hsu_pci_init6
ffffffff8170d010 t __initcall_ide_scan_pcibus6
ffffffff8170d410 t __initcall_pci_resource_alignment_sysfs_init7
ffffffff8170d418 t __initcall_pci_sysfs_init7
ffffffff8170d440 t __initcall_pci_mmcfg_late_insert_resources7
这些都是在kernel启动过程中会执行的pci相关函数,有些明显不是通用的可以忽略
整个扫描流程分为两部分 扫描设备 和 分配资源
3.2.1.1 扫描设备
从主桥开始扫
pcibios_scan_root(0)
>>> pci_scan_bus_parented()
    >>> pci_scan_child_bus()
        >>>  for (devfn = 0; devfn < 0x100; devfn += 8)
pci_scan_slot(bus, devfn);
前面有说一个总线下支持32个dev,每个dev支持8个fn,这里就是扫描总线下所有的dev
所有的dev有普通pci设备,还有pci桥
pci桥会扩展出另一个总线号,其下可能挂有其他设备,所以还需要扫描找到的pci桥
            >>> pci_scan_single_device()
                >>> pci_scan_device()
                    >>> pci_setup_device()
                        >>> pci_read_irq()
                        >>> pci_read_bases()
                            >>> __pci_read_base()
                                >>> pci_size()
获取pci设备信息,中断号,内存映射区域大小和地址等等
                >>> pci_device_add()
                    >>> list_add_tail(&dev->bus_list, &bus->devices)
扫到的设备全存放在 bus->devices链表中,后面会有很多扫描依赖这个链表
常见的比如: list_for_each_entry(dev, &bus->devices, bus_list)
就是循环该bus下所有pci设备
        >>> pci_scan_bridge()
            >>> pci_find_bus()
            >>> pci_add_new_bus()
桥扩展出来的总线给一个新的总线号
            >>> pci_scan_child_bus()
递归扫描新扫到的pci桥
这样是扫描到桥就往深处走,称深度优先扫描,将所有的pci设备扫描完毕,并获取内存映射区域信息

3.2.1.2  分配资源
从主桥开始
__pci_bus_assign_resources()
>>>  pbus_assign_resources_sorted()
    >>> __dev_sort_resources()
        >>> pdev_sort_resources()
            >>> pci_resource_alignment()
                >>> resource_alignment()
                    >>> resource_size()
将bus下所有dev所需要占用的资源都放入一个链表上--head
    >>> __assign_resources_sorted()
        >>> assign_requested_resources_sorted()
            >>> resource_size()
            >>> pci_assign_resource()
                >>> pci_resource_alignment()
                >>> __pci_assign_resource()
                    >>> pci_bus_alloc_resource()
                    ### allocate a resource from a parent bus
从bus所含资源中给链表head上所注明资源分配空间
                        >>> allocate_resource()
                            >>> find_resource()
在制定范围内寻找合适的空间[min, max]
min = (res->flags & IORESOURCE_IO) ? PCIBIOS_MIN_IO : PCIBIOS_MIN_MEM;
max = PCIBIOS_MAX_MEM_32;
匹配方式可看resource_clip(),移动最小匹配
                            >>> __request_resource()
分配
                    >>> request_resource_conflict()
                        ### request and reserve an I/O or memory resource
                        >>> __request_resource()
                    >>> pci_update_resource()
                        >>> pci_resource_bar()
更新配置空间
        >>> adjust_resources_sorted()
            >>> 
>>> __pci_bus_assign_resources()
### recursion
>>> pci_setup_bridge()
更新桥的配置空间,桥所包含地址范围
    >>> __pci_setup_bridge()
        >>> pci_setup_bridge_io()
            >>> res = bus->resource[0]
            >>> pcibios_resource_to_bus(bridge, &region, res)
            ### write to io base and limit
        >>> pci_setup_bridge_mmio()
            >>>  res = bus->resource[1]
            >>> pcibios_resource_to_bus(bridge, &region, res)
             ### write to mem base and limit
        >>> pci_setup_bridge_mmio_pref()
             >>> res = bus->resource[2]
            >>>  pcibios_resource_to_bus(bridge, &region, res)
            ###  write to pref mem base and limit

下面只是列出较具体流程,代码就不贴了
3.2.2  pci_reboot_init
>>> dmi_check_system(pci_reboot_dmi_table)
只有apple相关产品会做一些修正,不关心

3.2.3 pcibus_class_init
>>> class_register(&pcibus_class)
    >>> release_pcibus_dev()
        >>> pci_bus_remove_resources()
注册pci class

3.2.4  pci_driver_init()
>>> bus_register(&pci_bus_type)
    >>> bus_register(&pci_bus_type)
        >>> pci_bus_match()
            >>> pci_match_device()
                >>> pci_match_id()
                    >>> pci_match_one_device()
                    ### id->vendor == dev->vendor, id->device == dev->device
        >>> pci_uevent()
            >>> add_uevent_var()
            ### add key value string to the environment buffer
        >>> pci_device_probe()
            >>> pci_match_device()
            >>> pci_call_probe()
                >>> local_pci_probe()
                    >>> ddi->drv->probe(ddi->dev, ddi->id)
        >>> pci_device_remove()
            >>> drv->remove(pci_dev)
        >>> pci_device_shutdown()
            >>> drv->shutdown(pci_dev)
            >>> pci_msi_shutdown(pci_dev)
            >>> pci_msix_shutdown(pci_dev)
        >>> pci_dev_attrs
        >>> pci_bus_attrs
            >>> bus_rescan_store()
                >>> b = pci_find_next_bus()
                >>> pci_rescan_bus(b)
注册pci驱动,但是这里注意 pci_rescan_bus(b)在pci hotplug中是很有用的,另一篇文章有提到

3.2.5  acpi_pci_init()
从ACPI获取信息,ACPI还是很复杂的,可以简单看作一堆表示系统相关信息的表

3.2.6  pci_arch_init()
>>>  pci_direct_probe()
    >>> request_region(0xCF8, 8, "PCI conf1")
这里就是保留io总线访问配置空间的资源,0xCF8 0xCFC
>>> pci_mmcfg_early_init()
    >>> __pci_mmcfg_init()
        >>> acpi_sfi_table_parse(ACPI_SIG_MCFG, pci_parse_mcfg)
        ### mmconfig address is coming from ACPI table
MMCONFIG对于访问pcie设备配置空间很有作用,因为PCI的配置空间是256,而PCIE的配置空间是4K,多出的部分io总线访问不了,需要mmconfig进行访问
            >>> pci_parse_mcfg()
                >>> pci_mmconfig_add()
        >>> pci_mmcfg_reject_broken()
            >>> is_mmconf_reserved()
        >>> pci_mmcfg_arch_init()
>>> x86_init.pci.arch_init()
    >>> pci_acpi_init()
    ### about pci irq
>>> pci_pcbios_init()
    ​>>>  pci_find_bios()
    ### find BIOS32 0xe0000 - 0xfffff
>>> pci_direct_init()
    ###  raw_pci_ops = &pci_direct_conf1  
>>> dmi_check_pciprobe()
>>> dmi_check_skip_isa_align()

3.2.7  pci_slot_init()
>>>  pci_slots_kset = kset_create_and_add("slots", NULL,   &pci_bus_kset->kobj)

3.2.8  acpi_pci_root_init()
>>>  acpi_hest_init()
>>> pci_acpi_crs_quirks()
>>> acpi_bus_register_driver(&acpi_pci_root_driver)
### about irq

3.2.9  acpi_pci_link_init()
>>>  acpi_bus_register_driver(&acpi_pci_link_driver)
### about irq

3.2.10  pci_subsys_init()
>>> pci_legacy_init()
    >>> pci_root_bus = pcibios_scan_root(0)
        >>> bus = pci_scan_bus_parented(NULL, busnum, &pci_root_ops, sd)
            >>> b = pci_create_bus(parent, bus, ops, sysdata)
            >>> b->subordinate = pci_scan_child_bus(b)
            ### ...
    >>> pci_bus_add_devices(pci_root_bus)
        >>> pci_bus_add_device()
        ### insert newly discovered PCI devices
        >>> pci_bus_add_child()
        ### add a child bus
>>> pcibios_fixup_peer_bridges()
>>> x86_init.pci.init_irq()
>>> pcibios_init()
    >>>  pcibios_resource_survey()
        >>> pcibios_allocate_bus_resources()
        ### for PCI_BRIDGE_RESOURCES, for bridge
            >>> pci_claim_resource()
                >>> pci_find_parent_resource()
                >>> request_resource_conflict()
                    >>> __request_resource()
            >>> pcibios_allocate_bus_resources()
            ### recursion
        >>> pcibios_allocate_resources()
        ### from  PCI_STD_RESOURCES to PCI_STD_RESOURCE_END
            >>>  pci_claim_resource()
        >>>  e820_reserve_resources_late()
        >>> ioapic_insert_resources()
            >>> insert_resource(&iomem_resource, ioapic_resources)

3.2.11  pcibios_assign_resources()
>>>  pci_assign_unassigned_resources()
    >>> __pci_bus_size_bridges()
        >>> __pci_bus_size_bridges()
        >>> pci_bridge_check_ranges()
        >>> pbus_size_io()
        >>> pbus_size_mem()
    >>> __pci_bus_assign_resources()
        >>> pbus_assign_resources_sorted()
        >>> __pci_bus_assign_resources()
        >>> pci_setup_bridge()
            >>> __pci_setup_bridge()
                >>> pci_setup_bridge_io()
                >>> pci_setup_bridge_mmio()
                >>> pci_setup_bridge_mmio_pref()
    >>> pci_enable_bridges()
    >>> pci_bus_dump_resources()

3.3 kernel扫描关键的四个函数
3.3.1 
pci_scan_child_bus()
>>> pci_scan_slot()
### scan a PCI slot on a bus for devices
    >>> pci_scan_single_device()
        >>>  pci_get_slot()
        ### check devfn is exist or not under bus
        >>>  pci_scan_device()
        ### check VID and DID to decide devfn is exist or not
            >>> alloc_pci_dev()
            >>> pci_setup_device()
                >>> pci_fixup_device(pci_fixup_early, dev)
                ### do early fixup here
                >>> pci_read_bases()
                ### read pci bar 0 - 5
                    >>> __pci_read_base()
                    ### read a pci bar for region and size
        >>>  pci_device_add()
            >>>  pci_fixup_device(pci_fixup_header, dev)
            >>> list_add_tail(&dev->bus_list, &bus->devices)
            ### scan all devices under one bus, then add them (dev->bus_list) to bus->devices
>>> pci_iov_bus_range()
>>> pcibios_fixup_bus()
    >>> pci_read_bridge_bases()
        >>> pci_bus_remove_resources(child)
>>> pci_read_bridge_io(child)
>>> pci_read_bridge_mmio(child)
>>> pci_read_bridge_mmio_pref(child)
        >>> pci_bus_add_resource()
>>> pci_scan_bridge()
### find a bridge, then want to find bus under the bridge in system.
    >>> pci_find_bus()
    ### find the bus
        >>> pci_do_find_bus()
            >>> pci_do_find_bus()
            ### recursion
    >>> pci_add_new_bus()
    ### do not find the bus, so add a new one
    >>> pci_scan_child_bus()
    ###  recursion
    >>>  pci_fixup_parent_subordinate_busnr()

3.3.2  pci_bus_add_devices()
>>>  pci_bus_add_device()
>>> pci_bus_add_devices()
### recursion
>>> pci_bus_add_child()

3.3.3  __pci_bus_size_bridges()
### if it is bridge, recursion
>>>  __pci_bus_size_bridges()
### for bridge
>>> pci_bridge_check_ranges()
### check support 64bit or not
>>> pbus_size_io()
### calc io resource of all devices under this bus
>>>  pbus_size_mem()
### calc mem resource of all devices under this bus

3.3.4  __pci_bus_assign_resources()
>>>  pbus_assign_resources_sorted()
    >>> __dev_sort_resources()
        >>> pdev_sort_resources()
            >>> pci_resource_alignment()
                >>> resource_alignment()
                    >>> resource_size()
    >>> __assign_resources_sorted()
        >>> assign_requested_resources_sorted()
            >>> resource_size()
            >>> pci_assign_resource()
                >>> pci_resource_alignment()
                >>> __pci_assign_resource()
                    >>> pci_bus_alloc_resource()
                    ### allocate a resource from a parent bus
                        >>> allocate_resource()
                            >>> find_resource()
                            >>> __request_resource()
                    >>> request_resource_conflict()
                        ### request and reserve an I/O or memory resource
                        >>> __request_resource()
                    >>> pci_update_resource()
                        >>> pci_resource_bar()
        >>> adjust_resources_sorted()
            >>> 
>>> __pci_bus_assign_resources()
### recursion
>>> pci_setup_bridge()
    >>> __pci_setup_bridge()
        >>> pci_setup_bridge_io()
            >>> res = bus->resource[0]
            >>> pcibios_resource_to_bus(bridge, &region, res)
            ### write to io base and limit
        >>> pci_setup_bridge_mmio()
            >>>  res = bus->resource[1]
            >>> pcibios_resource_to_bus(bridge, &region, res)
             ### write to mem base and limit
        >>> pci_setup_bridge_mmio_pref()
             >>> res = bus->resource[2]
            >>>  pcibios_resource_to_bus(bridge, &region, res)
            ###  write to pref mem base and limit

4. 总结
linux pci 扫描流程
a. BIOS先扫描,但是扫描的结果可能不能直接用。
b. linux kernel扫描分两步, 扫描设备和分配资源。
c. 扫描设备会从主桥开始,按照深度优先方式扫描所有设备,如果遇到一个桥设备则扩展一个总线号,并对该总线进行扫描。
d. 扫描会获取pci总线拓扑和各个设备所需要的资源信息。
e. 分配资源也从主桥开始,按照深度优先方式扫描所有设备,并按需为设备分配资源和更新配置信息。
f. 桥根据其下设备所分配的资源更新配置范围,以供pci总线路由。
g. x86域的地址资源和pci域的地址资源是对等映射,所以分配需考虑到两者不能冲突。