Linux 核心提供計時器和中斷處理機制,方便驅動程式開發者管理週期性任務和硬體事件。計時器允許在特定時間點執行函式,適用於輪詢或週期性操作。中斷處理則回應硬體事件,執行緒化中斷將處理過程分為中斷上下文和行程上下文兩個階段,提升效率和穩定性。Workqueue 機制則延遲執行工作項,適用於中斷後續的複雜任務處理。鎖機制確保多執行緒環境下分享資源的安全存取,避免競態條件。等待佇列讓行程睡眠直到特定事件發生,並在中斷發生時被喚醒。理解這些機制對於開發高效穩定的 Linux 驅動程式至關重要。

核心概念:Linux 裝置驅動程式中的中斷處理與計時器應用

在 Linux 裝置驅動程式開發中,中斷處理(Interrupt Handling)與計時器(Timer)機制是兩個至關重要的核心概念。本文將探討這兩個主題,並透過實際範例程式碼展示如何在驅動程式中有效地運用它們。

計時器機制

Linux 核心提供了一套計時器機制,允許驅動程式開發者在特定的時間點執行特定的函式。這對於需要定期執行的任務(如輪詢裝置狀態或執行週期性操作)非常有用。

計時器的基本操作

  • 設定計時器:使用 setup_timer 函式初始化計時器,並指定當計時器到期時要呼叫的處理函式。
  • 啟動計時器:使用 mod_timer 函式啟動或重新啟動計時器,指定計時器到期時間。
  • 停止計時器:使用 del_timerdel_timer_sync 函式停止計時器,避免計時器到期後呼叫已經解除安裝的處理函式。

實際範例:LED 閃爍驅動程式

以下是一個使用計時器實作 LED 閃爍的驅動程式範例:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/io.h>
#include <linux/timer.h>

// 定義 GPIO 暫存器的結構
struct GpioRegisters {
    uint32_t GPFSEL[6];
    uint32_t Reserved1;
    uint32_t GPSET[2];
    uint32_t Reserved2;
    uint32_t GPCLR[2];
};

// 設定 GPIO 功能
static void SetGPIOFunction(int GPIO, int functionCode) {
    // ...
}

// 設定 GPIO 輸出值
static void SetGPIOOutputValue(int GPIO, bool outputValue) {
    // ...
}

static struct timer_list s_BlinkTimer;
static int s_BlinkPeriod = 1000; // 預設閃爍週期為 1000ms

// 計時器處理函式
static void BlinkTimerHandler(unsigned long unused) {
    static bool on = false;
    on = !on;
    SetGPIOOutputValue(27, on); // 控制 LED 亮滅
    mod_timer(&s_BlinkTimer, jiffies + msecs_to_jiffies(s_BlinkPeriod));
}

// 初始化函式
static int __init my_probe(struct platform_device *pdev) {
    // ...
    setup_timer(&s_BlinkTimer, BlinkTimerHandler, 0);
    mod_timer(&s_BlinkTimer, jiffies + msecs_to_jiffies(s_BlinkPeriod));
    // ...
}

// 清除函式
static int __exit my_remove(struct platform_device *pdev) {
    // ...
    del_timer(&s_BlinkTimer);
    // ...
}

程式碼解析

  1. setup_timermod_timer:在 my_probe 函式中,使用 setup_timer 初始化計時器,並指定 BlinkTimerHandler 為處理函式。然後,使用 mod_timer 啟動計時器,設定初始到期時間。
  2. BlinkTimerHandler:在計時器到期時,BlinkTimerHandler 被呼叫,負責切換 LED 狀態並重新啟動計時器。
  3. del_timer:在 my_remove 函式中,使用 del_timer 停止計時器,避免在模組解除安裝後仍觸發計時器中斷。

中斷處理

在 Linux 裝置驅動程式中,中斷處理是一種重要的機制,用於處理硬體中斷事件。對於某些裝置(如感測器),可能需要在中斷處理函式中存取裝置暫存器,但某些匯流排(如 I2C 或 SPI)的讀寫操作可能不是非阻塞的。

執行緒化中斷

為瞭解決上述問題,Linux 核心提供了執行緒化中斷(Threaded Interrupts)機制,允許將中斷處理分成兩個階段:

  1. 中斷上下文階段:執行關鍵操作,通常是快速、非阻塞的操作。
  2. 行程上下文階段:執行剩餘操作,可以是阻塞的,如存取裝置暫存器。

使用 devm_request_threaded_irq 函式註冊執行緒化中斷處理函式:

int devm_request_threaded_irq(struct device *dev, unsigned int irq,
                              irq_handler_t handler, irq_handler_t thread_fn,
                              unsigned long irqflags, const char *devname, void *dev_id)
  • handler:中斷上下文階段的處理函式。
  • thread_fn:行程上下文階段的處理函式。
