首页 理论教育PCI设备如何获取IRQ号|PCIExpress体系结构导读

PCI设备如何获取IRQ号|PCIExpress体系结构导读

【摘要】:在Linux系统中,PCI设备使用的irq号存放在pdev→irq参数中,该参数在Linux设备驱动程序进行初始化时,由pci_enable_device函数设置。本书在第12.3.2节曾简要介绍过这个函数,下文进一步说明如何使用该函数设置PCI设备的irq号。在acpi_prt_list链表中存放PCI总线的中断路由表,本章将在第15.1.2节进一步介绍该表。其中PCI设备使用低电平触发方式。值得注意的是,PCI设备的INTA#信号首先与LPC的PIRQA#信号相连,而PIRQA#信号再与I/O APIC1的IRQ_PIN16相连。

在Linux系统中,PCI设备使用的irq号存放在pdev→irq参数中,该参数在Linux设备驱动程序进行初始化时,由pci_enable_device函数设置。本书在第12.3.2节曾简要介绍过这个函数,下文进一步说明如何使用该函数设置PCI设备的irq号。pci_enable_device函数将依次调用__pci_enable_device_flags→do_pci_enable_device→pcibios_enable_device函数设置PCI设备使用的irq号。

pcibios_enable_device函数将调用pcibios_enable_irq函数,设置PCI设备使用的irq号。如果处理器系统使能了ACPI机制,pcibios_enable_irq函数将被赋值为acpi_pci_irq_enable。acpi_pci_irq_enable函数在./drivers/acpi/pci_irq.c文件中,其实现过程如源代码15-1所示。

源代码15-1 acpi_pci_irq_enable函数

978-7-111-29822-9-Part03-70.jpg

978-7-111-29822-9-Part03-71.jpg

该函数首先调用acpi_pci_irq_lookup→acpi_pci_irq_find_prt_entry函数,从acpi_prt_list链表中获得一个acpi_prt_entry结构的Entry。在acpi_prt_list链表中存放PCI总线的中断路由表,本章将在第15.1.2节进一步介绍该表。在这个Entry中,存放PCI设备使用的Segment、Bus、Device和Function号,PCI设备使用的中断请求信号(INTA#~INTD#)和GSI(Global System Interrupt)号。

这段程序在获得Entry后,将判断Entry→link是否为空,如果为空,表示当前x86处理器系统使用I/O APIC管理外部中断,而不是使用8259A。在Intel的ICH9中集成了两个中断控制器,一个是8259A,另一个是I/O APIC。Linux x86通过软件配置,决定究竟使用哪个中断控制器,在绝大多数情况下,Linux x86使用I/O APIC而不是8259A管理外部中断请求200。本章不再关心8259A中断控制器,因此也不再关心Entry→link不为空的处理情况[25]

这段程序在获得GSI号之后,将调用acpi_register_gsi函数,将GSI号转换为系统软件使用的irq号。acpi_register_gsi函数使用三个入口参数,分别为GSI号,中断触发方式和采用电平触发时的极性。其中PCI设备使用低电平触发方式。

acpi_register_gsi函数执行完毕后,将为PCI设备分配一个irq号,这个irq号是系统软件使用的,之后PCI设备的驱动程序可以使用request_irq函数将中断服务例程与irq号建立映射关系;该函数还将设置I/O APIC的REDIR_TBL表,将GSI号与REDIR_TBL表中的中断向量建立对应关系,同时初始化与操作系统相关的irq结构[26]。为了深入理解acpi_register_gsi函数,读者需要理解GSI号、I/O APIC的REDIR_TBL表、IRQ_PIN引脚和Linux使用的irq号之间的对应关系。

GSI号是ACPI规范引入的,用于记录I/O APIC的IRQ_PIN引脚号的参数。如果x86处理器系统使用I/O APIC管理外部中断请求,而且在这个处理器系统中具有多个I/O APIC控制器,那么GSI号与I/O APIC中断引脚号的对应关系如图15-1所示。

978-7-111-29822-9-Part03-72.jpg

图15-3 GSI和IO APIC中断引脚号的对应关系

假设在一个x86处理器系统中存在3个I/O APIC,其中有两个I/O APIC的外部中断引脚数为24根,另外一个I/O APIC的外部中断引脚数为16根。其中GSI号的0~23与I/O APIC1的IRQ_PIN0~23对应;GSI号的24~39与I/O APIC2的IRQ_PIN0~15对应;而GSI号的40~55与I/O APIC3的IRQ_PIN0~23对应。ACPI规范为统一起见使用GSI号描述外部设备与I/O APIC中断引脚的连接关系。

I/O APIC的IRQ_PIN引脚与外部设备的中断请求引脚相连,如I/O APIC1的IRQ_PIN16与某个PCI设备的INTA#相连。值得注意的是,PCI设备的INTA#信号首先与LPC的PIRQA#信号相连,而PIRQA#信号再与I/O APIC1的IRQ_PIN16相连。其中I/O APIC集成在ICH中,因此这些IRQ_PIN引脚并没有从ICH中引出。

REDIR_TBL表中存放对IRQ_PIN引脚的描述,一个I/O APIC具有多少个IRQ_PIN引脚,REDIR_TBL表就由多少项组成。该表的每一个Entry由多个字段组成,其中本节仅对这个Entry的Ve ctor字段感兴趣,Vector字段是这个Entry的第7~0位,存放对应IRQ_PIN引脚使用的中断向量。

在Linux系统中,与IRQ_PIN引脚对应的中断向量由acpi_register_gsi函数设置,当x86处理器系统使用I/O APIC管理外部中断时,acpi_register_gsi函数将调用mp_register_gsi函数。mp_register_gsi函数在./drivers/acpi/boot.c文件中定义,其实现机制如源代码15-2所示。我们假定在Linux系统中使能了CONFIG_X86_32选项。

源代码15-2 mp_register_gsi函数

978-7-111-29822-9-Part03-73.jpg

978-7-111-29822-9-Part03-74.jpg

这段程序首先根据GSI号,使用mp_find_ioapic和mp_find_ioapic_pin函数,确定当前PCI设备与I/O APIC中断控制器的哪个IRQ_PIN引脚相连(GSI号与I/O APIC和IRQ_PIN引脚的对应关系如图15-1所示)。(www.chuimin.cn)

然后mp_register_gsi函数调用io_apic_set_pci_routing函数设置I/O APIC中的寄存器。在Linux x86的源代码中,mp_register_gsi函数调用io_apic_set_pci_routing函数时,有一个并不恰当的处理,在mp_register_gsi函数中使用GSI号作为io_apic_set_pci_routing函数的第二个入口参数,但是io_apic_set_pci_routing函数要求的这个输入参数是irq号。

在Linux x86系统中,irq号是一个纯软件[27]概念,而这段代码的作用实际上是令GSI号直接等于irq号。笔者认为这种方法并不十分恰当,因为GSI号用来描述I/O APIC的IRQ_PIN输入引脚,而irq号是设备驱动程序用来挂接中断服务例程的。

本节在此强调这个问题,主要为了读者辨明GSI号和irq号的关系,目前在Linux x86系统中,PCI设备使用的GSI号与irq号采用了“直接相等”[28]的一一映射关系,实际上,GSI号并不等同于irq号。在系统软件的实现中,两者只要建立一一映射的对应关系即可,并不一定要“直接相等”。还有一点需要提醒读者注意,就是不同的PCI设备可以共享同一个GSI号,即共享I/O APIC的一个IRQ_PIN引脚,从而在Linux系统中共享同一个irq号。

io_apic_set_pci_routing函数调用__io_apic_set_pci_routing→setup_IO_APIC_irq操作I/O APIC中的寄存器。setup_IO_APIC_irq是一个重要函数,如源代码15-3所示。

源代码15-3 setup_IO_APIC_irq函数

978-7-111-29822-9-Part03-75.jpg

978-7-111-29822-9-Part03-76.jpg

该函数首先调用assign_irq_vector→_assign_irq_vector函数将外部设备使用的GSI号与I/O APIC中REDIR_TBL表建立联系,并将其结果记录到CPU的vector_irq表中。这个步骤非常重要,在Linux x86系统中,如果存在多个CPU,那么每一个CPU都有一个vector_irq表,这张表中包含了vector号与irq号的对应关系。这张表也是处理器硬件与系统软件联系的桥梁

处理器硬件并不知道irq号的存在,而仅仅知道vector号,而Linux x86系统使用的是irq号。在处理外部中断请求时,Linux系统需要通过vector_irq表将vector号转换为irq号才能通过irq_desc表找到相关设备的中断服务例程。

setup_ioapic_entry函数将初始化entry参数。该参数是一个IO_APIC_route_entry类型的结构。而ioapic_register_intr函数调用set_irq_chip_and_handler_name函数设置irq_desc[irq]变量,并将这个变量的chip参数设置为ioapic_chip,handle_irq参数设置为handle_fasteoi_irq,这个步骤对于Linux x86中断处理系统非常重要。

ioapic_write_entry函数将保存在entry参数中的数据写入到与GSI号对应的REDIR_TBL表中,该函数将直接操作I/O APIC的寄存器。

由以上描述,我们可以发现当acpi_pci_irq_enable函数执行完毕后,Linux系统将GSI号与irq号建立映射关系,同时又将irq号与I/O APIC中的vector号进行映射,并将这个映射关系记录到vector_irq表中,这个映射表由操作系统使用。之后该程序还将初始化I/O APIC的REDIR_TBL表,将PCI设备使用的GSI号与I/O APIC的vector号联系在一起。

在x86处理器系统中,PCI设备的INTx引脚首先与LPC的PIRQA~H引脚直接相连,而LPC中的PIRQA~H引脚将与I/O APIC的IRQ_PIN16~23引脚相连。当PCI设备通过INTx引脚提交中断请求时,最终将传递到IRQ_PIN16~23引脚。而I/O APIC接收到这个中断请求后,将根据REDIR_TBL表与“IRQ_PIN16~23引脚”对应的Entry向Local APIC发送中断请求消息,处理器通过Local APIC收到这个中断请求后,将执行中断处理程序进一步处理这个来自PCI设备的中断请求。

Linux x86系统使用do_IRQ函数处理外部中断请求,该函数在./arch/x86/kernel/irq.c文件中,如源代码15-4所示。

源代码15-4 do_IRQ函数

978-7-111-29822-9-Part03-77.jpg

do_IRQ函数首先获得vector号,这个vector号由I/O APIC传递给Local APIC,并与某个IRQ_PIN引脚对应,其描述在I/O APIC的REDIR_TBL表中。vector号是一个硬件概念,x86处理器系统在处理外部中断请求时,仅仅知道vector号的存在,而不知道irq号。

Linux x86系统通过vector_irq表,将vector号转换为irq号,之后执行handle_irq函数进一步处理这个中断请求。对于PCI设备,这个handle_irq函数将调用handle_fasteoi_irq函数,而handle_fasteoi_irq函数将最终执行PCI设备使用的中断服务例程。handle_fasteoi_irq函数的源代码在./kernel/irq/chip.c文件中,本节对该函数不做进一步分析。

在PCI设备的Linux驱动程序中,将使用request_irq函数将其中断服务例程挂接到系统中断服务处理程序中。