【原創】Linux中斷子系統(二)-通用框架處理

背景

  • Read the fucking source code! –By 魯迅
  • A picture is worth a thousand words. –By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

【原創】Linux中斷子系統(一)-中斷控制器及驅動分析講到了底層硬件GIC驅動,以及Arch-Specific的中斷代碼,本文將研究下通用的中斷處理的過程,屬於硬件無關層。當然,我還是建議你看一下上篇文章。

這篇文章會解答兩個問題:

  1. 用戶是怎麼使用中斷的(中斷註冊)?
  2. 外設觸發中斷信號時,最終是怎麼調用到中斷handler的(中斷處理)?

2. 數據結構分析

先來看一下總的數據結構,核心是圍繞着struct irq_desc來展開:

  • Linux內核的中斷處理,圍繞着中斷描述符結構struct irq_desc展開,內核提供了兩種中斷描述符組織形式:

    1. 打開CONFIG_SPARSE_IRQ宏(中斷編號不連續),中斷描述符以radix-tree來組織,用戶在初始化時進行動態分配,然後再插入radix-tree中;
    2. 關閉CONFIG_SPARSE_IRQ宏(中斷編號連續),中斷描述符以數組的形式組織,並且已經分配好;
    3. 不管哪種形式,最終都可以通過linux irq號來找到對應的中斷描述符;
  • 圖的左側灰色部分,主要在中斷控制器驅動中進行初始化設置,包括各個結構中函數指針的指向等,其中struct irq_chip用於對中斷控制器的硬件操作,struct irq_domain與中斷控制器對應,完成的工作是硬件中斷號到Linux irq的映射;

  • 圖的上側灰色部分,中斷描述符的創建(這裏指CONFIG_SPARSE_IRQ),主要在獲取設備中斷信息的過程中完成的,從而讓設備樹中的中斷能與具體的中斷描述符irq_desc匹配;

  • 圖中剩餘部分,在設備申請註冊中斷的過程中進行設置,比如struct irqactionhandler的設置,這個用於指向我們設備驅動程序中的中斷處理函數了;

中斷的處理主要有以下幾個功能模塊:

  1. 硬件中斷號到Linux irq中斷號的映射,並創建好irq_desc中斷描述符;
  2. 中斷註冊時,先獲取設備的中斷號,根據中斷號找到對應的irq_desc,並將設備的中斷處理函數添加到irq_desc中;
  3. 設備觸發中斷信號時,根據硬件中斷號得到Linux irq中斷號,找到對應的irq_desc,最終調用到設備的中斷處理函數;

上述的描述比較簡單,更詳細的過程,往下看吧。

3. 流程分析

3.1 中斷註冊

這一次,讓我們以問題的方式來展開:
先來讓我們回答第一個問題:用戶是怎麼使用中斷的?

  1. 熟悉設備驅動的同學應該都清楚,經常會在驅動程序中調用request_irq()接口或者request_threaded_irq()接口來註冊設備的中斷處理函數;
  2. request_irq()/request_threaded_irq接口中,都需要用到irq,也就是中斷號,那麼這个中斷號是從哪裡來的呢?它是Linux irq,它又是如何映射到具體的硬件設備的中斷號的呢?

先來看第二個問題:設備硬件中斷號到Linux irq中斷號的映射

  • 硬件設備的中斷信息都在設備樹device tree中進行了描述,在系統啟動過程中,這些信息都已經加載到內存中並得到了解析;
  • 驅動中通常會使用platform_get_irqirq_of_parse_and_map接口,去根據設備樹的信息去創建映射關係(硬件中斷號到linux irq中斷號映射);
  • 【原創】Linux中斷子系統(一)-中斷控制器及驅動分析提到過struct irq_domain用於完成映射工作,因此在irq_create_fwspec_mapping接口中,會先去找到匹配的irq domain,再去回調該irq domain中的函數集,通常irq domain都是在中斷控制器驅動中初始化的,以ARM GICv2為例,最終回調到gic_irq_domain_hierarchy_ops中的函數;
  • 如果已經創建好了映射,那麼可以直接進行返回linux irq中斷號了,否則的話需要irq_domain_alloc_irqs來創建映射關係;
  • irq_domain_alloc_irqs完成兩個工作:
    1. 針對linux irq中斷號創建一個irq_desc中斷描述符;
    2. 調用domain->ops->alloc函數來完成映射,在ARM GICv2驅動中對應gic_irq_domain_alloc函數,這個函數很關鍵,所以下文介紹一下;

gic_irq_domain_alloc函數如下:

  • gic_irq_domain_translate:負責解析出設備樹中描述的中斷號和中斷觸發類型(邊緣觸發、電平觸發等);
  • gic_irq_domain_map:將硬件中斷號和linux中斷號綁定到一個結構中,也就完成了映射,此外還綁定了irq_desc結構中的其他字段,最重要的是設置了irq_desc->handle_irq的函數指針,這個最終是中斷響應時往上執行的入口,這個是關鍵,下文講述中斷處理過程時還會提到;
  • 根據硬件中斷號的範圍設置irq_desc->handle_irq的指針,共享中斷入口為handle_fasteoi_irq,私有中斷入口為handle_percpu_devid_irq

上述函數執行完成后,完成了兩大工作:

  1. 硬件中斷號與Linux中斷號完成映射,併為Linux中斷號創建了irq_desc中斷描述符;
  2. 數據結構的綁定及初始化,關鍵的地方是設置了中斷處理往上執行的入口;

再看第一個問題:中斷是怎麼來註冊的?

設備驅動中,獲取到了irq中斷號后,通常就會採用request_irq/request_threaded_irq來註冊中斷,其中request_irq用於註冊普通處理的中斷,request_threaded_irq用於註冊線程化處理的中斷;