此圖示
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Linux 驅動程式中斷處理與計時器應用

package "Linux Shell 操作" {
    package "檔案操作" {
        component [ls/cd/pwd] as nav
        component [cp/mv/rm] as file
        component [chmod/chown] as perm
    }

    package "文字處理" {
        component [grep] as grep
        component [sed] as sed
        component [awk] as awk
        component [cut/sort/uniq] as text
    }

    package "系統管理" {
        component [ps/top/htop] as process
        component [systemctl] as service
        component [cron] as cron
    }

    package "管線與重導向" {
        component [| 管線] as pipe
        component [> >> 輸出] as redirect
        component [$() 命令替換] as subst
    }
}

nav --> file : 檔案管理
file --> perm : 權限設定
grep --> sed : 過濾處理
sed --> awk : 欄位處理
pipe --> redirect : 串接命令
process --> service : 服務管理

note right of pipe
  命令1 | 命令2
  前者輸出作為後者輸入
end note

@enduml

內容解密:

此圖示展示了 Linux 裝置驅動程式中的兩個重要概念:計時器機制和中斷處理。計時器機制包括設定、啟動和停止計時器的操作。中斷處理則著重於執行緒化中斷,將中斷處理分成中斷上下文階段和行程上下文階段,以提高系統的效率和穩定性。

中斷處理與Workqueue機制深入解析

在Linux裝置驅動程式開發中,中斷處理(Interrupt Handling)與Workqueue機制是兩個至關重要的概念。本文將探討這兩個主題,解析其核心原理、實作方法以及應用場景。

中斷處理機制

中斷是硬體與作業系統之間的一種溝通方式,當硬體需要CPU的關注時,便會發出中斷訊號。Linux核心透過中斷處理機制來回應這些訊號,並執行相應的中斷處理程式(Interrupt Handler)。

request_threaded_irq函式

在Linux核心中,request_threaded_irq函式用於註冊中斷處理程式。其函式原型如下:

int request_threaded_irq(unsigned int irq, 
                         irq_handler_t handler, 
                         irq_handler_t thread_fn, 
                         unsigned long irqflags, 
                         const char *devname, 
                         void *dev_id);

該函式允許開發者註冊兩個中斷處理函式:handlerthread_fn。當中斷發生時,handler會在中斷上下文(Interrupt Context)中被呼叫,而thread_fn則會在核心執行緒(Kernel Thread)中執行。

中斷處理標誌

在註冊中斷處理程式時,可以透過irqflags引數設定中斷處理的標誌。常見的標誌包括:

  • IRQF_DISABLED:執行中斷處理程式時停用所有中斷。
  • IRQF_SHARED:允許多個中斷處理程式分享同一中斷線。
  • IRQF_ONESHOT:在執行thread_fn後重新啟用中斷線。

程式碼範例與解析

struct irq_devres *dr;
int rc;
dr = devres_alloc(devm_irq_release, sizeof(struct irq_devres), GFP_KERNEL);
if (!dr)
    return -ENOMEM;
rc = request_threaded_irq(irq, handler, thread_fn, irqflags, devname, dev_id);
if (rc) {
    devres_free(dr);
    return rc;
}

內容解密:

  1. 分配irq_devres結構:使用devres_alloc函式分配一個irq_devres結構,用於管理中斷資源。
  2. 註冊中斷處理程式:呼叫request_threaded_irq函式註冊中斷處理程式。如果註冊失敗,則釋放之前分配的irq_devres結構。
  3. 錯誤處理:如果註冊失敗,函式傳回錯誤碼。

Workqueue機制

Workqueue是Linux核心提供的一種延遲工作機制,它允許將工作項(Work Item)排入工作佇列(Workqueue),並由核心執行緒非同步執行。

工作項與工作佇列

工作項是一個簡單的資料結構,包含一個指向待執行函式的指標。工作佇列則是由多個工作項組成的佇列,由核心執行緒負責執行其中的工作項。

Workqueue API

Linux核心提供了一系列Workqueue API,用於建立、初始化和排程工作項。常見的API包括:

  • DECLARE_WORKDECLARE_DELAYED_WORK:宣告並初始化工作項。
  • INIT_WORKINIT_DELAYED_WORK:初始化已宣告的工作項。
  • schedule_workschedule_delayed_work:將工作項排入工作佇列。

程式碼範例與解析

void my_work_handler(struct work_struct *work);
DECLARE_WORK(my_work, my_work_handler);
schedule_work(&my_work);

