ACPI热插拔在系统中的实现
1、系统初始化阶段
在上电阶段PCI设备的扫描节点,ACPI模式根节点(主桥)被PCI总线驱动枚举,从驱动程序的角度上来看可以把root看作一个PCI桥,包含4个地址区间用于描述PCI桥下次一级的总线的地址分配情况,通过扫描PCI root得到root,把从ACPI BIOS获得的资源(中断,总线号)绑定在PCI的root的描述结构acpi_pci_root上。
1.初始化root :acpi_pci_root_init是初始化root的第一步,在这里首先调用注册函数acpi_bus_register_driver,它的带入参数是acpi设备的驱动程序描述结构struct acpi_driver acpi_pci_root_driver,该函数注册支持ACPI PCI总线的驱动程序,同样一个ACPI PCI桥也和在名字空间创建之后,搜索名字空间中所有设备的类型和名字,并且把它们绑定到相对应的驱动程序上,启动过程中搜索名字空间中已经定义的ACPI PCI,ACPI桥设备都使用一个acpi_device数据结构表示,这个数据结构是从ACPI的层次上描述设备,支持ACPI的PCI桥和标准的PCI-PCI桥使用相同的模式遍历,但是在ACPI中可以使用(_ADR,_BBN,_PRT)从名字空间中获得相关的ACPI PCI桥资源信息(例如设备地址资源,总线号,设备的ID号,中断路由),当对acpi_device的所有的字段都完成初始化之后,就会调用acpi_pci_root_add把得到的root的数据结构挂在acpi_pci_root队列上,顾名思义这个队列就是系统内所有的root的集合。
2.初始化支持Hot Plug的root插槽:在完成root的初始化之后,接着就是要对ACPI PCI Hot Plug的插槽进行初始化,在系统初始化ACPI HotPlug驱动模块的时候,会首先遍历在acpi_pci_root队列上的所有ACPI root,并且调用Hot Plug驱动模块acpi_pci_register_driver中的add_bridge方法把当前的具备热拔插功能ACPI的root都找到。
如何确定是否支持热拔插呢?在Linux HotPlug中有一个函数detect_ejectable_slots检测是否有可以支持热拔插的SLOT(插槽以acpihp_slot结构表示),插槽是在名字空间中定义的,可以理解为物理设备上的插槽,也可以理解为在PCI桥或者root下的一个负责支持或者管理热拔插的设备,插槽上包含了功能模块(function),表示功能是针对具体设备,如果找到支持热拔插的插槽则表示当前的root是支持热拔插的,反之就会对当前的root下的PCI总线做深度遍历,寻找到下一级的支持Hot Plug的ACPI PCI-PCI桥;
热拔插的SLOT是连接在宿主桥也可以连接在PCI-PCI桥之上的,在宿主桥之下有PCI-PCI桥,它采用传统的PCI桥递归方式查找下一级总线的PCI桥;宿主桥获取资源(IO和MEM)是从地址空间中获得。从名字空间中调用_CRS对象找到地址资源后挂上当前的桥描述结构的资源树上。而PCI-PCI桥的资源则是从当前桥设备上的配置空间中获得, 例如在名字空间中,下面是某个设备内的资源表示:
| Device(EC0) { ... ... Name(_CRS,Buffer(){//表示该_CRS所归属的设备的系统空间,IO或者MEM,中断等资源 IO(Decode16,FC00,FC03,4,4)//表示IO区间宽度为16个Bit,开始地址为FC00结束为FC03,4个字节对//齐,长度为4个字节 IRQ(Level,ActiveHigh,Shared{5}//表示当前的中断为电平,高电平有效,共享的,系统中断5 } ) ... ... } 例1-5 嵌入式控制器EC0的CRS对象在名字空间中的描述 |
通过acpi_evaluate_integer调用指定设备的方法_BBN和_SEG,获得段号和总线号然后调用add_host_bridge初始化PCI宿主桥上的资源,(IO,MEM)获得总线资源:
| status = acpi_walk_resources(handle, METHOD_NAME__CRS, decode_acpi_resource, bridge); |
资源在ACPI中是作为一个对象节点保存在BIOS中的名字空间,acpi_walk_resources函数使用指定的_CRS方法可以读指定设备的资源对象,放在acpi_resource结构中,然后调用decode_acpi_resource对资源对象解析出合适的资源存储空间,IO空间,下级地址,总线地址,预存取存储空间(采用pci_resource数据结构),把解析完毕的资源绑定在桥芯片描述结构的四个资源队列树上(mem_head,io_head,bus_head,p_mem_head),然后调用专门的函数acpiphp_resource_sort_and_combine对资源树进行整理,并且回收/合并一些相连的资源区间;在获取地址空间资源上,传统PCI-PCI桥和PCI宿主桥是一致的。
完成了PCI-PCI桥和PCI宿主桥的资源配置之后,就是调用init_bridge_misc来初始化这两种根集中器,以及一些已经在PCI总线上的设备,并且完成对这些设备的枚举,另外在这里对ACPI PCI系统或者是设备事件的处理句柄的注册,当PCI桥出现了GPE事件发生时刻处理句柄的注册,会用于对这些事件的响应:
首先调用decode_hpp(调用acpi_evaluate_object)获得ACPI hot-plug的一些参数,例如Cache Line的长度,PCI总线延迟周期(以PCI总线的时钟来记数),PERR和SERR使能寄存器。
然后调用acpiphp_detect_pci_resource把在PCI总线上完成枚举的设备上已经占用的资源从桥的资源列表减掉,这样在PCI桥之下留下来的就是空闲的资源树。
遍历ACPI的名字区间,得到指定设备类型的ACPI设备的对象,并且调用相关的用户函数对其进行初始化,这里遍历函数中调用句柄register_slot,参数acpiphp_bridge *bridge为当前的PCI宿主桥或者是PCI-PCI桥结构,如下:
status = acpi_walk_namespace(ACPI_TYPE_DEVICE, bridge->handle, (u32)1,register_slot, bridge, NULL); |
register_slot是一个回调函数,遍历所有的已经存在于名字空间中的PCI总线上的PCI设备(也就是PCI的功能function)。遍历的过程中,有的设备是不支持拔插的,那么直接跳过,我们知道在Linux驱动中PCI的每个功能都是结构pci_dev,每个PCI插槽(SLOT)中对应有8个PCI功能设备(function),而在ACPI的名字空间中则列出每个PCI功能设备(下面的例子是一个关于Hot-plug方式的名字空间,例子中就非常明白地显示出每个PCI槽相对应的功能function)。并且为已经在名字空间中体现出来的设备建立数据描述结构acpiphp_func *newfunc,如果设备存在(也许是Legacy设备或者是已经在启动时候插入的热插拔板卡)那么就对其进行对应的资源初始化工作(acpiphp_init_func_resource),直接把在PCI设备配置空间中的地址资源读出,最后把设备挂在对应的插槽结构中(acpiphp_slot)。如果对应的设备没有插槽结构acpiphp_slot,那么就重新建立新的插槽结构。
最后调用acpi_install_notify_handler把控制句柄handle_hotplug_event_func安装在桥上等待ACPI系统事件和设备事件的触发,这两类事件将引发热拔插的后ACPI热拔插系统的动作。
2、发生拔插事件的处理
在设备插入后,有两个比较重要的GPE会从设备端或者是系统端发生:ACPI_NOTIFY_BUS_CHECK,ACPI_NOTIFY_DEVICE_CHECK,他们分别表示Bus Check事件和Device Check事件,都通告当前的PCI总线发生了变化,这两个事件的含义见表1-1,其实两者的处理模式基本是一致的,我们介绍一下Device Check的执行流程:
(1) 首先是遍历当前宿主桥下所有的SLOT,通过调用其_STA方法获得当前每个插槽上的状态,如果没有_STA方法,就直接检查当前的SLOT上的厂商号是否为全F,如果不是,表示有设备插入。
(2) 当获得当前状态是ACPI_STA_ALL(表示所有的功能模块均有效)的时候,表示当前的设备上需要使能所有的功能模块,这个时候就要调用使能一个SLOT的函数enable_slot。
(3) 使能一个SLOT的第一步是给当前设备上电--power_on_slot,调用当前设备的_PS0方法。
(4) 第二步是使能并且配置一个SLOT--enable_device,在enable_device中的函数acpiphp_configure_slot为每个插槽上的功能模块(function)进行初始化和配置工作,首先检查每个function是否存在,如果不存在,表明该模块是没有对应设备的,这样才进一步调用函数init_config_space对找到的每一个功能块进行初始化,对于一个标准的PCI设备(function),一般有6个存储空间,init_config_space程序将依次扫描6个存储空间,根据每个存储空间的长度(mem/pmem/io)从桥的地址空间"批发"地址资源到pci_resource *res中,并且写入当前找到的PCI功能模块(function)的配置空间中。
(5) 接下来调用pci_scan_slot扫描每个Slot上设备的功能模块,这里已经进入PCI层的初始化阶段,在上段中已经对ACPI通告的PCI设备的IO/MEM/Pre-MEM的PCI配置空间中的地址寄存器配置完毕,现在就在从PCI配置空间进一步读入设备信息,并根据设备的具体类型进一步初始化设备,获取PCI的IO和存储空间的基地址(pci_dev结构中的资源字段resource),中断输入脚(pci_read_irq函数会读出PCI_INTERRUPT_PIN/PCI_INTERRUPT_LINE寄存器中的内容,它表示了当前PCI设备的中断输出到8259A的中断输入或者是IO APIC的中断输入上),在PCI核心层上完成PCI设备结构(pci_dev结构)的构建工作。从而最终脱离ACPI层到达PCI设备层。
说到中断,ACPI层的PCI中断和标准的PCI设备的处理方式有一些不一样;对于ACPI层来说,这里为ACPI引入了一个新的中断表--PCI Routing Table,它在BIOS中保存了对PCI设备的中断输入脚映射到中断控制器的输入,需要在初始化ACPI子系统的以后,由设备驱动层调用pci_enable_device在PCI层次上初始化设备以及其驱动,特别是使能中断输入和使能配置空间中的各个IO/MEM空间,这里需通过_PRT获取某个设备中断输入;从_PRT方法获得的PCI设备中断引脚映射将覆盖从PCI枚举阶段所建立的PCI设备到中断控制器之间的中断映射;传统的PCI设备是使用中断修正的方式来进行PCI设备的中断映射。
| name(_PRT,Package{/*名字空间中PRT采用Package的方式包装中断定义*/ Package{0x0004fff,0,LINKA,8},/*设备的基地址为0x00004fff,中断请求线为中断A,中断输入为8*/ ... ... Package{0x0004fff,3,LINKD,8},/*设备的基地址为0x00004fff,中断请求线为中断D,中断输入为8*/ }) 例1-5 显卡中的_PRT表在名字空间中的部分描述 |
_PRT实质上是和PCI设备从BIOS中获得的中断路径表类似,对ACPI中断驱动初始化而言是实现从名字空间设备的_PRT节点中获取中断资源并且修正中断过程,从上面的表可以看出,设备中断脚到中断控制器的中断路径事实上也是被定义在_PRT中的,ACPI中规定如果是硬件上直接定义了中断输入脚到中断控制器的路径,那么就确定表示使用那个全局的中断(例如系统中断8),而中断INTA-INTD就不会被定义。
所以对于使用ACPI Hot-Plug PCI设备的驱动程序而言,一定需要使能PCI设备的有效资源,对中断重新安排,所以在热拔插的设备在完成插入之后,需要手工使用/sbin/modprobe命令载入设备内核驱动模块,调用.probe设备驱动接口,绝大部分的热拔插都是使用PCI共享中断,所以一般来说会在PRT方法中直接定义成系统的共享中断,直接获取设备_PRT的中断。
3、内核流程分析
1、acpiphp_enumerate_slots分析:
首先判断是否为主桥设备,当时主桥设备时,初始化struct acpiphp_root_context *root_context结构,然后加入&bridge_list全局链表中,遍历名称空间,调用acpiphp_add_context函数,在这个函数中,评估DSDT中pci主桥下设备的ADR对象,不存在或出错返回。获取成功则初始化设备节点的热插拔上下文,具体为初始化struct acpiphp_context *context结构:
(1) context->hp.notify = acpiphp_hotplug_notify热插拔事件处理函数:
获取指定设备的热插拔上下文。调用hotplug_event,处理当发生拔插事件:ACPI_NOTIFY_BUS_CHECK,ACPI_NOTIFY_DEVICE_CHECK,他们分别表示Bus Check事件和Device Check事件。
ACPI_NOTIFY_BUS_CHECK --- Bus Check事件。此通知在设备对象上执行,以指示OSPM需要在设备树上执行即插即用的重新枚举操作,该操作从通知它的位置开始。OSPM通常会在启动时自动执行完整的枚举,但是在系统初始化之后,ACPI AML代码有责任在需要重新枚举操作时通知OSPM。通知在设备树中越准确、越接近实际的变化,操作系统的响应就越有效; 但是,当无法确认设备更改时,也可能出现问题。例如,如果硬件在系统休眠状态下无法识别某个特定位置的设备更改,它会在唤醒时发出一个总线检查通知,通知OSPM需要检查设备更改的配置。
ACPI_NOTIFY_DEVICE_CHECK --- 设备检查。用于通知OSPM设备出现或消失。如果设备已经出现,OSPM将从父节点重新枚举。如果设备已经消失,OSPM将使设备状态无效。OSPM可以优化我们的重新枚举。如果_DCK存在,则假定Notify(object,1)表示取消停靠请求。如果设备是桥,OSPM可以重新枚举桥和子总线。
- 当为ACPI_NOTIFY_BUS_CHECK时,当为桥时:检测桥下设备是否支持热插拔,评估_STA对象,正常则检测移除没有响应的pci设备。如设备存在且有响应,则使能设备的所有功能(enable_slot)。否则将禁用设备。当为设备时,调用enable_slot使能设备(没有置位slot->flags & SLOT_IS_GOING_AWAY)。
- 当为ACPI_NOTIFY_DEVICE_CHECK时,当为桥时,和上述流程相同。当为设备时,重新扫描pci slot。
- 当为ACPI_NOTIFY_EJECT_REQUEST时,是pci设备的配置信息无效,遍历slot中所有func,检测DSDT中是否有_EJ0方法,有则执行_EJ0方法将设备移除。
2、热插拔调用栈
[ 50.496093] [<ffffffff8021be2c>] show_stack+0x9c/0x130
[ 50.500000] [<ffffffff80f0bc20>] dump_stack+0xb0/0xf0
[ 50.503906] [<ffffffff8087c790>] acpiphp_hotplug_notify+0x30/0x258
[ 50.507812] [<ffffffff808e2380>] acpi_device_hotplug+0xb0/0x500
[ 50.515625] [<ffffffff808d8298>] yun : Got handle forn+0x20/0x38
[ 50.519531] [<ffffffff80286588>] process_one_work+0x160/0x370
[ 50.527343] [<ffffffff802868f4>] worker_thread+0x15c/0x558
[ 50.531250] [<ffffffff8028d47c>] kthread+0x11c/0x150
[ 50.535156] [<ffffffff80213524>] ret_from_kernel_thread+0x14/0x1c
中断调用流程
[ 74.433593] CPU: 0 PID: 0 Comm: swapper/0 Tainted: G O 4.19.90+ #138
[ 74.433593] Bus check in hotplug_event()
[74.441406]Hardware name: Loongson Loongson-LS3A3000-7A1000-1w-V1.2-Dev/Loongson-LS3A3000-7A1000-1w-V1.2-Dev, BIOS Loongson-UDK2018-V1.5.7 04/15/2020
[ 74.441406] Stack : ffffffffd500cce0 ffffffff802c7848 ffffffffd500cce0 78375c2ea329aaf8
[ 74.441406] 78375c2ea329aaf8 0000000000000000 980000045e02fcb8 000000000000db78
[ 74.457031] 0000000000000000 000000000000000a ffffffffffffffff 0000000000000001
[ 74.457031] 000000000000008a 0000000000000002 ffffffff81441738 ffffffff81440000
[ 74.472656] ffffffff81440000 ffffffff81240000 0000000000000000 ffffffff812d0000
[ 74.472656] ffffffff811189c8 ffffffff812d0000 000000000000006f 0000000000000000
[ 74.511718] 980000045fbf6b00 0000000000000002 ffffffff80959258 0000000000000000
[ 74.519531] ffffffff81430000 ffffffff81230000 980000045e02fcb0 980000045e02fee4
[ 74.527343] ffffffff80f0bc20 0000000000000000 0000000000000000 0000000000000000
[ 74.539062] 0000000000000000 0000000000000000 ffffffff8021be2c 78375c2ea329aaf8
[ 74.546875] ...
[ 74.546875] Call Trace:
[ 74.550781] [<ffffffff8021be2c>] show_stack+0x9c/0x130
[ 74.554687] [<ffffffff80f0bc20>] dump_stack+0xb0/0xf0
[ 74.558593] [<ffffffff808f8d70>] acpi_ev_sci_xrupt_handler+0x28/0x70
[ 74.566406] [<ffffffff808d83a0>] acpi_irq+0x20/0x68
[ 74.570312] [<ffffffff802cc3c4>] __handle_irq_event_percpu+0xbc/0x198
[ 74.578125] [<ffffffff802cc4d0>] handle_irq_event_percpu+0x30/0x90
[ 74.585937] [<ffffffff802cc580>] handle_irq_event+0x50/0x90
[ 74.589843] [<ffffffff802d181c>] handle_level_irq+0xf4/0x1a0
[ 74.593750] [<ffffffff802cb1f4>] generic_handle_irq+0x2c/0x48
[ 74.601562] [<ffffffff80f31228>] do_IRQ+0x18/0x28
[ 74.605468] [<ffffffff80207ae0>] ls7a_msi_irq_dispatch+0x238/0x298
[ 74.613281] [<ffffffff80213ab8>] except_vec_vi_end+0xb8/0xc4
[ 74.617187] [<ffffffff802137a0>] __r4k_wait+0x20/0x40