來源: CSDN | phenix_lord的專欄
最近解決一個關於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、 NMI
NMI是不可遮蔽中斷(不可透過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、 中斷路由表 $PIR
struct 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 table
struct 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一類軟體可以用於完成中斷的均衡。
(完)