內容解密:

  1. 宣告並初始化工作項:使用DECLARE_WORK宏宣告並初始化一個名為my_work的工作項,其處理函式為my_work_handler
  2. 排程工作項:呼叫schedule_work函式將my_work排入工作佇列,等待核心執行緒執行。

核心觀念:Linux 核心中的工作佇列與鎖機制

Linux 核心在處理裝置驅動程式的中斷(Interrupt)時,需要有效地管理延遲工作(Deferred Work)並確保分享資源的安全存取。本文將探討工作佇列(Workqueue)的實作原理及其在中斷處理中的角色,同時分析核心中的鎖機制(Locking Mechanism),確保多執行緒環境下資料的一致性。

工作佇列(Workqueue)的基本原理與應用

工作佇列是一種將工作項(Work Item)延後執行的機制,適用於需要在中斷處理後續執行較複雜任務的情境。核心透過 work_struct 結構表示工作項,並使用 workqueue_struct 管理工作佇列。

建立與操作工作佇列

核心提供了兩種主要函式來建立工作佇列:

  1. create_workqueue(const char *name):為每個處理器核心建立一個執行緒。
  2. create_singlethread_workqueue(const char *name):僅建立一個執行緒處理所有工作項。

範例程式碼如下:

struct workqueue_struct *my_workqueue;
my_workqueue = create_singlethread_workqueue("my_workqueue");
INIT_WORK(&my_work, my_work_handler);
queue_work(my_workqueue, &my_work);

工作佇列的運作流程

  1. 初始化工作項並將其加入工作佇列。
  2. 工作佇列中的工作項由核心執行緒(如 events/x)執行。
  3. 可使用 flush_workqueue 確保所有工作項完成執行,並透過 destroy_workqueue 銷毀工作佇列。

內容解密:

  • create_singlethread_workqueue 建立一個名為 “my_workqueue” 的工作佇列,用於延後執行特定任務。
  • INIT_WORK 初始化 my_work,並指定其處理函式為 my_work_handler
  • queue_workmy_work 加入佇列,等待核心執行緒處理。
  • 使用 flush_workqueuedestroy_workqueue 確保資源正確釋放。

鎖機制在核心中的重要性

在多核心或多執行緒環境下,分享資源的存取可能導致競態條件(Race Condition)。核心透過鎖機制確保同一時間僅有一個執行緒能進入關鍵區域(Critical Region)。

自旋鎖(Spinlock)與互斥鎖(Mutex)

  1. 自旋鎖:適用於短時間持有鎖的場景。若無法取得鎖,執行緒會持續嘗試直到成功。自旋鎖在非 SMP(對稱多處理器)核心中僅停用搶佔(Preemption)。

    spin_lock_irqsave(&my_lock, flags);
    // 關鍵區域
    spin_unlock_irqrestore(&my_lock, flags);
    
  2. 互斥鎖:允許執行緒在無法取得鎖時進入睡眠狀態,適合長時間持有鎖的場景。

    mutex_lock_interruptible(&my_mutex);
    // 關鍵區域
    mutex_unlock(&my_mutex);
    

內容解密:

  • spin_lock_irqsavespin_unlock_irqrestore 用於在中斷上下文與行程上下文中分享鎖時,避免死鎖。
  • 在單處理器核心中,若未啟用搶佔,則自旋鎖不會實際生效。
  • 使用互斥鎖時,應避免呼叫不可中斷的 mutex_lock,以免影響系統回應性。

分享鎖在中斷與行程上下文中的應用

若需要在中斷服務例程(ISR)與行程上下文中使用相同的鎖,必須使用 spin_lock_irqsavespin_unlock_irqrestore 以停用中斷,避免死鎖發生。

範例流程:

  1. 行程上下文取得鎖後被中斷打斷。
  2. ISR 嘗試取得同一個鎖,導致自旋等待。
  3. 使用 spin_lock_irqsave 可避免此問題,因其會停用本地中斷。

內容解密:

  • 停用中斷可防止本地處理器上的死鎖,但其他處理器仍可處理中斷。
  • 正確使用鎖可確保分享資源的安全存取。

在裝置驅動程式中處理中斷:核心睡眠機制與實作

在Linux核心程式設計中,經常需要讓使用者行程進入睡眠狀態,並在特定工作完成時喚醒它們。「在核心中睡眠」的概念是裝置驅動程式設計中的重要環節。本文將探討Linux核心中的睡眠機制,並透過實驗室範例展示如何實作一個能夠讓行程睡眠並在中斷發生時喚醒的裝置驅動程式。

核心中的睡眠機制

當一個行程被置於睡眠狀態時,它會被標記為特殊狀態並從排程器的執行佇列中移除。在某些事件發生之前,該行程不會被排程到任何CPU上執行,因此不會執行。睡眠中的行程被擱置在一旁,等待某個未來事件的發生。

