日期 | 核心版本 | 架構 | 作者 | 内容 |
---|---|---|---|---|
2018-8-23 | Linux-2.6.32 | X86 | Bystander | Linux核心中斷 |
-
一、中斷概述
中斷是指在CPU正常運作期間,由于内外部事件或由程式預先安排的事件引起的CPU暫時停止正在運作的程式,轉而為該内部或外部事件或預先安排的事件服務的程式中去,服務完畢後再傳回去繼續運作被暫時中斷的程式。
1.1中斷類型
同步中斷由CPU本身産生,又稱為内部中斷。這裡同步是指中斷請求信号與代碼指令之間的同步執行,在一條指令執行完畢後,CPU才能進行中斷,不能在執行期間。是以也稱為異常(exception)。
異步中斷是由外部硬體裝置産生,又稱為外部中斷,與同步中斷相反,異步中斷可在任何時間産生,包括指令執行期間,是以也被稱為中斷(interrupt)。
異常又可分為可屏蔽中斷(Maskable interrupt)和非屏蔽中斷(Nomaskable interrupt)。而中斷可分為故障(fault)、陷阱(trap)、終止(abort)三類。
從廣義上講,中斷又可分為四類:中斷、故障、陷阱、終止。這些類别之間的異同點請參考 表 1。
表 1:中斷類别及其行為 | |||
類别 | 原因 | 異步/同步 | 傳回行為 |
中斷 | 來自I/O裝置的信号 | 異步 | 總是傳回到下一條指令 |
陷阱 | 有意的異常 | 同步 | 總是傳回到下一條指令 |
故障 | 潛在可恢複的錯誤 | 同步 | 傳回到目前指令 |
終止 | 不可恢複的錯誤 | 同步 | 不會傳回 |
有些參考資料中按照中斷來源進行分類,如下圖所示(個人建議不采用這種方式):
圖1-1
1.2區分中斷号與中斷向量
I/O裝置把中斷信号發送給中斷控制器(8259A)時與之相關聯的是一個中斷号,當中斷控制器把中斷信号發送給CPU時與之關聯的是一個中斷向量。換個角度分析就是中斷号是從中斷控制器層面劃分,中斷向量是從CPU層面劃分,是以中斷号與中斷向量之間存在一對一映射關系。在Intel X86中最大支援256種中斷,從0到255開始編号,這個8位的編号就是中斷向量。其中将0到31保留用于異常處理和不可屏蔽中斷。
-
二、中斷資料處理結構
Linux核心中進行中斷主要有三個資料結構,irq_desc,irq_chip和irqaction。
在\include\linux\ irq.h中定義了
1)irq_desc用于描述IRQ線的屬性與狀态,被稱為中斷描述符。
/**
* struct irq_desc - interrupt descriptor
* @irq: interrupt number for this descriptor
* @timer_rand_state: pointer to timer rand state struct
* @kstat_irqs: irq stats per cpu
* @irq_2_iommu: iommu with this irq
* @handle_irq: highlevel irq-events handler [if NULL, __do_IRQ()]
* @chip: low level interrupt hardware access
* @msi_desc: MSI descriptor
* @handler_data: per-IRQ data for the irq_chip methods
* @chip_data: platform-specific per-chip private data for the chip
* methods, to allow shared chip implementations
* @action: the irq action chain
* @status: status information
* @depth: disable-depth, for nested irq_disable() calls
* @wake_depth: enable depth, for multiple set_irq_wake() callers
* @irq_count: stats field to detect stalled irqs
* @last_unhandled: aging timer for unhandled count
* @irqs_unhandled: stats field for spurious unhandled interrupts
* @lock: locking for SMP
* @affinity: IRQ affinity on SMP
* @node: node index useful for balancing
* @pending_mask: pending rebalanced interrupts
* @threads_active: number of irqaction threads currently running
* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers
* @dir: /proc/irq/ procfs entry
* @name: flow handler name for /proc/interrupts output
*/
struct irq_desc{
unsigned int irq;
struct timer_rand_state *timer_rand_state;
unsigned int *kstat_irqs;
#ifdef CONFIG_INTR_REMAP
struct irq_2_iommu *irq_2_iommu;
#endif
irq_flow_handler_t handle_irq;
struct irq_chip *chip;
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
struct irqaction *action; /* IRQ action list */
unsigned int status; /* IRQ status */
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
spinlock_t lock;
#ifdef CONFIG_SMP
cpumask_var_t affinity;
unsigned int node;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
const char *name;
}
2)irq_chip用于描述不同類型的中斷控制器。
/**
* struct irq_chip - hardware interrupt chip descriptor
*
* @name: name for /proc/interrupts
* @startup: start up the interrupt (defaults to ->enable if NULL)
* @shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @enable: enable the interrupt (defaults to chip->unmask if NULL)
* @disable: disable the interrupt (defaults to chip->mask if NULL)
* @ack: start of a new interrupt
* @mask: mask an interrupt source
* @mask_ack: ack and mask an interrupt source
* @unmask: unmask an interrupt source
* @eoi: end of interrupt - chip level
* @end: end of interrupt - flow level
* @set_affinity: set the CPU affinity on SMP machines
* @retrigger: resend an IRQ to the CPU
* @set_type: set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
* @set_wake: enable/disable power-management wake-on of an IRQ
*
* @bus_lock: function to lock access to slow bus (i2c) chips
* @bus_sync_unlock: function to sync and unlock slow bus (i2c) chips
*
* @release: release function solely used by UML
* @typename: obsoleted by name, kept as migration helper
*/
struct irq_chip {
const char *name;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*mask)(unsigned int irq);
void (*mask_ack)(unsigned int irq);
void (*unmask)(unsigned int irq);
void (*eoi)(unsigned int irq);
void (*end)(unsigned int irq);
int (*set_affinity)(unsigned int irq,
const struct cpumask *dest);
int (*retrigger)(unsigned int irq);
int (*set_type)(unsigned int irq, unsigned int flow_type);
int (*set_wake)(unsigned int irq, unsigned int on);
void (*bus_lock)(unsigned int irq);
void (*bus_sync_unlock)(unsigned int irq);
/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
/*
* For compatibility, ->typename is copied into ->name.
* Will disappear.
*/
const char *typename;
}
在\include\linux\ interrupt.h中定義了 irqaction用來描述特定裝置所産生的中斷描述符。
/**
* struct irqaction - per interrupt action descriptor
* @handler: interrupt handler function
* @flags: flags (see IRQF_* above)
* @name: name of the device
* @dev_id: cookie to identify the device
* @next: pointer to the next irqaction for shared interrupts
* @irq: interrupt number
* @dir: pointer to the proc/irq/NN/name entry
* @thread_fn: interupt handler function for threaded interrupts
* @thread: thread pointer for threaded interrupts
* @thread_flags: flags related to @thread
*/
struct irqaction {
irq_handler_t handler;
unsigned long flags;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
};
-
三、Linux中斷機制
Linux中斷機制由三部分組成:
- 中斷子系統初始化:核心自身初始化過程中對中斷處理機制初始化,例如中斷的資料結構以及中斷請求等。
- 中斷或異常處理:中斷整體處理過程。
- 中斷API:為裝置驅動提供API,例如注冊,釋放和激活等。
3.1中斷子系統初始化
3.1.1中斷描述符表(IDT)初始化
中斷描述符表初始化需要經過兩個過程:
- 第一個過程在核心引導過程。由兩個步驟組成,首先給配置設定IDT配置設定2KB空間(256中斷向量,每個向量由8bit組成)并初始化;然後把IDT起始位址存儲到IDTR寄存器中。
- 第二個過程核心在初始化自身的start_kernal函數中使用trap_init初始化系統保留中斷向量,使用init_IRQ完成其餘中斷向量初始化。
3.1.2中斷請求隊列初始化
init_IRQ調用pre_intr_init_hook,進而最終調用init_ISA_irqs初始化中斷控制器以及每個IRQ線的中斷請求隊列。
3.2中斷或異常處理
中斷處理過程:裝置産生中斷,并通過中斷線将中斷信号送往中斷控制器,如果中斷沒有被屏蔽則會到達CPU的INTR引腳,CPU立即停止目前工作,根據獲得中斷向量号從IDT中找出門描述符,并執行相關中斷程式。
異常處理過程:異常是由CPU内部發生是以不會通過中斷控制器,CPU直接根據中斷向量号從IDT中找出門描述符,并執行相關中斷程式。
圖3-1
中斷控制器處理主要有5個步驟:1.中斷請求 2.中斷響應 3.優先級比較 4.送出中斷向量 5.中斷結束。這裡不再贅述5個步驟的具體流程。
CPU處理流程主要有6個步驟:1.确定中斷或異常的中斷向量 2.通過IDTR寄存器找到IDT 3.特權檢查 4.特權級發生變化,進行堆棧切換 5.如果是異常将異常代碼壓入堆棧,如果是中斷則關閉可屏蔽中斷 6.進入中斷或異常服務程式執行。這裡不再贅述6個步驟的具體流程。
3.3中斷API
核心提供的API主要用于驅動的開發。
注冊IRQ:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
釋放IRQ:
void free_irq(unsigned int, void *);
注:IRQ線資源非常寶貴,我們在使用時必須先注冊,不使用時必須釋放IRQ資源。
激活目前CPU中斷:
local_irq_enable();
禁止目前CPU中斷:
local_irq_disable();
激活指定中斷線:
void enable_irq(unsigned int irq);
禁止指定中斷線:
void disable_irq(unsigned int irq);
禁止指定中斷線:
void disable_irq_nosync(unsigned int irq);
注:此函數調用irq_chip中disable禁止指定中斷線,是以不會保證中斷線上執行的中斷服務程式已經退出。
3.4中斷機制劃分
由于中斷會打斷核心中程序的正常排程運作,是以要求中斷服務程式盡可能的短小精悍;但是在實際系統中,當中斷到來時,要完成工作往往進行大量的耗時處理。是以期望讓中斷處理程式運作得快,并想讓它完成的工作量多,這兩個目标互相制約,誕生——頂/底半部機制。
中斷處理程式是頂半部——接受中斷,它就立即開始執行,但隻有做嚴格時限的工作。能夠被允許稍後完成的工作會推遲到底半部去,此後,在合适的時機,底半部會被開終端執行。頂半部簡單快速,執行時禁止一些或者全部中斷。
底半部稍後執行,而且執行期間可以響應所有的中斷。這種設計可以使系統處于中斷屏蔽狀态的時間盡可能的短,以此來提高系統的響應能力。頂半部隻有中斷處理程式機制,而底半部的實作有軟中斷,tasklet和工作隊列實作。
圖3-2 注:登記中斷,将底半部處理程式挂到該裝置的低半部執行隊列中。
3.4.1頂/底半部劃分原則:
1) 如果一個任務對時間非常敏感,将其放在頂半部中執行;
2) 如果一個任務和硬體有關,将其放在頂半部中執行;
3) 如果一個任務要保證不被其他中斷打斷,将其放在頂半部中執行;
4) 其他所有任務,考慮放置在底半部執行。
3.4.2底半部實作機制
圖3-3
軟中斷:
軟中斷作為下半部機制的代表,是随着SMP(share memory processor)的出現應運而生的,它也是tasklet實作的基礎(tasklet實際上隻是在軟中斷的基礎上添加了一定的機制)。軟中斷一般是“可延遲函數”的總稱,有時候也包括了tasklet(請讀者在遇到的時候根據上下文推斷是否包含tasklet)。它的出現就是因為要滿足上面所提出的上半部和下半部的差別,使得對時間不敏感的任務延後執行,軟中斷執行中斷處理程式留給它去完成的剩餘任務,而且可以在多個CPU上并行執行,使得總的系統效率可以更高。它的特性包括:
a)産生後并不是馬上可以執行,必須要等待核心的排程才能執行。軟中斷不能被自己打斷,隻能被硬體中斷打斷(上半部)。
b)可以并發運作在多個CPU上(即使同一類型的也可以)。是以軟中斷必須設計為可重入的函數(允許多個CPU同時操作),是以也需要使用自旋鎖來保護其資料結構。
核心中定義了幾種軟中斷的用途:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
Tasklet:
tasklet是通過軟中斷實作的,是以它本身也是軟中斷。
軟中斷用輪詢的方式處理。假如正好是最後一種中斷,則必須循環完所有的中斷類型,才能最終執行對應的處理函數。顯然當年開發人員為了保證輪詢的效率,于是限制中斷個數為32個。
為了提高中斷處理數量,順道改進處理效率,于是産生了tasklet機制。
Tasklet采用無差别的隊列機制,有中斷時才執行,免去了循環查表之苦。Tasklet作為一種新機制,顯然可以承擔更多的優點。正好這時候SMP越來越火了,是以又在tasklet中加入了SMP機制,保證同種中斷隻能在一個cpu上執行。在軟中斷時代,顯然沒有這種考慮。是以同一種軟中斷可以在兩個cpu上同時執行,很可能造成沖突。
總結下tasklet的優點:
(1)無類型數量限制;
(2)效率高,無需循環查表;
(3)支援SMP機制;
它的特性如下:
1)一種特定類型的tasklet隻能運作在一個CPU上,不能并行,隻能串行執行。
2)多個不同類型的tasklet可以并行在多個CPU上。
3)軟中斷是靜态配置設定的,在核心編譯好之後,就不能改變。但tasklet就靈活許多,可以在運作時改變(比如添加子產品時)。
工作隊列:
上面我們介紹的可延遲函數運作在中斷上下文中,于是導緻了一些問題,說明它們不可挂起,也就是說軟中斷不能睡眠、不能阻塞,原因是由于中斷上下文出于核心态,沒有程序切換,是以如果軟中斷一旦睡眠或者阻塞,将無法退出這種狀态,導緻核心會整個僵死。是以,可阻塞函數不能用軟中斷來實作。但是它們往往又具有可延遲的特性。而且由于是串行執行,是以隻要有一個處理時間較長,則會導緻其他中斷響應的延遲。為了完成這些不可能完成的任務,于是出現了工作隊列,它能夠在不同的程序間切換,以完成不同的工作。
工作隊列能運作在程序上下文,它将工作給一個核心線程,作為中斷守護線程來使用。多個中斷可以放在一個線程中,也可以每個中斷配置設定一個線程。我們用結構體workqueue_struct表示工作者線程,工作者線程是用核心線程實作的。而工作者線程是如何執行被推後的工作——有這樣一個連結清單,它由結構體work_struct組成,而這個work_struct則描述了一個工作,一旦這個工作被執行完,相應的work_struct對象就從連結清單上移去,當連結清單上不再有對象時,工作者線程就會繼續休眠。因為工作隊列是線程,是以我們可以使用所有可以線上程中使用的方法。
如何選擇下半部機制:
- 軟中斷和tasklet運作在中斷上下文,工作隊列運作在程序上下文。如果需要休眠則選擇工作隊列,否則選擇tasklet;如果對性能要求較高則選擇軟中斷。
- 從易用性考慮,首選工作隊列,然後tasklet,最後是軟中斷,因為軟中斷需要靜态建立。
- 從代碼安全考慮,如果對底半部代碼保護不夠安全,則選擇tasklet,因為相對軟中斷,tasklet對鎖要求低,上面也簡述它們工作方式以及運用場景。
四、多處理器系統中斷相關概念
4.1處理器間中斷
在多處理器系統中,作業系統需要在多個處理器中協調操作,是以需要處理器中斷(Inter-Processor-Interrupt,IPI)實作,IPI是一種特殊硬體中斷,由CPU送出,其他CPU接收,處理CPU之間通信和同步操作。以下是x86中SMP定義的IPI,中斷向量号用十六進制表示:
SPURIOUS_APIC_VECTOR 0xff
ERROR_APIC_VECTOR 0xfe
RESCHEDULE_VECTOR 0xfd
CALL_FUNCTION_VECTOR 0xfc
CALL_FUNCTION_SINGLE_VECTOR 0xfb
THERMAL_APIC_VECTOR 0xfa
THRESHOLD_APIC_VECTOR 0xf9
REBOOT_VECTOR 0xf8
INVALIDATE_TLB_VECTOR_END 0xf7
INVALIDATE_TLB_VECTOR_START 0xf0
4.2中斷親和力
将一個或多個中斷服務程式綁定到特定的CPU上處理,這就是中斷親和力(SMP IRQ affinity)。我們可以使用中斷親和力來均衡各個CPU的負載,提高系統處理能力。
以上便是本人對Linux中斷的了解,若有纰漏歡迎指正!