本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
關於中斷和異常
一般在書中都會把中斷和異常一起說明,因為它們具有相同的特點,同時也有不同的地方。在CPU里,中斷和異常都會放入到一個中斷描述符表中,都需要特定的處理程序進行處理,並且它們都是異步事件,內核完全不知道何時會有一個異常或者中斷發生。當異常或者中斷發生時,進程都會陷入內核,在內核中執行相應的處理。異常一般都是由CPU內部或者進程產生,而中斷一般都是由外部設備產生。異常處理過程實際上和系統調用沒什么區別(實際上系統調用是通過一個0x80異常陷入內核當中),而中斷的處理過程和情況就相對來說比較復雜。一個中斷處理分為硬中斷和軟中斷兩個部分,在中斷處理的過程中系統是禁止調度和搶占的,而異常處理過程中是允許的。一個中斷處理程序可以搶占其他的中斷處理程序,也可以搶占異常處理程序,相反的,異常處理程序卻不能夠搶占中斷處理程序。
可編程中斷控制器(PIC、APIC)
為了方便說明,這里我們將PIC和APIC統稱為中斷控制器。中斷控制器是作為中斷(IRQ)和CPU核之間的一個橋梁而存在的,每個CPU內部都有一個自己的中斷控制器,中斷線並不是直接與CPU核相連,而是與CPU內部或外部的中斷控制器相連。而為什么叫做可編程中斷控制器,是因為其本身有一定的寄存器,CPU可以通過操作設置中斷控制器屏蔽某個中斷引腳的信號,實現硬件上的中斷屏蔽。中斷控制器也可以級聯提供更多的中斷線,具體如下:
如上圖,CPU的INTR與中斷控制器的INT相連,INTA與ACK相連,當一個外部中斷發生時(比如鍵盤中斷IRQ1),中斷控制器與CPU交互操作如下:
- IRQ1發生中斷,主中斷控制器接收到中斷信號,檢查中斷屏蔽寄存器IRQ1是否被屏蔽,如果屏蔽則忽略此中斷信號。
- 將中斷控制器中的中斷請求寄存器對應的IRQ1位置位,表示收到IRQ1中斷。
- 中斷控制器拉高INT引腳電平,告知CPU有中斷發生。
- CPU每執行完一條指令時,都會檢查INTR引腳是否被拉高,這里已被拉高。
- CPU檢查EFLAGS寄存器的中斷運行標志位IF是否為1,若為1,表明允許中斷,通過INTA向中斷控制器發出應答。
- 中斷控制器接收到應答信號,將IRQ1的中斷向量號發到數據總線上,此時CPU會通過數據總線讀取IRQ1的中斷向量號。
- 最后,如果中斷控制器需要EOI(End of Interrupt)信號,CPU則會發送,否則中斷控制器自動將INT拉低,並清除IRQ1對應的中斷請求寄存器位。
在linux內核中,用struct irq_chip結構體描述一個可編程中斷控制器,它的整個結構和調度器中的調度類類似,里面定義了中斷控制器的一些操作,如下:
struct irq_chip { /* 中斷控制器的名字 */ const char *name; /* 控制器初始化函數 */ unsigned int (*irq_startup)(struct irq_data *data); /* 控制器關閉函數 */ void (*irq_shutdown)(struct irq_data *data); /* 使能irq操作,通常是直接調用irq_unmask(),通過data參數指明irq */ void (*irq_enable)(struct irq_data *data); /* 禁止irq操作,通常是直接調用irq_mask,嚴格意義上,他倆其實代表不同的意義,disable表示中斷控制器根本就不響應該irq,而mask時,中斷控制器可能響應該irq,只是不通知CPU */ void (*irq_disable)(struct irq_data *data); /* 用於CPU對該irq的回應,通常表示cpu希望要清除該irq的pending狀態,准備接受下一個irq請求 */ void (*irq_ack)(struct irq_data *data); /* 屏蔽irq操作,通過data參數表明指定irq */ void (*irq_mask)(struct irq_data *data); /* 相當於irq_mask() + irq_ack() */ void (*irq_mask_ack)(struct irq_data *data); /* 取消屏蔽指定irq操作 */ void (*irq_unmask)(struct irq_data *data); /* 某些中斷控制器需要在cpu處理完該irq后發出eoi信號 */ void (*irq_eoi)(struct irq_data *data); /* 用於設置該irq和cpu之間的親和力,就是通知中斷控制器,該irq發生時,那些cpu有權響應該irq */ int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); int (*irq_retrigger)(struct irq_data *data); /* 設置irq的電氣觸發條件,例如 IRQ_TYPE_LEVEL_HIGH(電平觸發) 或 IRQ_TYPE_EDGE_RISING(邊緣觸發) */ int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); /* 通知電源管理子系統,該irq是否可以用作系統的喚醒源 */ int (*irq_set_wake)(struct irq_data *data, unsigned int on); void (*irq_bus_lock)(struct irq_data *data); void (*irq_bus_sync_unlock)(struct irq_data *data); void (*irq_cpu_online)(struct irq_data *data); void (*irq_cpu_offline)(struct irq_data *data); void (*irq_suspend)(struct irq_data *data); void (*irq_resume)(struct irq_data *data); void (*irq_pm_shutdown)(struct irq_data *data); void (*irq_calc_mask)(struct irq_data *data); void (*irq_print_chip)(struct irq_data *data, struct seq_file *p); int (*irq_request_resources)(struct irq_data *data); void (*irq_release_resources)(struct irq_data *data); unsigned long flags; };
中斷描述符表(IDT)
在中斷系統中有兩個名字很相像的結構,就是中斷描述符表和中斷描述符數組。這里我們先說說中斷描述符表。 一個系統中的中斷和異常加起來一共是256個,它們以向量的形式保存在中斷描述符表中,每一個向量是8字節(整個表大小就是8 x 256=2048字節),其主要保存着權限位和向量對應的中斷或異常處理程序的入口地址。而一般的,linux會將中斷描述符表中的0~31用於非屏蔽中斷和異常,其他的中斷用於32~255之間。CPU把中斷描述符表的向量類型分為三種類型:任務門、中斷門、陷阱門。CPU為了防止惡意程序訪問中斷,限制了中斷門的權限,而在某些時候,用戶程序又必須使用中斷,所以Linux把中斷描述符的中斷向量類型改為了5種:中斷門,系統門,系統中斷門,陷阱門,任務門。這個中斷描述符表的基地址保存在idtr寄存器中。
中斷門
系統門
系統中斷門
陷阱門
任務門
當我們發生異常或中斷時,系統首先會判斷權限字段(安全處理),權限通過則進入指定的處理函數,而所有的中斷門的中斷處理函數都是同一個,它首先是一段匯編代碼,匯編代碼操作如下:
- 執行SAVE_ALL宏,保存中斷向量號和寄存器上下文至當前運行進程的內核棧或者硬中斷請求棧(當內核棧大小為8K時保存在內核棧,若為4K,則保存在硬中斷請求棧)。
- 調用do_IRQ()函數。
- 跳轉到ret_from_intr,這是一段匯編代碼,主要用於判斷是否需要進行調度。
中斷處理
每個能夠產生中斷的設備或者模塊都會在內核中注冊一個中斷服務例程(ISR),當產生中斷時,中斷處理程序會被執行,在中斷處理程序中,首先會保存中斷向量號和上下文,之后執行中斷線對應的中斷服務例程。對於CPU來說,中斷線是非常寶貴的資源,而由於計算機的發展,外部設備數量和種類越來越多,導致了中斷線資源不足的情況,linux為了應對這種情況,實現了兩種中斷線分配方式,分別是:共享中斷線,中斷線動態分配。
共享中斷線
多個設備共用一條中斷線,當此條中斷線發生中斷時,因為不可能預先知道哪個特定的設備產生了中斷,因此,這條中斷線上的每個中斷服務例程都會被執行,以驗證是哪個設備產生的中斷(一般的,設備產生中斷時,會標記自己的狀態寄存器,中斷服務例程通過檢查每個設備的狀態寄存器來查找產生中斷的設備)。
中斷線動態分配
一條中斷線在可能使用的時刻才與一個設備驅動程序關聯起來,這樣一來,即使幾個硬件設備並不共享中斷線,同一個中斷向量也可以由這幾個設備在不同時刻運行。
共享中斷線的分配方式是比較常見的,一次典型的基於共享中斷線的中斷處理流程如下:
由於中斷處於中斷上下文中,所以在中斷處理過程中,會有以下幾個特性:
- 中斷處理程序正在運行時,CPU會通知中斷控制器屏蔽產生此中斷的中斷線。此中斷線發出的信號被暫時忽略,當中斷處理程序結束時恢復此中斷線。
- 在中斷服務例程的設計中,原則上是立即處理緊急的操作,將非緊急的操作延后處理(交給軟中斷進行處理)。
- 中斷處理程序是運行在中斷上下文,但是其是代表進程運行的,因此它所代表的進行必須處於TASK_RUNNING狀態,否則可能出現僵死情況,因此在中斷處理程序中不能執行任何阻塞過程。
中斷描述符
中斷描述符用於描述IRQ號的屬性與狀態,每個IRQ號都有它自己的中斷描述符,這些中斷描述符用一個數組保存, 這個數組就是中斷描述符數組,整個中斷描述符數組長度為NR_IRQS(通常為224)項。而當系統中的中斷號允許超過224項時, 會用一個radix_tree來組織這些中斷描述符, 每次新分配一個IRQ號時, 就會生成對應的中斷描述符, 放入radix_tree中。當產生一個中斷或者異常時,首先會從中斷描述符表中獲取到一個中斷向量號時(此中斷向量號有可能表示中斷,也可能表示的是一個異常),如果是一個中斷導致的,會執行do_IRQ()函數,而在do_IRQ()函數中,會根據中斷向量號,從中斷描述符數組中獲取對應的中斷描述符,如下圖:
整個中斷描述符結構如下:
struct irq_desc { struct irq_data irq_data; /* irq的統計信息,在proc中可查到 */ unsigned int __percpu *kstat_irqs; /* 回調函數,當此中斷產生中斷時,會調用handle_irq,在handle_irq中進行遍歷irqaction鏈表 * handle_simple_irq 用於簡單處理; * handle_level_irq 用於電平觸發中斷的流控處理; * handle_edge_irq 用於邊沿觸發中斷的流控處理; * handle_fasteoi_irq 用於需要響應eoi的中斷控制器; * handle_percpu_irq 用於只在單一cpu響應的中斷; * handle_nested_irq 用於處理使用線程的嵌套中斷; */ irq_flow_handler_t handle_irq; #ifdef CONFIG_IRQ_PREFLOW_FASTEOI irq_preflow_handler_t preflow_handler; #endif /* 中斷服務例程鏈表 */ struct irqaction *action; /* IRQ action list */ /* 狀態 */ unsigned int status_use_accessors; /* 函數調用中使用,另一個名稱為istate */ unsigned int core_internal_state__do_not_mess_with_it; /* 嵌套深度,中斷線被激活顯示0,如果為正數,表示被禁止次數 */ unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ /* 此中斷線上發生的中斷次數 */ unsigned int irq_count; /* For detecting broken IRQs */ /* 上次發生未處理中斷時的jiffies值 */ unsigned long last_unhandled; /* Aging timer for unhandled count */ /* 中斷線上無法處理的中斷次數,如果當第100000次中斷發生時,有超過99900次是意外中斷,系統會禁止這條中斷線 */ unsigned int irqs_unhandled; atomic_t threads_handled; int threads_handled_last; /* 鎖 */ raw_spinlock_t lock; struct cpumask *percpu_enabled; #ifdef CONFIG_SMP /* CPU親和力關系,其實就是每個CPU是占一個bit長度,某CPU上置為1表明該CPU可以進行這個中斷的處理 */ const struct cpumask *affinity_hint; struct irq_affinity_notify *affinity_notify; #ifdef CONFIG_GENERIC_PENDING_IRQ /* 用於調整irq在各個cpu之間的平衡 */ cpumask_var_t pending_mask; #endif #endif unsigned long threads_oneshot; atomic_t threads_active; /* 用於synchronize_irq(),等待該irq所有線程完成 */ wait_queue_head_t wait_for_threads; #ifdef CONFIG_PM_SLEEP /* irqaction數量 */ unsigned int nr_actions; unsigned int no_suspend_depth; unsigned int force_resume_depth; #endif #ifdef CONFIG_PROC_FS /* 指向與IRQn相關的/proc/irq/n目錄的描述符 */ struct proc_dir_entry *dir; #endif int parent_irq; struct module *owner; /* 在/proc/interrupts所顯示名稱 */ const char *name; } ____cacheline_internodealigned_in_smp;
core_internal_state__do_not_mes_with_it成員是用於記錄此中斷線狀態的,中斷線狀態有如下幾種形式:
IRQS_AUTODETECT /* 該IRQ線用來進行硬件設備探測 */ IRQS_SPURIOUS_DISABLED /* 該IRQ線被禁止,是由於產生了欺騙性中斷 */ IRQS_POLL_INPROGRESS /* 該IRQ進行輪詢檢查是否發生中斷 */ IRQS_ONESHOT /* 此IRQ沒有在主處理函數中進行unmasked處理 */ IRQS_REPLAY /* IRQ線已被禁止,但前一個出現的中斷還沒有被應答 */ IRQS_WAITING /* 進行硬件設備探測時,會將所有沒有掛載中斷服務程序的IRQ線狀態設置為IRQS_WAITING,如果該IRQ上有中斷產生,就清除這個狀態,可以推斷哪些引腳產生過中斷 */ IRQS_PENDING /* IRQ已經被應答(掛起),但是內核還沒有進行處理 */ IRQS_SUSPENDED /* 此IRQ被延遲 */
中斷服務例程(ISR)
中斷服務例程用於描述一個設備的中斷處理(區別與中斷處理函數),每個申請了中斷的外部設備都會有一個中斷服務例程,其作用就是執行對應設備的中斷處理。當多個設備共享IRQ線時,內核會將此IRQ線上所有設備的中斷服務例程組織成一個鏈表並保存在中斷描述符中,當此IRQ線產生中斷時,中斷處理函數會依次執行此IRQ線上的中斷服務例程。內核使用struct irqaction描述一個中斷服務例程:
struct irqaction { /* 此中斷服務例程的中斷處理函數 */ irq_handler_t handler; /* 設備ID,一般用於指向中斷處理時需要的數據結構傳入handler */ void *dev_id; /* 此中斷服務例程在CPU上所對應的設備ID */ void __percpu *percpu_dev_id; /* 鏈表中下一個中斷服務例程 */ struct irqaction *next; /* 進行中斷處理的內核線程執行函數 */ irq_handler_t thread_fn; /* 一個內核線程,用於執行中斷處理 */ struct task_struct *thread; /* IRQ線,IRQ號 */ unsigned int irq; unsigned int flags; unsigned long thread_flags; unsigned long thread_mask; const char *name; /* 指向/proc/irq/n目錄的描述符 */ struct proc_dir_entry *dir; } ____cacheline_internodealigned_in_smp;
irq_stat數組
此數組包含NR_CPUS個元素,系統中每個CPU對應數組中的一個元素。每個元素的類型為irq_cpustat_t,其包含幾個計數器和內核記錄CPU正在做什么的標志。
typedef struct { unsigned int __softirq_pending; /* 表示掛起的軟中斷,每一位表示一個軟中斷,為1表示掛起 */ long idle_timestamp; /* CPU變為空閑的時間點 */ /* 硬中斷統計. */ unsigned int irq_timer_count; /* 定時器中斷統計 */ unsigned int irq_syscall_count; /* 系統調用中斷統計 */ unsigned int irq_resched_count; unsigned int irq_hv_flush_count; unsigned int irq_call_count; unsigned int irq_hv_msg_count; unsigned int irq_dev_intr_count; } ____cacheline_aligned irq_cpustat_t;
數據結構總結
到此,在中斷處理中所涉及的幾個重要的數據結構已經說明,其最主要的數據結構為:中斷描述符(struct irq_desc),中斷控制器描述符(struct irq_chip),中斷服務例程(struct irqaction)。它們的組織形式如下: