/************************************************************************************
*本文為個人學習記錄,如有錯誤,歡迎指正。
* https://blog.csdn.net/fridayll/article/details/51854126
* https://www.cnblogs.com/edver/p/7260696.html
* https://www.linuxidc.com/Linux/2017-08/146264.htm
* https://www.cnblogs.com/amanlikethis/p/6941666.html?utm_source=itdadao&utm_medium=referral
* https://www.cnblogs.com/chen-farsight/p/6155503.html
************************************************************************************/
1. 中斷簡介
中斷是指在CPU正常運行期間,由於內外部事件或由程序預先安排的事件引起的CPU暫時停止正在運行的程序,轉而為該內部或外部事件或預先安排的事件服務的程序中去,服務完畢后再返回去繼續運行被暫時中斷的程序。Linux中通常分為外部中斷(又叫硬件中斷)和內部中斷(又叫異常)。
2. Linux內核中斷機制的初始化
2.1 相關數據結構
linux內核將所有的中斷統一編號,使用一個irq_desc[NR_IRQS]的結構體數組來描述這些中斷:每個數組項對應着一個中斷源,記錄了中斷的入口處理函數(不是用戶注冊的處理函數)、中斷標記,並提供了中斷的底層硬件訪問函數(中斷清除、屏蔽、使能)。另外,通過這個結構體數組項成員action,能夠找到用戶注冊的中斷處理函數。
(1)irq_desc[NR_IRQS]數組
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { [0 ... NR_IRQS-1] = { .status = IRQ_DISABLED, .chip = &no_irq_chip, .handle_irq = handle_bad_irq, .depth = 1, .lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock), } };
(2)struct irq_desc
Linux內核中使用struct irq_desc來描述一個中斷源。Linux內核中將CPU的所有中斷進行編號,具體編號定義在/kernel/arch/arm/mach-s5pv210/include/mach/irqs.h。

struct irq_desc { unsigned int irq; /*中斷號*/ irq_flow_handler_t handle_irq; /* 當前中斷的處理函數入口 */ struct irq_chip *chip; /* 低層的硬件訪問 */ struct msi_desc *msi_desc; void *handler_data; void *chip_data; struct irqaction *action; /* 用戶提供的中斷處理函數鏈表 */ unsigned int status; /* IRQ狀態 */ unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ unsigned int irq_count; /* For detecting broken IRQs */ unsigned int irqs_unhandled; spinlock_t lock; const char *name; /* 中斷名稱 */ } ____cacheline_internodealigned_in_smp;
(3)struct irq_chip
struct irq_chip 是中斷控制器描述符, CPU所對應的一個具體的中斷控制器。

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);//設置中斷類型,其中包括設置GPIO口為中斷輸入 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; };
(4)struct irqaction
struct irqaction是中斷服務程序描述符,該IRQ對應的一系列中斷服務程序。

