硬件處理
最近解決一個關于Linux中斷的問題,把相關機制整理了一遍,記錄在此。
不同的外部設備、不同的體系結構、不同的OS其中斷實現機制都有差別,本文對應的OS為linux3.4版本,外部設備為PCI設備、系統為X86。
概覽
中斷讓外設能夠通知CPU他需要獲得服務(讓CPU執行指定的中斷服務例程ISR)。為了達到這個目的,首先要為中斷執行做好準備,完成初始化相關的操作。包括:1、 初始化中斷控制器等相關器件(OS初始化過程中完成);2、 配置并使能外部設備(比如使用pci_enable_msix),得到irq號;在這個操作過程中,內核需要完成的大致操作是:
1、 確定該中斷的執行CPU,并在對應CPU上建立vector和irq號的對應關系(利用全局per-cpu變量vector_irq),配置中斷控制器(I/OAPIC、PIR等),可能還需要設置外部設備(比如設置MSI
Capacity registers);
2、 為對應的irq_desc初始化正確的handle_irq接口(通用邏輯接口);
3、 為對應的irq_desc初始化正確的底層chip操作接口。
3、 使用request_irq號為該中斷號指定一個服務例程;
完成了以上的初始化操作,在外設中斷到來的時候,為該中斷指定的ISR(Interrupt Service Routines)就能得到執行,這個執行過程大致如下:
1、 外設根據各自的配置,產生中斷信號或者中斷消息(MSI,INT# message)。2、 中斷控制器從外設獲取中斷電信號或者中斷消息,把它翻譯為vector(CPU使用這個參數來決定是誰發生了中斷,要如何處理)并提交到CPU。3、 對X86系統,CPU利用從中斷控制器獲取到的vector為索引,查詢IDT (interrupt descriptor table)得到該中斷的處理接口(對linux,是在entry_64.s中定義的函數common_interrupt接口)并執行。4、 在linux定義的common_interrupt接口中,執行完中斷執行環境建立后,會進入generic interrupt layer執行,其首先通過vector查找到irq和對應的irq_desc結構,并執行該結構的handle_irq接口,這個接口就是generic interrupt layer的通用邏輯接口,比如handle_edge_irq/handle_level_irq等;在中斷執行的通用邏輯接口中,會通過irq_desc::action調用外設指定的ISR。在linux中可以通過/proc/interrupts查看當前系統中所有中斷的統計信息,在/proc/irq/xxx(中斷號)下面,可以看到該中斷的詳細信息。
中斷相關硬件
這里的描述很多來自INTEL的文檔《Intel Software developer’s Manual, system programming guide》和《PCI Express System Architecture》
中斷控制器
中斷控制器的功能是:把外設的中斷信號,轉換成CPU能夠明白的vector,并完成中斷執行控制,確保在合適的時機把中斷提交給CPU執行。對這部分內容,《interrupt in linux》有詳細的描述。1、 8259A:每個8259A有8個管腳,每個管腳對應其連接的CPU的IDT中的一個vector,單獨使用8259A,其硬件連線就決定了對設備vector的使用。典型的場景是使用兩個8259A級聯,理論最多16個中斷號(就是ISA IRQs),實際能提供對15個中斷線的處理(master的IRQ2用于連接slave),其具體的分配見下圖。2、 PIR:用于完成輸入的信號到輸出信號的映射。在下圖中PIR被用于完成多個PCI設備的INT#信號到8259A對應引腳的路由。對應這種連接方式,在PCI設備初始化的時候,OS會根據BISO提供的信息設置PIR,把INT#路由到O0-O3中正確的管腳,從而體現到8259A的正確管腳(對應了vector),這樣INT#信號就被轉換為vector并提交到CPU。由于可能有較多的PCI設備,而PIR的輸入/出錯管腳有限,所以連接到相同輸入關鍵的INT#會共享一個中斷。
3、 I/O APIC每個I/O APIC提供24個管腳,能夠和外部設備的中斷線連接,每個管腳都可以通過配RTE(Redirection table entry)配置對應的vector。其功能是:把外部設備的中斷請求,翻譯為local APIC的interrupt message,并按照配置的vector,發送給指定的local APIC處理(在SMP系統,存在多個CPU,也就有多個local APIC)。通常的配置方式是:第一個I/O APIC的前16個管腳,配置來處理之前的ISA IRQs,其它外設比如PCI設備,則直接使用其他管腳連接。4、 local APIC其負責處理IPI(inter-process interrupt)、直接連接的中斷處理、接收和處理interrupt message,每個CPU有自己的local APIC。對應I/O APIC和local APIC的組合,其連接方式見下圖
針對X86中斷控制器硬件和linux對這些硬件的初始化,在《interrupt in linux》中有很詳細的描述。
X86對中斷的處理
Local APIC的處理過程每個local APIC對應了一個CPU。其處理interrupt message的過程如下:1、 判斷該中斷的destination是否為當前APIC,如果不是則忽略,否則繼續處理2、 如果是SMI/NMI/INIT/ExtINT, or SIPI(這些中斷都負責特殊的系統管理任務,外設一般不會使用)被直接送到CPU執行,否則執行下一步。3、 設置Local APIC 的IRR寄存器的對應bit位。4、 如果該中斷優先級高于當前CPU正在執行的中斷,且當前CPU沒有屏蔽中斷(按照X86和LINUX的實現,這時是屏蔽了中斷的),則該高優先級中斷會中斷當前正在執行的中斷(置ISR位,并開始執行),低優先級中斷會在高優先級中斷完成后繼續執行,否則只有等到當前中斷執行完成(寫了EOI寄存器)后才能開始執行下一個中斷。5、 在CPU可以處理下一個中斷的時候,從IRR中選取最高優先級的中斷,清0 IRR中的對應位,并設置ISR中的對應位,然后ISR中最高優先級的中斷被發送到CPU執行(如果其它優先級和屏蔽檢查通過)。6、 CPU執行中斷處理例程,在合適的時機(在IRET指令前)通過寫EOI寄存器來確認中斷處理已經完成,寫EOI寄存器會導致local APIC清理ISR的對應bit,對于level trigged中斷,還會向所有的I/O APIC發送EOI message,通告中斷處理已經完成。
說明:1、 關于Local APIC的IRR和ISR寄存器interrupt request register (IRR) 和 in-service register (ISR),都是256bit寄存器,每個bit對應一個中斷(其中[0-15]不能使用,SMI/NMI/INIT/ExtINT/SIPI的發送和執行不經過ISR和IRR) 。IRR中保存的是已經被local APIC接納但是還沒有開始執行的中斷;ISR中保持的是當前正在執行但是還沒有完成的中斷。2、 中斷優先級對應通過local APIC發送到CPU的中斷,按照其vector進行優先級排序:優先級=vector/16數值越大,優先級越高。由于local APIC允許的vector范圍為[16,255],而X86系統預留了[0,31]作為系統保留使用的vector,實際的用戶定義中斷的優先級的取值范圍為[2,15],在每個優先級內部,vector的值越大,優先級越高。Local APIC中還有一個關于中斷優先級的寄存器TPR(task priority register)寄存器:用于確定打斷線程執行需要的中斷優先級級別,只有優先級高于設置值的中斷才會被CPU執行 (SMI/NMI/INIT/ExtINT, or SIPI不受限制),也就是除了特殊中斷外,優先級低于TPR指定值的中斷將被忽略。3、 中斷的pending對于同一個vector,如果有多次中斷請求,可能IRR和ISR對應的bit位都被置位,也就是對同一個vector,local APIC可以pending兩個中斷,其后的即使有多處,也會被合并為一個執行。4、 中斷執行時機中斷的執行總是在指令邊界開始(只有一個特殊的exception:abort在外,出現了這個中斷,系統基本上也就完蛋了),也就是中斷不可能打斷指令的執行。
CPU對中斷和異常的處理相關概念1、 vector(中斷向量)vector是一個整數,在X86CPU上,使用vector對中斷(interrupt,外部設備產生)和異常(exception,CPU在程序執行中產生)統一編號,每個CPU核心內部,中斷/異常和vector所以一一對應的;但是在各個不同的CPU核心上,相同的vector可以對應不同的中斷(至少對于linux的設置,異常還是使用相同的vector)。vector的取值范圍為[0,255],其中[0,31]被系統保留使用(多數作為異常的vector),其余的可供外設中斷使用(系統設備比如local APIC也占用了部分[32,255]這個范圍的vector)。2、 IDT(interrupt descriptor table)X86 CPU采用一個有256個元素的數組來描述中斷/異常,該數組的index為vector;其內容包括了三種gate descriptor,用于描述一個中斷/異常的處理接口;這個數組就是IDT,CPU在收到中斷請求的時候,就利用vector獲取到對應的中斷處理接口描述并執行。3、 可屏蔽中斷通過CPU INTR管腳/local APIC接收到的中斷是可屏蔽中斷,這些中斷能夠通過清零EFLAGS的IF來屏蔽(CLI指令)。通過INT n指令生成的中斷即使使用了和外部中斷一樣的vector,也是不可屏蔽的;同樣CPU運行過程中同步產生的trap、fault、abort等異常也是不可屏蔽的。4、 NMINMI是不可屏蔽中斷(不可通過IF標志屏蔽),是通過CPU的NMI管腳發出的中斷或者通過delivery mode為NMI的方式提交的中斷。NMI中斷在執行前,CPU不僅會屏蔽其它中斷,也會屏蔽NMI中斷,直到NMI中斷處理執行完成(IRET指令被執行)。使用INT 2指令雖然能執行NMI中斷處理函數,但是相關硬件不會介入,也就是沒有相關的屏蔽NMI中斷的操作。
CPU執行中斷的過程1、 利用vector,查IDT得到中斷描述符;2、 如果中斷發生在用戶態,會首先執行stack switch切換到內核態執行;3、 依次保存EFLAGS CS IP到當前棧,如果需要(有error code的異常),把error code PUSH到當前棧。并把IF/TF位清零屏蔽可屏蔽中斷;至此,CPU完成了中斷處理程序執行環境的建立。4、 執行中斷描述符定義的中斷處理入口(IDT中指定地址的代碼);5、 根據環境執行不同的中斷退出方式,比如執行現場調度操作(retint_careful和retint_kernel),最終都會執行IRET指令;至此,中斷執行完成。異常的執行過程類似,只不過異常在執行前不會把IF位清零,只清零TF位。
PCI設備的中斷
本部分的很多內容來自《PCI Interrupts for x86 Machines under FreeBSD》和《PCI Express?Base Specification Revision 3.0》和《PCI Express System Architecture》。PCI設備的中斷有兩種模式:一種是INT#模式,一種是MSI模式。
INT#模式每個PCI設備用四個中斷信號,對應INTA#、INTB# INTC#、INTD#,這些中斷信號采用level trigger 的方式并且為低電平有效,PCI設備通過拉低對應的信號來assert對應的中斷,并在ISR訪問PCI設備的指定寄存器deassert該中斷。
中斷線和X86系統的連接這里存在兩種常見連接模式,一種是使用老的8259A+PIR的系統,一種是使用新的I/O APIC的系統。對于使用8259A的系統:PCI的中斷線連接到一個可編程的PIR設備,再通過該設備連接到8259A(見X86中斷控制器一章的圖);對于采用I/OAPIC的系統,可以使用以下的連接方式,同樣這里只畫出了一個中斷線,同時根據不同的系統配置可能存在多個I/OAPIC。除了采用直接的中斷引腳連接,PCI還支持virtual INT#,使用INT# message(Assert INT# message和deassert INT# message)的方式來使用INT#信號。
NT#模式的局限1、 中斷數量有限且不方便擴展:每個物理的PCI設備,最多只有4個中斷但是至少能支持8個function,且系統中可能存在多個PCI設備,不得不使用中斷共享的模式,影響使用性能。2、 同步問題:由于INT#中斷采用的是side channel,中斷信號和數據本身存在不同步的問題:可能在中斷到達的時候,對應的數據沒有達到,為了處理這個問題,一般采用“讀刷新”的做法,也就是在使用該設備寫入到X86的數據之前,ISR先對這個設備進行一次讀操作來確保相關數據已經寫入完成,比如讀PCI設備的中斷狀態寄存器等。
MSI/MSI-X模式在這種模式下,PCI設備通過和數據DMA一樣的通道來完成中斷處理,通過向特定地址空間(系統FSB Interrupt存儲器空間)發起一個寫操作來發起中斷。該寫操作的地址和數據信息在PCI設備初始化MSI功能的時候已經填寫到MSI Capacity registers(MSI模式)/MSI-X table(MSI-X)中(對X86,這個地址空間是FEE00000H開始的地址空間,其實就是local APIC寄存器映射的地址空間),地址信息保存在Message address register,其中包含了目標CPU信息和FSB Interrupt存儲器空間;數據中包含了該MSI中斷對應的vector,保存在Message data register中。 MCH(memory control hub)截獲這個寫操作,轉換為FSB interrupt message并向各個CPU核心廣播,local APIC接收并處理這個消息,最終觸發CPU的中斷處理過程。使用這種機制,中斷的數量不受PIR/ IOAPIC等各種器件管腳數量的限制,MSI可以支持32個中斷,而MSI-X可以達到2048個;中斷的傳遞相當直接,省略了中斷路由的過程;并且能直接從interrupt message中獲取vector信息,減少了交互過程。
初始化
相關概念和關鍵數據結構
1、 irq號:在當前系統中全局唯一,對應內核數據結構struct irq_desc,每個外設的中斷有一個irq號(體系結構預留的中斷,是沒有對應的irq_desc結構和irq號的),該irq在該中斷的生命周期內都不會改變,且和該中斷的中斷處理函數關聯;內核使用一個bitmap allocated_irqs來標識當前系統已經分配的irq;irq號的管理與底層中斷設備和配置無關,屬于Generic Interrupt Layer;對于irq號分布集中的情況,不配置CONFIG_SPARSE_IRQ,內核采用數組直接管理,數組下標就是irq號;而對于irq號比較分散的,設置CONFIG_SPARSE_IRQ,內核采用radix tree來管理所有的irq號。2、 vector號:內核使用全局bitmap used_vectors來標識那些vector被系統預留,不能被外設分配使用。3、 irq號和vector號的關聯:內核中使用per-cpu變量vector_irq來描述irq號和vector號的關聯,對每個CPU,vector_irq是一個數組,在X86架構下成員數量為256,其數組的index為vector,值為irq,如果為-1則表示該CPU上的這個vector尚未分配。4、 struct irq_desc結構,用來描述一個中斷,是內核generic interrupt layer的關鍵數據結構,其包含了中斷的大部分信息,并連接了driver層和物理中斷設備層,每個irq號對應一個該結構,共享相同irq號的中斷共享該結構。它的關鍵成員包括:
a) irq_data :為該中斷對應的物理中斷設備層相關的數據。
b) handle_irq:為該該中斷使用的通用邏輯接口。
c) action:為driver層提供的ISR信息,其為一個單向鏈表結構,所有共享該中斷的設備的ISR都鏈接在這里。
內核關鍵數據結構和相關初始化
對X86 CPU,Linux內核使用全局idt_table來表達當前的IDT,該變量定義在traps.c
gate_desc idt_table[NR_VECTORS] __page_aligned_data = { { { { 0, 0 } } }, };//初始化為全0。
對中斷相關的初始化,內核主要有以下工作:1、 設置used_vectors,確保外設不能分配到X86保留使用的vector(預留的vector范圍為[0,31],另外還有其他通過apic_intr_init等接口預留的系統使用的vector);2、 設置X86CPU保留使用的vector對應的IDT entry;這些entry使用特定的中斷處理接口;3、 設置外設 (包括ISA中斷)使用的中斷處理接口,這些中斷處理接口都一樣。4、 設置ISA IRQ使用的irq_desc;5、 把IDT的首地址加載到CPU的IDTR(Interrupt Descriptor Table Register);6、 初始化中斷控制器(下一章描述)以上工作主要在以下函數中完成:
可以看到,這個過程會完成每個中斷vector對應的idt entry的初始化,系統把這些中斷vector分成以下幾種:1、X86保留vector,這些vector包括[0,0x1f]和APIC等系統部件占用的vector,對這些vector,會記錄在bitmap used_vectors中,確保不會被外設分配使用;同時這些vector都使用各自的中斷處理接口,其中斷處理過程相對簡單(沒有generic interrupt layer的參與,CPU直接調用到各自的ISR)。2、ISA irqs,對這些中斷,在初始化過程中已經完成了irq_desc、vector_irq、以及IDT中對應entry的分配和設置,同時可以發現ISA中斷,在初始化的時候都被設置為運行在0號CPU。3、其它外設的中斷,對這些中斷,在初始化過程中僅設置了對應的IDT,和ISA中斷一樣,其中斷處理接口都來自interrupt數組。
中斷處理接口interrupt數組interrupt數組是內核中外設中斷對應的IDT entry,其在entry_64.S中定義,定義如下:
這段匯編的效果是:在代碼段,生成了一個符號irq_entries_start,該符號對應的內容是一組可執行代碼,一共(NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7組,每組為7個中斷入口,為:
每組的最后一個中斷入口不需要jmp 2f是因為其pushq_cfi(就是pushq咯)下面就是2f這個標號的地址了。(不明白的是:為什么不在jmp 2f的地方直接寫上jmp common_interrupt?非要jmp 2f,2f的地方再次jmp common_interrupt?)
而interrupt是一個數組,該數組在初始化完成后釋放,其每個數組項都是一個地址,是對應的“pushq_cfi”代碼的地址(每個代表中斷入口的標號)。系統在初始化的時候,對外設使用interrupt數組作中斷處理接口,就是在中斷發生時,執行代碼段:
初始化中斷控制器
對中斷控制器的使用基本上有三種機制:1、 中斷路由表 $PIRstruct irq_routing_table,該結構用于使用PIR和8259A的系統,在微軟的文獻《PCI IRQ Routing Table Specification》中描述了該結構詳細信息。其描述了一個PCI設備的INT#是如何連接到PIR設備的輸入端口的。其關鍵數據是一個可變長的struct irq_info數組,每個struct irq_info描述了一個PCI物理設備的4個INT#相關的中斷路由信息和對應可用的ISA IRQs的bitmap。BIOS根據相關設備的物理連接填寫該數據結構,OS在設備初始化過程中使用這些信息為使用INT#的設備分配對應的vector和irq。2、 MP tablestruct mpc_intsrc,該數據結構用于使用I/O APIC的系統中,描述系統中所有PCI設備4個INT#信號和I/O APIC輸入引腳的對應關系。該數據結構的srcbus成員為對應PCI設備的bus id;srcbusirq描述了一個INT#信號,其bit0-bit1用于描述是INTA#–INTD#中的哪一個(對應值為0-3),bit2-bit6描述該PCI設備的slot id。dstapic為該描述對應的I/O APIC的ID。dstirq描述srcbus和srcbusirq確定的INT#對應的irq號信息(具體的解析有多種情況)。在系統中有一個以該數據結構為成員的全局數組mp_irqs,用于管理系統中所有的硬件中斷信號和irq之間的關聯。對MP table及其使用的更加詳細的描述,見《Multiprocessor Specification v1.4》3、 ACPI(Advanced Configuration and PowerInterface)機制這種機制為I/O APIC機制和中PIR機制提供統一的管理界面,該機制使用struct acpi_prt_entry描述INT#和GSI(能和vector、irq對應)的關系,系統中所有的struct acpi_prt_entry由OS從BIOS提供的信息中獲取,并保存在鏈表acpi_prt_list中。注:對GSI的說明,GSI(global system interrupt)表示的是系統中中斷控制器的每個輸入管腳的唯一編號,在使用ACPI模式管理中斷控制器的時候使用。對使用8259A的系統,GSI和ISA IRQ是一一對應的。對于使用APIC的,每個I/O APIC會由BISO分配一個基址,這個base+對應管腳的編號(從0開始)就是對應的GSI。通常是基址為0的I/O APIC的前16個管腳用于ISA IRQS,對GSI更加詳細的描述,見《Advanced Configuration and Power Interface Revision 2.0》
除了中斷路由表,其它兩種機制的初始化(包括相關中斷路由信息的初始化)的在《interrupt in linux》中有很詳細的描述。這些初始化操作都在內核初始化的時候完成。
為PCI設備配置中斷
為PCI設備配置中斷,分為兩個步驟,步驟一:為設備分配irq號(對MSIX,會有多個),為該中斷分配執行CPU和它使用的vector,并通過對中斷控制器的設置,確保對應的中斷信號和vector匹配。對于使用INT#類型的中斷,通常通過pci_enable_device/pci_enable_device_mem/pci_enable_device_io中對函數pcibios_enable_device的調用來完成(只有在沒有開啟MSI/MSIX的時候才會為INT#做配置),而要配置MSI/MSIX中斷要使用的是pci_enable_msix。步驟二:request_irq為該設備的irq指定對應的中斷處理例程,把irq號和驅動定義ISR關聯。
pcibios_enable_device
該接口用于使能PCI設備INT#模式的中斷。其主要功能由pcibios_enable_irq(dev)完成,pcibios_enable_irq是一個函數指針,對于ACPI模式,其在上電過程中被設置為acpi_pci_irq_enable,其它情況被設置為pirq_enable_irq。
對ACPI模式,其執行過程為:1、 acpi_pci_irq_enable:其先根據設備的管腳信息獲取一個GSI(可以認為有了GSI,就有了irq號,gsi_to_irq可以完成其轉換),有了gsi/irq,要完成設置還必須有vector并且把它們關聯起來,因此如果GSI獲取成功,會使用acpi_register_gsi來完成后續操作。2、 acpi_register_gsi:其主要功能由__acpi_register_gsi來完成,該函數指針在ACPI模式下被設置為acpi_register_gsi_ioapic,acpi_register_gsi_ioapic的執行過程如下:mp_register_gsi===>io_apic_set_pci_routing===>io_apic_set_pci_routing===>io_apic_setup_irq_pin_once===>io_apic_setup_irq_pin===>setup_ioapic_irq,在setup_ioapic_irq中,就會利用assign_irq_vector為該irq選擇對應的執行CPU,并分配該CPU上的vector,同時還把該vector等配置寫入到I/O APIC對應管腳的RTE,從而完成整個中斷的配置。這樣在該INT#信號到來的時候,I/O APIC就能根據對應管腳的RTE,把該信號翻譯為一個vector,并通過中斷消息發送到local APIC。同時在setup_ioapic_irq中,還通過ioapic_register_intr===>irq_set_chip_and_handler_name為得到的irq號對應的irq_desc設置了->irq_data.chip和handle_irq函數指針(對level觸發的,為handle_fasteoi_irq,否則為handle_edge_irq)
對其它模式,其通過pcibios_lookup_irq完成執行:在配置了I/O APIC的場景,pirq_enable_irq通過IO_APIC_get_PCI_irq_vector獲取到irq號,然后和ACPI模式一樣,通過io_apic_set_pci_routing完成對I/O APIC的配置。而對沒有配置I/O APIC的場景,主要通過pcibios_lookup_irq來完成相關操作:1、 pcibios_lookup_irq通過讀取BIOS提供的中斷路由表 ($PIR表,irq_routing_table)信息和當前irq分配情況(pirq_penalty數組),在考慮均衡的前提下為當前設備分配一個可用的irq。2、 根據當前PIR的相關信息,決定最終的irq號選擇,相關代碼行如下
也就是:如果是硬鏈接(INT#直接連接到了8259A,沒有經過PIR),直接獲取irq號,如果PIR中已經有該輸入線的配置,使用已有的值,否則利用剛剛分配的可用irq,并寫入到PIR,以便能夠完成中斷信號到irq號的轉換。注意:1、這里的r,也就是pirq_router,代表一種PIR硬件,全局配置pirq_routers中描述了當前支持的PIR,并在初始化的時候通過pirq_find_router獲取了對應當前配置的PIR對應的描述。2、這里沒有分配vector,是因為這里使用的irq號范圍為0-16,是ISA IRQs,其與vector的對應關系簡單:vector = IRQ0_VECTOR + irq,并在系統初始化過程中,已經通過early_irq_init中分配了irq_desc結構,通過init_IRQ設置了vector_irq(只運行于CPU0上),然后通過x86_init.irqs.intr_init(native_init_IRQ)===> x86_init.irqs.pre_vector_init(init_ISA_irqs)設置了->irq_data.chip(i8259A_chip)和handle_irq函數指針(handle_level_irq)。
Pci_enable_msix
該函數完成MSIX中斷相關的設置。
msix_capability_init中實現中斷初始化的是arch_setup_msi_irqs,對于X86系統,其為x86_setup_msi_irqs,x86_setup_msi_irqs中直接調用了native_setup_msi_irqs,該函數是X86系統中實現MSIX中斷初始化的關鍵函數,對于沒有啟用interrupt remap的系統,其實現如下:
該函數中有兩個關鍵函數,分別是create_irq_nr和setup_msi_irq,其中create_irq_nr是分配一個vector給當前的中斷,分配vector的同時,也為該中斷指定了執行CPU。setup_msi_irq則負責把相關配置信息寫入到PCIE配置區,并設置irq_desc的數據,其中關鍵的是irq_desc的handle_irq被設置為handle_edge_irq。create_irq_nr的實現如下:
其中__assign_irq_vector負責分配vector,并和中斷在CPU上的調度相關,其實現如下
從實現中可以看到,該函數從FIRST_EXTERNAL_VECTOR(外設中斷的起始vector號,通常是0x20) 到first_system_vector(外部中斷結束vector號,通常是254,255被系統作為保留的SPURIOUS_APIC_VECTOR使用)的范圍中,為當前中斷分配一個vector,要求該vector在對應的cpu上均可用,該vector按照系統配置的要求和對應的cpu核心綁定,并在要求的cpu中沒有被其它中斷使用。需要說明的是,在setup_msi_irq中會再次通過msi_compose_msg再次調用__assign_irq_vector,但是由于這時已經存在滿足CPU綁定要求的vector,不會多次分配。
從以上分析可以得到MSI-X中斷的一個綁定特征:根據當前APIC配置,每個中斷都有對應的可以運行的cpu,pci_enable_msix在這些要求的cpu核心上建立了vector (APIC的配置由數據結構struct apic來抽象,其vector_allocation_domain用于決定需要在那些cpu核心上為該中斷建立vector),當前我的系統使用的是apic_physflat,對每個MSI中斷,其只在一個cpu核心上建立vector,對應的MSI-X中斷事實上被綁定到該cpu核心上。在用戶通過echo xxx > /proc/irq/xxx/affinity來調整中斷的綁定屬性時,內核會重新為該中斷分配一個新的在對應核心上可用的vector,但是irq號不會改變。綁定屬性調整的調用路徑大致為irq_affinity_proc_fops===>irq_affinity_proc_write===> write_irq_affinity===>irq_set_affinity===>__irq_set_affinity_locked===>chip->irq_set_affinity(msi_set_affinity)。也就是最終通過msi_set_affinity來實現,在該函數中首先通過 __ioapic_set_affinity在綁定屬性要求的cpu中選擇空閑vector,然后通過__write_msi_msg把配置寫入PCIE配置區。需要說明的是:該irq最終可以運行的cpu數量并不完全由用戶指定,還與apic的模式相關,對于apic_physflat,實際上只為該irq分配了一個cpu核心,該irq只能運行在用戶指定的cpu中的一個,而不是全部。
附:關于全局變量apic
該全局變量為local apic的抽象,在不同的系統配置下,有不同的選擇,其最終的選擇結果,由內核的config(反應在/arch/x86/kernel/apic/Makefile)和硬件配置等來決定。1、 定義各種apic driver首先,每種apic配置都會使用apic_driver/ apic_drivers來定義,apic_driver的定義如下
這個定義的目的是把sym的地址寫入到名為” .apicdrivers”的段中。2、 定義全局符號__apicdrivers和__apicdrivers_end在linker script vmlinux.lds.S中,定義了__apicdrivers為” .apicdrivers”段的開始地址,而__apicdrivers_end為結束地址。” .apicdrivers”段中是各個不同的apic配置對應的struct apic。
3、 apic的probe在初始化過程(start_kernel)中,會調用default_setup_apic_routing(probe_64.c中定義)來完成apic的probe,該函數會按照各個struct apic結構在.apicdrivers中的順序,依次調用其probe接口,第一個調用返回非0的struct apic結構就被初始化到全局變量apic。也就是:如果有多個apic結構可用,最終會選擇在.apicdrivers段中出現的第一個;所以makefile文件中各個.o出現的順序也會覺得最終的apic probe結果。
request_irq
該函數把irq和用戶指定的中斷處理函數關聯。用戶指定的每個處理函數對應于一個struct irqaction結構,這些處理函數構成一個鏈表,保存在struct irq_desc::action成員中。詳細見request_irq===>request_threaded_irq中的處理。
中斷的執行
在內核代碼中,對X86平臺中斷執行的基本過程是:1、 通過IDT中的中斷描述符,調用common_interrupt;2、 通過common_interrupt,調用do_IRQ,完成vector到irq_desc的轉換,進入Generic interrupt layer(調用處理函數generic_handle_irq_desc);3、 調用在中斷初始化的時候,按照中斷特性(level觸發,edge觸發等、simple等)初始化的irq_desc:: handle_irq,執行不同的通用處理接口,比如handle_simple_irq;4、 這些通用處理接口會調用中斷初始化的時候注冊的外部中斷處理函數;完成EOI等硬件相關操作;并完成中斷處理的相關控制。
common_interrupt
按照之前CPU執行中斷過程的描述,X86 CPU在準備好了中斷執行環境后,會調用中斷描述符定義的中斷處理入口;根據中斷相關初始化過程我們知道,對于用戶自定義中斷,中斷處理入口都是(對系統預留的,就直接執行定義的接口了):
就是在把vector入棧后,執行common_interrupt,common_interrupt在entry_64.S中定義,其中關鍵步驟為:調用do_IRQ,完成后會根據環境判斷是否需要執行調度,最后執行iretq指令完成中斷處理,iret指令的重要功能就是回復中斷函數前的EFLAGS(執行中斷入口前被入棧保存,并清零IF位關中斷),并恢復執行被中斷的程序(這里不一定會恢復到之前的執行環境,可能執行軟中斷處理,或者執行調度)。
do_IRQ
do_IRQ的基本處理過程如下,其負責中斷執行環境建立、vector到irq的轉換等
Generic interrupt layer
該層負責的是平臺無關/設備無關的中斷通用邏輯,對這部分,在《Linux generic IRQ handling》中有詳細描述。其負責完成中斷處理的接口是generic_handle_irq_desc,該接口會執行irq_desc::handle_irq; Generic interrupt layer根據中斷特性的不同,把中斷分成幾類,包括:level type(handle_level_irq)、edge type(handle_edge_irq)、simple type(handle_simple_irq)等,這些中斷類型對應的處理函數是都在kernel/irq/chip.c中定義,并入前面的描述,在相關中斷初始化的時候,被賦值給irq_desc::handle_irq;對于PCI設備,只用了兩種,level type(INT#模式)、edge type(MSI/MSI-X模式)。
edge 觸發中斷的基本處理過程:
電壓跳變觸發中斷===>中斷控制器接收中斷,記IRR寄存器===>中斷控制器置ISR寄存器===>CPU屏蔽本CPU中斷===>CPU處理中斷,發出EOI===>中斷控制器確認可以處理下一次中斷===>ISR清中斷源,電壓歸位===>中斷源可以發起下一次中斷===>CPU中斷處理完成,執行完現場處理后執行IRET,不再屏蔽本CPU中斷。edge觸發的特點:a) 中斷不會丟如果中斷觸發時中斷被屏蔽,那么中斷控制器會記錄下該中斷,在屏蔽取消的時候會再執行。b) edge觸發的缺點是完成共享不方便:比如A和B兩個中斷源共享一個中斷,每次ISR先檢查A再檢查B,如果B先發生中斷,在ISR檢查完A,檢查B的過程中,A發生中斷。那么在ISR處理開始的時候,A會告訴ISR,不是它干的,然后ISR處理B的中斷,完成后通過清理中斷源把B的電壓歸位,但是由于A的中斷沒有得到處理,電壓沒有歸位,這個共享的中斷就不能得到再次觸發了。edge觸發對應的通用邏輯接口
level 觸發:
這種模式下,外設通過把電壓保持到某個門限值來完成觸發中斷,在處理完成(EOI)后,如果電壓還在門限值,就會再次觸發中斷的執行。level觸發的特點:a) 方便中斷共享b) 對中斷觸發時中斷被屏蔽的情況,如果中斷屏蔽解除后仍然引腳電壓仍然在門限值,就執行該中斷的ISR,否則不執行。需要說明的是:對于使用local APIC的系統,level觸發和edge觸發需要配置local APIC的Local Vector Table。4、 level觸發對應的通用邏輯接口
level觸發和edge觸發在通用邏輯層最大的不同就是當其他CPU正在處理該中斷的時候,系統的行為,對edge觸發,會把該中斷記錄下來,當前處理結束后再次執行,而level直接退出。產生這種差異的原因是:level觸發不怕丟?
無論是那種觸發方式,都會調用handle_irq_event處理中斷,該函數中會遍歷irq_desc::action鏈表,執行action->handler,也就是驅動在中斷初始化的時候,通過request_irq注冊的中斷處理接口。
總結
中斷的使能狀態
1、 在local APIC層次(當前CPU),一個中斷正在處理的時候,不會有相同的中斷或者優先級低于該中斷的其它中斷來打斷當前中斷的執行;但是高優先級中斷可以打斷低優先級中斷。2、 在X86 CPU層次(當前CPU),從中斷執行開始到IRET,IF位都被清零,也就是只有不可屏蔽中斷能夠打斷當前中斷的執行。3、 在Generic interrupt layer層次,如果一個中斷已經在系統中執行,會阻止該中斷在其它CPU上的執行。4、 在外設/驅動中斷處理函數層次往往也有中斷使能的功能,比如啟用了NAPI的網卡,在中斷處理函數開始執行的時候,往往會通過硬件功能關閉該中斷,要在對應的軟中斷完成處理后才通過硬件功能使能該中斷。注:NMI中斷雖然稱為不可屏蔽中斷,也有一個例外:NMI中斷執行過程中,該CPU屏蔽了后來的NMI中斷。
中斷的執行CPU
通過中斷初始化過程我們知道:中斷在那個CPU上執行,取決于在那個CPU上申請了vector并配置了對應的中斷控制器(比如local APIC)。如果想要改變一個中斷的執行CPU,必須重新申請vector并配置中斷控制器。一般通過echo xxx > /proc/irq/xxx/affinity來完成調整,同時irq_balance一類軟件可以用于完成中斷的均衡。
(完)
-
cpu
+關注
關注
68文章
10905瀏覽量
213031 -
Linux
+關注
關注
87文章
11345瀏覽量
210406 -
初始化
+關注
關注
0文章
50瀏覽量
11953
原文標題:Linux中斷機制:硬件處理,初始化和中斷處理
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論