1.概述
外設産生中斷後,Linux核心會執行一個具體的函數來響應中斷,此函數被稱為中斷服務函數。中斷服務函數運作在中斷上下文中,若中斷線程化後,則中斷事件在程序上下文中處理。不同的外設中斷源,其對應的中斷服務函數一般也不同。是以在使用具體的外設中斷之前,需要注冊對應的中斷服務函數,并指明軟體中斷号、中斷标志、中斷名稱及傳遞給中斷服務函數的參數等。
2.中斷注冊接口介紹
2.1.request_irq
使用
request_irq
注冊一個中斷服務函數,内部調用的是
request_threaded_irq
函數,隻是将
thread_fn
設定為
NULL
。
irq
為軟體中斷号,
handler
為中斷服務函數,
flags
為中斷标志,
name
中斷名稱,
dev
為傳遞給中斷服務函數的參數,可為
NULL
,指明
IRQF_SHARED
時,必須傳遞此參數。傳回值為0表示注冊成功,非0表示注冊失敗。
[include/linux/interrupt.h]
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
irq:軟體中斷号
handler:中斷服務函數
flags:中斷标志
#define IRQF_TRIGGER_RISING 0x00000001 // 上升沿觸發
#define IRQF_TRIGGER_FALLING 0x00000002 // 下降沿觸發
#define IRQF_TRIGGER_HIGH 0x00000004 // 高電平觸發
#define IRQF_TRIGGER_LOW 0x00000008 // 下降沿觸發
// 共享中斷,多個裝置共享一個中斷号(中斷引腳),在中斷處理程式中需要查詢那個外設發生了中斷
#define IRQF_SHARED 0x00000080
#define IRQF_PROBE_SHARED 0x00000100 // 中斷處理程式允許sharing mismach發生
#define __IRQF_TIMER 0x00000200
// 時鐘中斷
#define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
#define IRQF_PERCPU 0x00000400 // 某個CPU特有的中斷
#define IRQF_NOBALANCING 0x00000800 // 禁止CPU之間的中斷均衡
#define IRQF_IRQPOLL 0x00001000 // 中斷被用作輪訓
// 中斷線程執行完成之前不會打開中斷,直至中斷線程處理完所有中斷,才會重新使能中斷
#define IRQF_ONESHOT 0x00002000
#define IRQF_NO_SUSPEND 0x00004000 // 系統suspend期間不要關閉中斷
#define IRQF_FORCE_RESUME 0x00008000 // 系統resume時必須強制使能此中斷
#define IRQF_NO_THREAD 0x00010000 // 此中斷不能中斷線程化
#define IRQF_EARLY_RESUME 0x00020000 // 系統resume時提前使能此中斷
name:中斷名稱
dev:傳遞給中斷服務函數的參數,可為NULL,共享中斷必須傳遞此參數
2.2.request_threaded_irq
使用
request_threaded_irq
注冊一個中斷服務函數和中斷線程化後調用的函數。
irq
為軟體中斷号,
handler
為中斷服務函數,
thread_fn
中斷線程化後核心線程執行的函數,
flags
為中斷标志,
name
中斷名稱,
dev
為傳遞給中斷服務函數的參數,可為
NULL
,指明
IRQF_SHARED
時,必須傳遞此參數。傳回值為0表示注冊成功,非0表示注冊失敗。
[include/linux/interrupt.h]
int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev)
irq:軟體中斷号
handler:中斷服務函數
thread_fn:中斷線程化後調用的函數
flags:中斷标志,和request_irq的中斷标志含義一樣
name:中斷名稱
dev:給中斷服務函數傳遞的參數
2.3.free_irq
request_irq
和
request_threaded_irq
注冊的中斷服務函數使用
free_irq
函數釋放。
[include/linux/interrupt.h]
void free_irq(unsigned int irq, void *dev_id)
irq:軟體中斷号
dev_id:給中斷服務函數傳遞的參數
3.源代碼分析
由于
request_irq
内部調用的是
request_threaded_irq
,是以這裡隻分析
request_threaded_irq
函數。
request_threaded_irq
函數的主要工作是配置設定一個
struct irqaction
結構體并設定相關成員,設定的主要成員有中斷服務函數
handler
,軟體中斷号
irq
、中斷線程化執行的函數
thread_fn
及中斷标志
flags
,
handler
、
irq
、
thread_fn
和
flags
根據傳入的參數設定。最後将配置設定的
struct irqaction
結構體挂接到
struct irq_desc
結構體中的
action
連結清單下面,非共享中斷
action
連結清單隻有一個節點,共享中斷
action
連結清單有多個節點。中斷發生後,會周遊
action
連結清單,使用
handler
或
thread_fn
等進行中斷。使用
request_threaded_irq
注冊中斷時需要注意一下幾點:
(1)IRQ為軟體(虛拟)中斷号,是由硬體中斷号映射而來。
(2)primary handler(中斷上半部分處理函數)和threaded_fn(中斷下半部分處理函數)不能同時為NULL。
(3)當primary handler為NULL且硬體中斷控制器不支援硬體ONESHOT功能時,應設定IRQF_ONESHOT标志來確定不會産生中斷風暴。
(4)啟用了中斷線程化,primary handler函數應該傳回IRQ_WAKE_THREAD,以喚醒中斷處理線程。
[include/linux/interrupt.h]
// 和具體的中斷邏輯處理相關
struct irqaction {
irq_handler_t handler; // 中斷服務函數,注冊中斷時設定
void *dev_id; // 中斷服務函數參數
void __percpu *percpu_dev_id;
struct irqaction *next; // 共享中斷有多個irqaction結構體,next指向下一個irqaction
irq_handler_t thread_fn; // 中斷線程化執行的函數
struct task_struct *thread; // 指向被中斷線程的task_struct
struct irqaction *secondary; // 指向中斷下半部分的irqaction
unsigned int irq; // 軟體中斷号
unsigned int flags; // 中斷标志
unsigned long thread_flags; // 線程标志
unsigned long thread_mask; // 跟蹤中斷線程活動的位圖
const char *name; // 中斷名稱
struct proc_dir_entry *dir; // 指向/proc/irq/NN/name
} ____cacheline_internodealigned_in_smp;
[include/linux/interrupt.h]
request_threaded_irq
// 檢查參數,設定IRQF_SHARED标志時,必須向中斷服務函數傳遞參數,即必須傳入dev_id參數,共享中斷根據
// dev_id區分産生中斷的外設
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
->irq_to_desc // 擷取軟體中斷号對應的irq_desc
->radix_tree_lookup // 定義CONFIG_SPARSE_IRQ,從irq_desc_tree中查找
// 沒有定義CONFIG_SPARSE_IRQ,從irq_desc數組中查找,索引為軟體中斷号
return (irq < NR_IRQS) ? irq_desc + irq : NULL
->handler = irq_default_primary_handler // 若沒有設定中斷服務函數,則設定預設的中斷服務函數
->kzalloc // 配置設定irqaction結構體記憶體
// 設定irqaction結構體
action->handler = handler; // 設定中斷處理函數
action->thread_fn = thread_fn; // 設定中斷線程化核心線程執行的函數
action->flags = irqflags; // 中斷标志
action->name = devname; // 中斷名稱
action->dev_id = dev_id; // 傳遞給中斷服務函數的參數
->__setup_irq
new->irq = irq // 設定irqaction結構體中的軟體中斷号irq
->irq_settings_is_nested_thread // 檢查此中斷是否嵌套到另一個中斷線程中
return desc->status_use_accessors & _IRQ_NESTED_THREAD
->new->handler = irq_nested_primary_handler // 如嵌套,則設定新的中斷處理函數
->irq_settings_can_thread // 中斷是否可以線程化
->irq_setup_forced_threading
Linux中斷強制線程化是一個過渡方案,目前還有很多的驅動使用舊版本的API注冊中斷,這些驅動的中斷處理通常采用上下半部分的方式。
force_irqthreads // 如果force_irqthreads定義為true,則強制将中斷線程化
new->flags |= IRQF_ONESHOT // 強制中斷服務函數線程化,其在關中斷的狀态下運作,是以需要設定此标志
// 如果handler不為irq_default_primary_handler且設定了中斷線程化執行的函數
new->secondary = kzalloc() // 則配置設定中斷下半部分使用的irqaction結構體
// 設定強制中斷線程化的中斷服務函數
new->secondary->handler = irq_forced_secondary_handler;
// 設定中斷線程化核心線程執行的函數
new->secondary->thread_fn = new->thread_fn;
new->secondary->dev_id = new->dev_id; // 設定傳遞給中斷服務函數的參數
new->secondary->irq = new->irq; // 設定軟體中斷号
new->secondary->name = new->name; // 設定中斷名稱
->set_bit // 設定強制中斷線程化标志,設定到irqaction的thread_flags标志中
new->thread_fn = new->handler
// 設定預設的中斷服務函數為irq_default_primary_handler
new->handler = irq_default_primary_handler
->setup_irq_thread // 如果傳入了thread_fn,則建立中斷線程,優先級為50,排程政策為SCHED_FIFO
if (!secondary) { // 沒有強制中斷線程化
// kthread_create建立的核心線程由核心線程kthreadd建立,傳回建立線程的task_struct指針
// 中斷線程喚醒後運作的第一個函數為irq_thread
t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
} else { // 強制中斷線程化
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
param.sched_priority -= 1;
}
->sched_setscheduler_nocheck // 設定核心線程的優先級
new->thread = t // 使irqaction中的thread指向建立線程的task_struct
->set_bit // 設定irqaction中的标記為IRQTF_AFFINITY
->setup_irq_thread // 如果需要中斷下半部分,則啟動進行中斷下半部分的核心線程
// 将新配置設定的irqaction結構體追加到irq_desc結構體的action連結清單的末尾,共享中斷,action
// 連結清單有多個節點,非共享中斷,action連結清單隻有一個節點
*old_ptr = new
->irq_pm_install_action // 電源管理相關
desc->irq_count = 0
desc->irqs_unhandled = 0
// kthread_create建立的核心線程預設不運作,需要使用wake_up_process喚醒
->wake_up_process(new->thread) // 喚醒中斷線程化的線程
->wake_up_process(new->secondary->thread) // 喚醒中斷下半部分的線程
->register_irq_proc // 将中斷注冊到proc檔案系統中
下面分析一下
free_irq
的執行過程,主要功能是釋放掉
request_threaded_irq
注冊的資源。
[include/linux/interrupt.h]
free_irq
->irq_to_desc // 根據軟體中斷号擷取中斷描述符
desc->affinity_notify = NULL // 如果定義CONFIG_SMP,則将中斷對CPU的親和力設定為NULL
->__free_irq
->irq_to_desc // 根據軟體中斷号擷取中斷描述符
->in_interrupt // 檢測是否在中斷上下文中free IRQ
->raw_spin_lock_irqsave // 擷取自旋鎖并禁止中斷
// 周遊action連結清單,查找要釋放的action
action_ptr = &desc->action;
for (;;) {
action = *action_ptr;
if (action->dev_id == dev_id) // action中的dev_id和傳入的dev_id相等,則跳出循環
break;
action_ptr = &action->next;
}
// 從連結清單中移除此action
*action_ptr = action->next
->irq_pm_remove_action // 電源管理相關操作
// 如果action連結清單為空,說明此中斷不需要處理,需要關閉中斷
if (!desc->action) { // action連結清單為空
irq_shutdown(desc); // 關閉此中斷
irq_release_resources(desc); // 釋放中斷資源
}
->raw_spin_unlock_irqrestore // 釋放自旋鎖
->unregister_handler_proc // 登出proc檔案系統中的中斷資訊
->synchronize_irq // 同步中斷,等待其他CPU處理完要free的中斷任務
->irq_to_desc // 擷取中斷描述符
->__synchronize_hardirq
do {
unsigned long flags;
while (irqd_irq_inprogress(&desc->irq_data)) // 擷取中斷處理狀态
// arm32 cpu_relax執行一條記憶體屏障指令,arm64執行一條yield指令和記憶體屏障指令
// yield指令可以降低CPU功耗
cpu_relax();
// 上述擷取中斷處理狀态缺乏嚴格的記憶體屏障,可能存在誤讀的情況,是以這裡再次讀取
raw_spin_lock_irqsave(&desc->lock, flags); // 加鎖
inprogress = irqd_irq_inprogress(&desc->irq_data); // 讀取
raw_spin_unlock_irqrestore(&desc->lock, flags); // 解鎖
} while (inprogress); // 循環讀取,直到其他CPU處理完中斷
->wait_event // 等待中斷處理線程處理完中斷
->kthread_stop // 停止中斷處理線程
->kthread_stop // 停止中斷下半部分處理線程
->kfree // 釋放中斷下半部分secondary的記憶體
->kfree // 釋放action對應的記憶體
4.zynq7k序列槽0的中斷注冊
zynq7k序列槽0的中斷注冊在
cdns_uart_startup
函數中完成,
irq
為已經映射好的軟體中斷号,
cdns_uart_isr
為注冊的中斷處理函數,
flags
為0,中斷類型和中斷觸發方式已在裝置樹中指定,即序列槽0的中斷類型為SPI中斷,中斷觸發方式為高電平,中斷名稱為
"xuartps"
,
port
為給中斷處理函數傳遞的參數。中斷處理函數
cdns_uart_startup
在Linux核心高層中斷進行中分析。
[drivers/tty/serial/xilinx_uartps.c]
#define CDNS_UART_NAME "xuartps" // 中斷名稱
cdns_uart_startup
->request_irq(port->irq, cdns_uart_isr, 0, CDNS_UART_NAME, (void *)port) // 注冊中斷
參考資料
- Linux kernel V4.6版本源碼
- 《奔跑吧 Linux核心:基于Linux 4.x核心源代碼問題分析》
- 《Zynq-7000 SoC Technical Reference Manual》
- 《ARM® Generic Interrupt Controller Architecture version 2.0》