在講具體的註冊流程前,先看一下主要的中斷標誌位:

#define IRQF_SHARED		0x00000080              //多個設備共享一个中斷號,需要外設硬件支持
#define IRQF_PROBE_SHARED	0x00000100              //中斷處理程序允許sharing mismatch發生
#define __IRQF_TIMER		0x00000200              //時鐘中斷
#define IRQF_PERCPU		0x00000400              //屬於特定CPU的中斷
#define IRQF_NOBALANCING	0x00000800              //禁止在CPU之間進行中斷均衡處理
#define IRQF_IRQPOLL		0x00001000              //中斷被用作輪訓
#define IRQF_ONESHOT		0x00002000              //一次性觸發的中斷,不能嵌套,1)在硬件中斷處理完成后才能打開中斷;2)在中斷線程化中保持關閉狀態,直到該中斷源上的所有thread_fn函數都執行完
#define IRQF_NO_SUSPEND		0x00004000              //系統休眠喚醒操作中,不關閉該中斷
#define IRQF_FORCE_RESUME	0x00008000              //系統喚醒過程中必須強制打開該中斷
#define IRQF_NO_THREAD		0x00010000              //禁止中斷線程化
#define IRQF_EARLY_RESUME	0x00020000              //系統喚醒過程中在syscore階段resume,而不用等到設備resume階段
#define IRQF_COND_SUSPEND	0x00040000              //與NO_SUSPEND的用戶共享中斷時,執行本設備的中斷處理函數

  • request_irq也是調用request_threaded_irq,只是在傳參的時候,線程處理函數thread_fn函數設置成NULL;
  • 由於在硬件中斷號和Linux中斷號完成映射后,irq_desc已經創建好,可以通過irq_to_desc接口去獲取對應的irq_desc
  • 創建irqaction,並初始化該結構體中的各個字段,其中包括傳入的中斷處理函數賦值給對應的字段;
  • __setup_irq用於完成中斷的相關設置,包括中斷線程化的處理:
    1. 中斷線程化用於減少系統關中斷的時間,增強系統的實時性;
    2. ARM64默認開啟了CONFIG_IRQ_FORCED_THREADING,引導參數傳入threadirqs時,則除了IRQF_NO_THREAD外的中斷,其他的都將強制線程化處理;
    3. 中斷線程化會為每个中斷都創建一個內核線程,如果中斷進行共享,對應irqaction將連接成鏈表,每個irqaction都有thread_mask位圖字段,當所有共享中斷都處理完成后才能unmask中斷,解除中斷屏蔽;

3.2 中斷處理

當完成中斷的註冊后,所有結構的組織關係都已經建立好,剩下的工作就是當信號來臨時,進行中斷的處理工作。

來回顧一下【原創】Linux中斷子系統(一)-中斷控制器及驅動分析中的Arch-specific處理流程:

  • 中斷收到之後,首先會跳轉到異常向量表的入口處,進而逐級進行回調處理,最終調用到generic_handle_irq來進行中斷處理。

generic_handle_irq處理如下圖:

  • generic_handle_irq函數最終會調用到desc->handle_irq(),這個也就是對應到上文中在建立映射關係的過程中,調用irq_domain_set_info函數,設置好了函數指針,也就是handle_fasteoi_irqhandle_percpu_devid_irq
  • handle_fasteoi_irq:處理共享中斷,並且遍歷irqaction鏈表,逐個調用action->handler()函數,這個函數正是設備驅動程序調用request_irq/request_threaded_irq接口註冊的中斷處理函數,此外如果中斷線程化處理的話,還會調用__irq_wake_thread()喚醒內核線程;
  • handle_percpu_devid_irq:處理per-CPU中斷處理,在這個過程中會分別調用中斷控制器的處理函數進行硬件操作,該函數調用action->handler()來進行中斷處理;

來看看中斷線程化處理后的喚醒流程吧__handle_irq_event_percpu->__irq_wake_thread

  • __handle_irq_event_percpu->__irq_wake_thread將喚醒irq_thread中斷內核線程;
  • irq_thread內核線程,將根據是否為強制中斷線程化對函數指針handler_fn進行初始化,以便後續進行調用;
  • irq_thread內核線程將while(!irq_wait_for_interrupt)循環進行中斷的處理,當滿足條件時,執行handler_fn,在該函數中最終調用action->thread_fn,也就是完成了中斷的處理;
  • irq_wait_for_interrupt函數,將會判斷中斷線程的喚醒條件,如果滿足了,則將當前任務設置成TASK_RUNNING狀態,並返回0,這樣就能執行中斷的處理,否則就調用schedule()進行調度,讓出CPU,並將任務設置成TASK_INTERRUPTIBLE可中斷睡眠狀態;

3.3 總結

中斷的處理,總體來說可以分為兩部分來看:

  1. 從上到下:圍繞irq_desc中斷描述符建立好連接關係,這個過程就包括:中斷源信息的解析(設備樹),硬件中斷號到Linux中斷號的映射關係、irq_desc結構的分配及初始化(內部各個結構的組織關係)、中斷的註冊(填充irq_desc結構,包括handler處理函數)等,總而言之,就是完成靜態關係創建,為中斷處理做好準備;
  2. 從下到上,當外設觸發中斷信號時,中斷控制器接收到信號併發送到處理器,此時處理器進行異常模式切換,並逐步從處理器架構相關代碼逐級回調。如果涉及到中斷線程化,則還需要進行中斷內核線程的喚醒操作,最終完成中斷處理函數的執行。

歡迎關注個人公眾號,不定期分享Linux內核機制文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

您可能也會喜歡…