struct irqaction { irq_handler_t handler; //用戶注冊的中斷處理函數 unsigned long flags; //中斷標識 const char *name; //用戶注冊的中斷名字,cat/proc/interrupts時可以看到 void *dev_id; //可以是用戶傳遞的參數或者用來區分共享中斷 struct irqaction *next; //irqaction結構鏈,一個共享中斷可以有多個中斷處理函數 int irq; //中斷號 struct proc_dir_entry *dir; irq_handler_t thread_fn; struct task_struct *thread; unsigned long thread_flags; };
2.2 中斷的初始化
Linux內核在初始化階段調用 init_IRQ()函數用來初始化中斷體系結構,即初始化irq_desc[NR_IRQS]數組。
//所在文件:/kernel/arch/arm/kernel/irq.c void __init init_IRQ(void) { int irq; for (irq = 0; irq < NR_IRQS; irq++) irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE; init_arch_irq(); }
s5pv210_init_irq()函數在arch/arm/mach-s5pv210/cpu.c中定義,它為所有的中斷設置了芯片相關的數據結構irq_desc[irq].chip,設置了處理函數入口irq_desc[irq].handle_irq。
void __init s5pv210_init_irq(void) { u32 vic[4]; /* S5PV210 supports 4 VIC */ /* All the VICs are fully populated. */ vic[0] = ~0; vic[1] = ~0; vic[2] = ~0; vic[3] = ~0; s5p_init_irq(vic, ARRAY_SIZE(vic)); }
2.3 中斷的處理流程
中斷的處理流程如下:
1) 發生中斷后,CPU執行異常向量vector_irq的代碼;
2)在vector_irq里面,最終會調用中斷處理C程序總入口函數asm_do_IRQ();
3)asm_do_IRQ()根據中斷號調用irq_des[NR_IRQS]數組中的對應數組項中的handle_irq();
4)handle_irq()會使用chip的成員函數來設置硬件,例如清除中斷,禁止中斷,重新開啟中斷等;
5)handle_irq逐個調用用戶在action鏈表中注冊的處理函數。
可見,中斷體系結構的初始化,就是構造irq_desc[NR_IRQS]這個數據結構;用戶注冊中斷就是構造action鏈表;用戶卸載中斷就是從action鏈表中去除對應的項。
3. Linux內核中斷處理程序架構
3.1 申請和釋放中斷
(1)申請IRQ
/* 參數: ** irq:要申請的硬件中斷號
** ** handler:中斷處理函數(頂半部)
** ** irqflags:觸發方式及工作方式 ** 觸發方式:IRQF_TRIGGER_RISING 上升沿觸發 ** IRQF_TRIGGER_FALLING 下降沿觸發 ** IRQF_TRIGGER_HIGH 高電平觸發 ** IRQF_TRIGGER_LOW 低電平觸發
** ** 工作方式:默認是快速中斷(一個設備占用,且中斷例程回調過程中會屏蔽中斷) ** IRQF_SHARED:共享中斷
** ** dev_id:在共享中斷時會用到(中斷注銷與中斷注冊的此參數應保持一致)
** ** 返回值:成功返回 - 0;失敗返回 - 負值(絕對值為錯誤碼) */ int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)
(2)釋放IRQ
/* 參數: ** irq:要注銷的硬件中斷號 ** dev_id:在共享中斷時會用到(中斷注銷與中斷注冊的此參數應保持一致) */ void free_irq(unsigned int irq, void *dev_id);
3.2 Linux中斷處理中的頂半部和底半部機制
(1)頂半部和底半部機制
由於中斷服務程序的執行並不存在於進程上下文,因此,要求中斷服務程序的時間盡可能的短。 為了在中斷執行事件盡可能短和中斷處理需完成大量耗時工作之間找到一個平衡點,Linux將中斷處理分為兩個部分:頂半部(top half)和底半部(bottom half)。
頂半部完成盡可能少的比較緊急的功能,它往往只是簡單地讀取寄存器中的中斷狀態並清除中斷標志后進行“登記中斷”的工作。“登記”意味着將底半部的處理程序掛載到該設備的底半部指向隊列中去。底半部作為工作重心,完成中斷事件的絕大多數任務。
(2)頂半部和底半部划分原則
1) 如果一個任務對時間非常敏感,將其放在頂半部中執行;
2) 如果一個任務和硬件有關,將其放在頂半部中執行;
3) 如果一個任務要保證不被其他中斷打斷,將其放在頂半部中執行;
4) 如果中斷要處理的工作本身很少,所有的工作可在頂半部全部完成;
5) 其他所有任務,考慮放置在底半部執行。
(3)舉例分析
當網卡接受到數據包時,通知內核,觸發中斷,所謂的頂半部就是,及時讀取數據包到內存,防止因為延遲導致丟失,這是很急迫的工作。讀到內存后,對這些數據的處理不再緊迫,此時內核可以去執行中斷前運行的程序,而對網絡數據包的處理則交給底半部處理。
3.3 底半部處理策略1:tasklet(小任務)
引入tasklet,最主要的是考慮支持SMP,提高SMP多個cpu的利用率;不同的tasklet可以在不同的cpu上運行。但是tasklet屬於中斷上下文,因此不能被阻塞,不能睡眠,不可被打斷。
tasklet使用模版:
/* 定義tasklet和底半部處理函數並關聯 */ void xxx_tasklet(unsigned long data); //定義一個名為 my_tasklet 的 struct tasklet 並將其與 my_tasklet_func 綁定,data為傳入 my_tasklet_func的參數 DECLARE_TASKLET(xxx_tasklet, xxx_tasklet_func, data); /* 中斷處理底半部 */ void xxx_tasklet_func() { /* 中斷處理具體操作 */ } /* 中斷處理頂半部 */ irqreturn xxx_interrupt(int irq, void *dev_id) { //do something task_schedule(&xxx_tasklet);//調用tasklet_schedule()函數使系統在適當的時候調度運行底半部 //do something return IRQ_HANDLED; } /* 設備驅動模塊 init */ int __init xxx_init(void) { ... /* 申請設備中斷 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ... return 0; } /* 設備驅動模塊exit */ void __exit xxx_exit(void) { ... /* 釋放中斷 */ free_irq(xxx_irq, NULL); } module_init(xxx_init); module_exit(xxx_exit);
3.4 底半部處理策略2:workqueue(工作隊列)
workqueue的突出特點是下半部會交給worker thead,因此下半部處於進程上下文,可以被重新調度,可以阻塞,也可以睡眠。
workqueue使用模板:
/* 定義工作隊列和底半部函數 */ struct work_struct xxx_wq; void xxx_do_work(unsigned long); /* 中斷處理底半部 */ void xxx_work(unsigned long) { /* do something */ } /* 中斷處理頂半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) { ... schedule_work(&xxx_wq); ... return IRQ_HANDLED; } /* 設備驅動模塊 init */ int __init xxx_init(void) { ... /* 申請設備中斷 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); /* 初始化工作隊列 */ INIT_WORK(&xxx_wq, xxx_do_work);//關聯工作隊列和底半部函數 ... return 0; } /* 設備驅動模塊exit */ void __exit xxx_exit(void) { ... /* 釋放中斷 */ free_irq(xxx_irq, NULL); } module_init(xxx_init); module_exit(xxx_exit);