在 Linux 系統中,硬體中斷是驅動程式與硬體互動的重要機制。有效的中斷處理能讓系統即時回應硬體事件,本文將以 Raspberry Pi 的按鈕中斷為例,解析 Linux 驅動程式中斷處理的流程與核心概念,包含中斷處理函式的撰寫、裝置樹的設定、平台驅動程式模型的註冊,以及如何運用 Tasklets 和 Softirqs 等延遲工作機制來提升系統的反應速度和效率。理解這些核心機制對於開發高效能且穩定的 Linux 驅動程式至關重要,能確保系統在處理硬體事件時保持穩定和快速回應。
中斷處理在裝置驅動程式中的應用
在Linux裝置驅動程式開發中,中斷處理是一種關鍵機制,允許硬體裝置在需要時通知CPU。本章節將探討如何在裝置驅動程式中處理中斷,並透過一個按鈕中斷裝置的實驗室練習來演示相關概念。
中斷處理基礎
在Linux核心中,中斷處理函式的傳回型別是irqreturn_t。這個列舉型別定義了三種可能的傳回值:IRQ_NONE、IRQ_HANDLED和IRQ_WAKE_THREAD。
irqreturn_t列舉定義
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
IRQ_NONE:表示中斷不是由該裝置產生的,或者未被處理。IRQ_HANDLED:表示中斷已被裝置處理。IRQ_WAKE_THREAD:請求喚醒處理該中斷的執行緒。
中斷分享與處理
驅動程式應該盡可能支援中斷分享,這需要能夠檢測硬體是否觸發了中斷。當中斷發生時,驅動程式會被呼叫,如果它確定是自己的硬體觸發了中斷,則傳回IRQ_HANDLED;否則,傳回IRQ_NONE,允許核心呼叫下一個中斷處理程式。
實驗室練習:按鈕中斷裝置模組
這個實驗室練習將實作一個簡單的驅動程式,用於管理按鈕產生的中斷。
硬體描述
使用MikroElektronika Button R click板,將其INT引腳連線到Raspberry Pi的GPIO23引腳。
裝置樹描述
需要在裝置樹中組態GPIO23為輸入,並啟用內部下拉電阻。當按鈕被按下時,GPIO輸入值被設為Vcc,釋放時設為GND,如果在request_irq()函式中傳遞了IRQF_TRIGGER_FALLING標誌,則會產生中斷。
&gpio {
key_pin: key_pin {
brcm,pins = <23>;
brcm,function = <0>; /* Input */
brcm,pull = <1>; /* Pull down */
};
};
&soc {
int_key {
compatible = "arrow,intkey";
pinctrl-names = "default";
pinctrl-0 = <&key_pin>;
gpios = <&gpio 23 0>;
interrupts = <23 1>;
interrupt-parent = <&gpio>;
};
};
程式碼描述
驅動程式的主要部分包括:
- 包含必要的標頭檔案
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/miscdevice.h>
- 在
probe()函式中取得Linux IRQ號
使用兩種方法取得Linux IRQ號:透過devm_gpiod_get()和gpiod_to_irq(),或透過platform_get_irq()。
static int __init my_probe(struct platform_device *pdev)
{
int ret_val, irq;
struct gpio_desc *gpio;
struct device *dev = &pdev->dev;
/* 第一種方法取得虛擬Linux IRQ號 */
gpio = devm_gpiod_get(dev, NULL, GPIOD_IN);
irq = gpiod_to_irq(gpio);
/* 第二種方法取得虛擬Linux IRQ號 */
irq = platform_get_irq(pdev, 0);
devm_request_irq(dev, irq, hello_keys_isr, IRQF_TRIGGER_FALLING, HELLO_KEYS_NAME, dev);
misc_register(&helloworld_miscdevice);
return 0;
}
#### 內容解密:
devm_gpiod_get(dev, NULL, GPIOD_IN):用於從裝置樹中的gpios屬性取得GPIO描述符,並將其組態為輸入模式。gpiod_to_irq(gpio):將GPIO描述符轉換為對應的Linux IRQ號。platform_get_irq(pdev, 0):從裝置樹中的interrupts屬性取得硬體IRQ號,並傳回對應的Linux IRQ號。devm_request_irq(dev, irq, hello_keys_isr, IRQF_TRIGGER_FALLING, HELLO_KEYS_NAME, dev):請求IRQ線,並註冊中斷處理函式hello_keys_isr,當檢測到下降沿觸發的中斷時呼叫。
這個實驗室練習展示瞭如何在Linux裝置驅動程式中處理中斷,包括組態裝置樹、取得IRQ號、註冊中斷處理函式等關鍵步驟。透過這些知識,可以開發出能夠有效處理硬體事件的驅動程式。
在裝置驅動程式中處理中斷(Handling Interrupts in Device Drivers)
簡介
在 Linux 驅動程式開發中,處理硬體中斷是一項關鍵任務。本文將探討如何在裝置驅動程式中處理中斷,以 Raspberry Pi 為例,使用平台驅動程式模型實作按鈕中斷的處理。
驅動程式實作步驟
1. 中斷處理函式的編寫
中斷處理函式(Interrupt Handler)是驅動程式的核心部分,負責在硬體中斷發生時執行相應的處理。以下是一個範例:
static irqreturn_t hello_keys_isr(int irq, void *data)
{
struct device *dev = data;
dev_info(dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);
return IRQ_HANDLED;
}
內容解密:
irqreturn_t是中斷處理函式的傳回型別,表示中斷處理的結果。hello_keys_isr函式接收兩個引數:irq(中斷號)和data(與中斷相關的資料,通常是裝置結構指標)。- 透過
dev_info列印中斷接收訊息到控制檯。 - 傳回
IRQ_HANDLED表示中斷已被正確處理。
2. 宣告支援的裝置列表
驅動程式需要宣告它所支援的裝置,這是透過 of_device_id 結構完成的:
static const struct of_device_id my_of_ids[] = {
{ .compatible = "arrow,intkey"},
{},
};
MODULE_DEVICE_TABLE(of, my_of_ids);
內容解密:
.compatible欄位指定了驅動程式支援的裝置相容字串。MODULE_DEVICE_TABLE巨集註冊裝置表格,使其可供核心使用。
3. 平台驅動程式結構註冊
平台驅動程式結構包含了驅動程式的回撥函式和相關資訊:
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "intkey",
.of_match_table = my_of_ids,
.owner = THIS_MODULE,
}
};
內容解密:
.probe和.remove分別是驅動程式的初始化和移除回撥函式。.driver.name指定了驅動程式的名稱。.of_match_table指向支援的裝置列表。.owner指定了模組的所有者,通常是THIS_MODULE。
4. 向平台匯流排註冊驅動程式
module_platform_driver(my_platform_driver);
內容解密:
- 這行巨集呼叫註冊了平台驅動程式結構,使其可供平台匯流排使用。
程式碼範例完整呈現
以下是完整的 int_rpi3_key.c 程式碼,包含了上述所有部分:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/miscdevice.h>
static char *HELLO_KEYS_NAME = "PB_KEY";
static irqreturn_t hello_keys_isr(int irq, void *data)
{
struct device *dev = data;
dev_info(dev, "interrupt received. key: %s\n", HELLO_KEYS_NAME);
return IRQ_HANDLED;
}
static int __init my_probe(struct platform_device *pdev)
{
// 省略部分程式碼...
return 0;
}
static int __exit my_remove(struct platform_device *pdev)
{
// 省略部分程式碼...
return 0;
}
static const struct of_device_id my_of_ids[] = {
{ .compatible = "arrow,intkey"},
{},
};
MODULE_DEVICE_TABLE(of, my_of_ids);
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "intkey",
.of_match_table = my_of_ids,
.owner = THIS_MODULE,
}
};
module_platform_driver(my_platform_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alberto Liberal <[email protected]>");
MODULE_DESCRIPTION("This is a button INT platform driver");
Linux 核心中的中斷處理與延遲工作機制
Linux 核心在處理硬體中斷時,採用了兩種不同的執行上下文:行程上下文(Process Context)與中斷上下文(Interrupt Context)。這兩種上下文決定了核心程式碼的執行方式與限制。
行程上下文與中斷上下文
行程上下文:當核心代表使用者行程執行時,例如執行系統呼叫的核心服務例程,就處於行程上下文中。此外,由工作佇列(Workqueues)和執行緒中斷(Threaded Interrupts)排程的延遲工作也在行程上下文中執行。這些核心執行緒在核心空間的行程上下文中執行,但不代表任何使用者行程。行程上下文中的程式碼可以阻塞(Block)。
中斷上下文:當硬體中斷控制器發出請求時,核心會非同步地執行中斷處理程式,這種特殊的上下文稱為「原子上下文」(Atomic Context),因為在這種上下文中執行的程式碼無法阻塞。中斷不是可排程的,它們發生並執行自己的處理程式,產生自己的上下文。Softirqs、Tasklets 和 Timers 在中斷上下文中執行,這意味著它們不能呼叫阻塞函式。
延遲工作機制
延遲工作允許將程式碼排程到稍後執行。這種機制用於補充中斷處理程式的功能,因為中斷處理有重要的需求和限制:
- 中斷處理程式的執行時間必須盡可能短。
- 在中斷上下文中不能使用阻塞呼叫。
使用延遲工作機制,可以在中斷處理程式中執行最少量的時間敏感工作,並將非時間敏感的工作排程到稍後執行,當中斷被啟用時。這種延遲工作在中斷處理中也稱為「下半部」(Bottom-Half),因為它的目的是執行中斷處理程式(上半部,Top-Half)剩下的工作。
下半部與上半部的區別
上半部(Top-Half):立即執行時間關鍵的工作,通常是中斷處理程式本身。上半部應該盡快完成,因為所有中斷都被停用。
下半部(Bottom-Half):執行被延遲的工作,即那些時間依賴性較低、較不關鍵的動作。下半部由中斷服務例程(ISR)觸發。當下半部執行時,中斷是啟用的。
Softirqs
Softirqs 在中斷上下文中執行,用於最關鍵和時間敏感的下半部處理工作。它們在所有中斷處理程式完成後執行,可以被任何上半部中斷搶佔。Softirqs 不能被裝置驅動程式使用,因為它們是為各種核心子系統保留的;在編譯時期定義了固定數量的 Softirqs。
目前的核心版本定義了以下型別的 Softirqs:
enum {
HI_SOFTIRQ = 0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
NR_SOFTIRQS
};
每種型別都有特定的用途,例如 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 用於執行 Tasklets,而 TIMER_SOFTIRQ 用於執行 Timers。Softirqs 在中斷上下文中執行,因此不能呼叫阻塞函式。如果 Softirq 處理程式需要呼叫阻塞函式,可以排程工作佇列來執行這些阻塞呼叫。
Tasklets
Tasklet 是建立在 Softirqs 之上的另一種下半部機制,同樣在中斷上下文中執行。Tasklet 與 Softirq 的主要區別在於 Tasklet 可以動態分配,因此可以被裝置驅動程式使用。Tasklet 由一個 Tasklet 結構表示,需要在使用前初始化。
Tasklet 在 HI 和 TASKLET Softirqs 中執行。它們在所有中斷啟用的情況下執行,但保證在同一時間只會在一個 CPU 上執行。可以使用 DECLARE_TASKLET 或 tasklet_init 函式初始化 Tasklet。
void handler(unsigned long data);
DECLARE_TASKLET(tasklet, handler, data);
或者手動初始化:
struct tasklet_struct tasklet;
tasklet_init(&tasklet, handler, data);
中斷處理程式可以使用 tasklet_schedule 或 tasklet_hi_schedule 函式排程 Tasklet 的執行。
Timers
Timers 是另一種形式的延遲工作,在中斷上下文中執行,根據 Softirqs 實作。它們由 timer_list 結構定義。使用前,必須透過呼叫 setup_timer 初始化 Timer:
void setup_timer(struct timer_list *timer,
void (*function)(unsigned long),
unsigned long data);
然後,可以使用 mod_timer 排程 Timer:
int mod_timer(struct timer_list *timer, unsigned long expires);
內容解密:
setup_timer 函式的作用:初始化
timer_list結構的內部欄位,並將指定的函式作為 Timer 的處理程式。mod_timer 函式的作用:排程 Timer 的到期時間,並啟動 Timer。
Timer 的使用場景:Linux 驅動程式中使用 Timer 來實作延遲工作,適用於需要週期性或延遲執行的任務。
Timer 的限制:由於 Timer 在中斷上下文中執行,因此不能呼叫阻塞函式,否則會導致系統當機或未定義行為。