在Linux裝置驅動程式中,讓行程睡眠是一件簡單的事情,但必須遵守一些規則以確保安全地讓行程睡眠。首先,絕不能在原子上下文中睡眠,也不能在中斷被關閉時睡眠。雖然在持有訊號量(semaphore)時可以睡眠,但需要非常小心,因為這可能會導致其他等待該訊號量的執行緒也進入睡眠狀態。

另一個重要的注意事項是,當行程被喚醒時,我們不知道它離開CPU多久,也不知道這段時間內發生了什麼變化。同時,我們也不知道是否有其他行程因為相同的事件而進入睡眠狀態。因此,必須確保行程能夠被找到並喚醒,這是透過一個名為等待佇列(wait queue)的資料結構來實作的。

等待佇列的實作

等待佇列是一個行程列表,所有行程都在等待某個特定事件的發生。在Linux中,等待佇列用於等待某個條件成立時被喚醒。我們需要宣告一個wait_queue_head_t型別的結構,可以靜態或動態地定義和初始化等待佇列:

DECLARE_WAIT_QUEUE_HEAD(name);
// 或
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

wait_event宏(及其變體)會將行程置於睡眠狀態(TASK_UNINTERRUPTIBLE),直到指定的條件評估為真。這些條件在每次wq等待佇列被喚醒時都會被檢查。以下是wait_event宏的不同變體:

wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);

所有這些函式都分享queue引數。condition引數會在睡眠前後被宏評估;直到條件評估為真時,行程才會繼續執行。如果使用wait_event,行程會進入不可中斷的睡眠狀態。建議使用wait_event_interruptible,因為它可以被訊號中斷。帶有超時功能的版本(wait_event_timeoutwait_event_interruptible_timeout)會在指定的時間(以jiffies為單位)後傳回0,無論條件評估結果如何。

喚醒機制

另一個行程或中斷處理程式會喚醒睡眠中的行程。wake_up()函式會喚醒使用相同條件變數進入睡眠狀態的行程。該函式不會阻塞,可以從中斷處理程式中呼叫。以下是其兩種形式:

void wake_up(wait_queue_head_t *queue); /* 喚醒給定佇列上的所有行程 */
void wake_up_interruptible(wait_queue_head_t *queue); /* 限制喚醒執行可中斷睡眠的行程 */

內容解密:

  1. wait_event宏與其變體:這些宏用於讓行程進入睡眠狀態,直到某個條件成立。它們的差異在於是否可以被訊號中斷以及是否具有超時功能。
  2. wake_up函式:用於喚醒因某個條件而進入睡眠狀態的行程。它們可以在中斷處理程式中使用,不會阻塞。
  3. 等待佇列的初始化:透過靜態或動態方式初始化等待佇列頭,為後續的行程睡眠與喚醒做準備。

實驗室7.2:「睡眠裝置」模組實作

在這個實驗室中,我們將開發一個核心模組,使行程進入睡眠狀態,並透過中斷喚醒它。當使用者應用程式嘗試從裝置讀取(系統呼叫)時,行程會被置於睡眠狀態。每次按下或釋放按鈕時,產生的中斷會喚醒行程,並透過驅動程式的讀取回呼函式將中斷型別(按下或釋放)傳送到使用者空間。離開使用者應用程式後,可以從檔案中讀取所有產生的中斷。

裝置樹描述

GPIO23引腳將在裝置樹(DT)中組態為具有內部下拉電阻的GPIO輸入。當按鈕被按下和釋放時,會產生兩個中斷。需要在interrupts屬性中設定IRQ_TYPE_EDGE_BOTH

&gpio {
    key_pin: key_pin {
        brcm,pins = <23>;
        brcm,function = <0>; /* 輸入 */
        brcm,pull = <1>; /* 下拉 */
    };
};

&soc {
    int_key_wait {
        compatible = "arrow,intkeywait";
        pinctrl-names = "default";
        pinctrl-0 = <&key_pin>;
        gpios = <&gpio 23 0>;
        interrupts = <23 IRQ_TYPE_EDGE_BOTH>;
        interrupt-parent = <&gpio>;
    };
};

內容解密:

  1. 裝置樹組態:組態GPIO23為輸入模式,並啟用內部下拉電阻,以檢測按鈕按下和釋放事件。
  2. IRQ_TYPE_EDGE_BOTH:設定中斷觸發型別為雙邊緣觸發,即按鈕按下和釋放都會觸發中斷。
  3. 裝置樹節點:定義了一個名為int_key_wait的裝置樹節點,用於描述中斷按鍵等待裝置的屬性。