linux驅動程序設計10 中斷與時鐘

本章主要講解Linux設備驅動編程中的中斷與定時器處理。由於中斷服務程序的執行並不存在於進程
上下文中,所以要求中斷服務程序的時間要儘量短。因此,Linux在中斷處理中引入了頂半部和底半部分
離的機制。另外,內核對時鐘的處理也採用中斷方式,而內核軟件定時器最終依賴於時鐘中斷。
10.1節講解中斷和定時器的概念及處理流程。
10.2節講解Linux中斷處理程序的架構,以及頂半部、底半部之間的關係。
10.3節講解Linux中斷編程的方法,涉及申請和釋放中斷、使能和屏蔽中斷以及中斷底半部tasklet、工
作隊列、軟中斷機制和threaded_irq。
10.4節講解多個設備共享同一個中斷號時的中斷處理過程。
10.5節和10.6節分別講解Linux設備驅動編程中定時器的編程以及內核延時的方法。
10.1 中斷與定時器
所謂中斷是指CPU在執行程序的過程中,出現了某些突發事件急待處理,CPU必須暫停當前程序的執
行,轉去處理突發事件,處理完畢後又返回原程序被中斷的位置繼續執行。
根據中斷的來源,中斷可分爲內部中斷和外部中斷,內部中斷的中斷源來自CPU內部(軟件中斷指
令、溢出、除法錯誤等,例如,操作系統從用戶態切換到內核態需藉助CPU內部的軟件中斷),外部中斷
的中斷源來自CPU外部,由外設提出請求。
根據中斷是否可以屏蔽,中斷可分爲可屏蔽中斷與不可屏蔽中斷(NMI),可屏蔽中斷可以通過設置
中斷控制器寄存器等方法被屏蔽,屏蔽後,該中斷不再得到響應,而不可屏蔽中斷不能被屏蔽。
根據中斷入口跳轉方法的不同,中斷可分爲向量中斷和非向量中斷。採用向量中斷的CPU通常爲不同
的中斷分配不同的中斷號,當檢測到某中斷號的中斷到來後,就自動跳轉到與該中斷號對應的地址執行。
不同中斷號的中斷有不同的入口地址。非向量中斷的多箇中斷共享一個入口地址,進入該入口地址後,再
通過軟件判斷中斷標誌來識別具體是哪個中斷。也就是說,向量中斷由硬件提供中斷服務程序入口地址,
非向量中斷由軟件提供中斷服務程序入口地址。
一個典型的非向量中斷服務程序如代碼清單10.1所示,它先判斷中斷源,然後調用不同中斷源的中斷
服務程序。
代碼清單10.1 非向量中斷服務程序的典型結構
1 irq_handler()
2 {
3 ...
4 int int_src = read_int_status(); /* 讀硬件的中斷相關寄存器 */
5 switch (int_src) { /* 判斷中斷源 */
6 case DEV_A:
7 dev_a_handler();
8 break;
9 case DEV_B:
10 dev_b_handler();
11 break;
12 ...
13 default:
14 break;
15 }
16 ...
17}
嵌入式系統以及x86PC中大多包含可編程中斷控制器(PIC),許多MCU內部就集成了PIC。如在
80386中,PIC是兩片i8259A芯片的級聯。通過讀寫PIC的寄存器,程序員可以屏蔽/使能某中斷及獲得中斷
狀態,前者一般通過中斷MASK寄存器完成,後者一般通過中斷PEND寄存器完成。
定時器在硬件上也依賴中斷來實現,圖10.1所示爲典型的嵌入式微處理器內可編程間隔定時器
(PIT)的工作原理,它接收一個時鐘輸入,當時鍾脈衝到來時,將目前計數值增1並與預先設置的計數值
(計數目標)比較,若相等,證明計數週期滿,併產生定時器中斷且復位目前計數值。
圖10.1 PIT定時器的工作原理
在ARM多核處理器裏最常用的中斷控制器是GIC(Generic Interrupt Controller),如圖10.2所示,它支
持3種類型的中斷。
圖10.2 ARM多核處理器裏的GIC
SGI(Software Generated Interrupt):軟件產生的中斷,可以用於多核的核間通信,一個CPU可以通過
寫GIC的寄存器給另外一個CPU產生中斷。多核調度用的IPI_WAKEUP、IPI_TIMER、
IPI_RESCHEDULE、IPI_CALL_FUNC、IPI_CALL_FUNC_SINGLE、IPI_CPU_STOP、IPI_IRQ_WORK、
IPI_COMPLETION都是由SGI產生的。
PPI(Private Peripheral Interrupt):某個CPU私有外設的中斷,這類外設的中斷只能發給綁定的那個
CPU。
SPI(Shared Peripheral Interrupt):共享外設的中斷,這類外設的中斷可以路由到任何一個CPU。
對於SPI類型的中斷,內核可以通過如下API設定中斷觸發的CPU核:
extern int irq_set_affinity (unsigned int irq, const struct cpumask *m);
在ARM Linux默認情況下,中斷都是在CPU0上產生的,比如,我們可以通過如下代碼把中斷irq設定
到CPU i上去:
irq_set_affinity(irq, cpumask_of(i));

10.2 Linux中斷處理程序架構
設備的中斷會打斷內核進程中的正常調度和運行,系統對更高吞吐率的追求勢必要求中斷服務程序盡
量短小精悍。但是,這個良好的願望往往與現實並不吻合。在大多數真實的系統中,當中斷到來時,要完
成的工作往往並不會是短小的,它可能要進行較大量的耗時處理。
圖10.3描述了Linux內核的中斷處理機制。爲了在中斷執行時間儘量短和中斷處理需完成的工作儘量
大之間找到一個平衡點,Linux將中斷處理程序分解爲兩個半部:頂半部(Top Half)和底半部(Bottom
Half)。
圖10.3 Linux中斷處理機制
頂半部用於完成儘量少的比較緊急的功能,它往往只是簡單地讀取寄存器中的中斷狀態,並在清除中
斷標誌後就進行“登記中斷”的工作。“登記中斷”意味着將底半部處理程序掛到該設備的底半部執行隊列中
去。這樣,頂半部執行的速度就會很快,從而可以服務更多的中斷請求。
現在,中斷處理工作的重心就落在了底半部的頭上,需用它來完成中斷事件的絕大多數任務。底半部
幾乎做了中斷處理程序所有的事情,而且可以被新的中斷打斷,這也是底半部和頂半部的最大不同,因爲
頂半部往往被設計成不可中斷。底半部相對來說並不是非常緊急的,而且相對比較耗時,不在硬件中斷服
務程序中執行。
儘管頂半部、底半部的結合能夠改善系統的響應能力,但是,僵化地認爲Linux設備驅動中的中斷處
理一定要分兩個半部則是不對的。如果中斷要處理的工作本身很少,則完全可以直接在頂半部全部完成。
其他操作系統中對中斷的處理也採用了類似於Linux的方法,真正的硬件中斷服務程序都應該
儘量短。因此,許多操作系統都提供了中斷上下文和非中斷上下文相結合的機制,將中斷的耗時工作保留
到非中斷上下文去執行。例如,在VxWorks中,網絡設備包接收中斷到來後,中斷服務程序會通過
netJobAdd()函數將耗時的包接收和上傳工作交給tNetTask任務去執行。
在Linux中,查看/proc/interrupts文件可以獲得系統中中斷的統計信息,並能統計出每一箇中斷號上的
中斷在每個CPU上發生的次數,具體如圖10.4所示。
圖10.4 Linux中的中斷統計信息
10.3 Linux中斷編程
10.3.1 申請和釋放中斷
在Linux設備驅動中,使用中斷的設備需要申請和釋放對應的中斷,並分別使用內核提供的
request_irq()和free_irq()函數。
1.申請irq
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
irq是要申請的硬件中斷號。
handler是向系統登記的中斷處理函數(頂半部),是一個回調函數,中斷髮生時,系統調用這個函
數,dev參數將被傳遞給它。
irqflags是中斷處理的屬性,可以指定中斷的觸發方式以及處理方式。在觸發方式方面,可以是
IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW
等。在處理方式方面,若設置了IRQF_SHARED,則表示多個設備共享中斷,dev是要傳遞給中斷服務程
序的私有數據,一般設置爲這個設備的設備結構體或者NULL。
request_irq()返回0表示成功,返回-EINVAL表示中斷號無效或處理函數指針爲NULL,返回-
EBUSY表示中斷已經被佔用且不能共享。
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
此函數與request_irq()的區別是devm_開頭的API申請的是內核“managed”的資源,一般不需要在出
錯處理和remove()接口裏再顯式的釋放。有點類似Java的垃圾回收機制。比如,對於at86rf230驅動,如
下的補丁中改用devm_request_irq()後就刪除了free_irq(),該補丁對應的內核commit ID是652355c5。
--- a/drivers/net/ieee802154/at86rf230.c
+++ b/drivers/net/ieee802154/at86rf230.c
@@ -1190,24+1190,22@@ static int at86rf230_probe(struct spi_device *spi)
if (rc)
goto err_hw_init;
- rc = request_irq(spi->irq, irq_handler, IRQF_SHARED,
- dev_name(&spi->dev), lp);
+ rc = devm_request_irq(&spi->dev, spi->irq, irq_handler, IRQF_SHARED,
+ dev_name(&spi->dev), lp);
if (rc)
goto err_hw_init;
/* Read irq status register to reset irq line */
rc = at86rf230_read_subreg(lp, RG_IRQ_STATUS, 0xff, 0, &status);
if (rc)
- goto err_irq;
+ goto err_hw_init;
rc = ieee802154_register_device(lp->dev);
if (rc)
- goto err_irq;
+ goto err_hw_init;
return rc;
-err_irq:
- free_irq(spi->irq, lp);
err_hw_init:
flush_work(&lp->irqwork);
spi_set_drvdata(spi, NULL);
@@ -1232,7+1230,6@@ static int at86rf230_remove(struct spi_device *spi)
at86rf230_write_subreg(lp, SR_IRQ_MASK, 0);
ieee802154_unregister_device(lp->dev);
- free_irq(spi->irq, lp);
flush_work(&lp->irqwork);
if (gpio_is_valid(pdata->slp_tr))
頂半部handler的類型irq_handler_t定義爲:
typedef irqreturn_t (*irq_handler_t)(int, void *);
typedef int irqreturn_t;
2.釋放irq
與request_irq()相對應的函數爲free_irq(),free_irq()的原型爲:
void free_irq(unsigned int irq,void *dev_id);
free_irq()中參數的定義與request_irq()相同。
10.3.2 使能和屏蔽中斷
下列3個函數用於屏蔽一箇中斷源:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
disable_irq_nosync()與disable_irq()的區別在於前者立即返回,而後者等待目前的中斷處理完
成。由於disable_irq()會等待指定的中斷被處理完,因此如果在n號中斷的頂半部調用disable_irq(n),
會引起系統的死鎖,這種情況下,只能調用disable_irq_nosync(n)。
下列兩個函數(或宏,具體實現依賴於CPU的體系結構)將屏蔽本CPU內的所有中斷:
#define local_irq_save(flags) ...
void local_irq_disable(void);
前者會將目前的中斷狀態保留在flags中(注意flags爲unsigned long類型,被直接傳遞,而不是通過指
針),後者直接禁止中斷而不保存狀態。
與上述兩個禁止中斷對應的恢復中斷的函數(或宏)是:
#define local_irq_restore(flags) ...
void local_irq_enable(void);
以上各以local_開頭的方法的作用範圍是本CPU內。
10.3.3 底半部機制
Linux實現底半部的機制主要有tasklet、工作隊列、軟中斷和線程化irq。
1.tasklet
tasklet的使用較簡單,它的執行上下文是軟中斷,執行時機通常是頂半部返回的時候。我們只需要定
義tasklet及其處理函數,並將兩者關聯則可,例如:
void my_tasklet_func(unsigned long); /*定義一個處理函數*/
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/*定義一個tasklet結構my_tasklet,與my_tasklet_func(data)函數相關聯*/
代碼DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)實現了定義名稱爲my_tasklet的
tasklet,並將其與my_tasklet_func()這個函數綁定,而傳入這個函數的參數爲data。
在需要調度tasklet的時候引用一個tasklet_schedule()函數就能使系統在適當的時候進行調度運行:
tasklet_schedule(&my_tasklet);
使用tasklet作爲底半部處理中斷的設備驅動程序模板如代碼清單10.2所示(僅包含與中斷相關的部
分)。
代碼清單10.2 tasklet使用模板
1/* 定義tasklet和底半部函數並將它們關聯 */
2void xxx_do_tasklet(unsigned long);
3DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
4
5/* 中斷處理底半部 */
6void xxx_do_tasklet(unsigned long)
7{
8 ...
9}
10
11/* 中斷處理頂半部 */
12irqreturn_t xxx_interrupt(int irq, void *dev_id)
13{
14 ...
15 tasklet_schedule(&xxx_tasklet);
16 ...
17}
18
19/* 設備驅動模塊加載函數 */
20int __init xxx_init(void)
21{
22 ...
23 /* 申請中斷 */
24 result = request_irq(xxx_irq, xxx_interrupt,
25 0, "xxx", NULL);
26 ...
27 return IRQ_HANDLED;
28}
29
30/* 設備驅動模塊卸載函數 */
31void __exit xxx_exit(void)
32{
33 ...
34 /* 釋放中斷 */
35 free_irq(xxx_irq, xxx_interrupt);
36 ...
37}
上述程序在模塊加載函數中申請中斷(第24~25行),並在模塊卸載函數中釋放它(第35行)。對應
於xxx_irq的中斷處理程序被設置爲xxx_interrupt()函數,在這個函數中,第15行的
tasklet_schedule(&xxx_tasklet)調度被定義的tasklet函數xxx_do_tasklet()在適當的時候執行。
2.工作隊列
工作隊列的使用方法和tasklet非常相似,但是工作隊列的執行上下文是內核線程,因此可以調度和睡
眠。下面的代碼用於定義一個工作隊列和一個底半部執行函數:
struct work_struct my_wq; /* 定義一個工作隊列 */
void my_wq_func(struct work_struct *work); /* 定義一個處理函數 */
通過INIT_WORK()可以初始化這個工作隊列並將工作隊列與處理函數綁定:
INIT_WORK(&my_wq, my_wq_func);
/* 初始化工作隊列並將其與處理函數綁定 */
與tasklet_schedule()對應的用於調度工作隊列執行的函數爲schedule_work(),如:
schedule_work(&my_wq); /* 調度工作隊列執行 */
與代碼清單10.2對應的使用工作隊列處理中斷底半部的設備驅動程序模板如代碼清單10.3所示(僅包
含與中斷相關的部分)。
代碼清單10.3 工作隊列使用模板
1/* 定義工作隊列和關聯函數 */
2struct work_struct xxx_wq;
3void xxx_do_work(struct work_struct *work);
4
5/* 中斷處理底半部 */
6void xxx_do_work(struct work_struct *work)
7{
8 ...
9}
10
11/*中斷處理頂半部*/
12irqreturn_t xxx_interrupt(int irq, void *dev_id)
13{
14 ...
15 schedule_work(&xxx_wq);
16 ...
17 return IRQ_HANDLED;
18}
19
20/* 設備驅動模塊加載函數 */
21int xxx_init(void)
22{
23 ...
24 /* 申請中斷 */
25 result = request_irq(xxx_irq, xxx_interrupt,
26 0, "xxx", NULL);
27 ...
28 /* 初始化工作隊列 */
29 INIT_WORK(&xxx_wq, xxx_do_work);
30 ...
31}
32
33/* 設備驅動模塊卸載函數 */
34void xxx_exit(void)
35{
36 ...
37 /* 釋放中斷 */
38 free_irq(xxx_irq, xxx_interrupt);
39 ...
40}
與代碼清單10.2不同的是,上述程序在設計驅動模塊加載函數中增加了初始化工作隊列的代碼(第29
行)。
工作隊列早期的實現是在每個CPU核上創建一個worker內核線程,所有在這個核上調度的工作都在該
worker線程中執行,其併發性顯然差強人意。在Linux 2.6.36以後,轉而實現了“Concurrency-managed
workqueues”,簡稱cmwq,cmwq會自動維護工作隊列的線程池以提高併發性,同時保持了API的向後兼
容。
3.軟中斷
軟中斷(Softirq)也是一種傳統的底半部處理機制,它的執行時機通常是頂半部返回的時候,tasklet
是基於軟中斷實現的,因此也運行於軟中斷上下文。
在Linux內核中,用softirq_action結構體表徵一個軟中斷,這個結構體包含軟中斷處理函數指針和傳遞
給該函數的參數。使用open_softirq()函數可以註冊軟中斷對應的處理函數,而raise_softirq()函數可以
觸發一個軟中斷。
軟中斷和tasklet運行於軟中斷上下文,仍然屬於原子上下文的一種,而工作隊列則運行於進程上下
文。因此,在軟中斷和tasklet處理函數中不允許睡眠,而在工作隊列處理函數中允許睡眠。
local_bh_disable()和local_bh_enable()是內核中用於禁止和使能軟中斷及tasklet底半部機制的函
數。
內核中採用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、
NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般來說,驅動的編寫者不會也不宜直
接使用softirq。
第9章異步通知所基於的信號也類似於中斷,現在,總結一下硬中斷、軟中斷和信號的區別:硬中斷
是外部設備對CPU的中斷,軟中斷是中斷底半部的一種處理機制,而信號則是由內核(或其他進程)對某
個進程的中斷。在涉及系統調用的場合,人們也常說通過軟中斷(例如ARM爲swi)陷入內核,此時軟中
斷的概念是指由軟件指令引發的中斷,和我們這個地方說的softirq是兩個完全不同的概念,一個是
software,一個是soft。
需要特別說明的是,軟中斷以及基於軟中斷的tasklet如果在某段時間內大量出現的話,內核會把後續
軟中斷放入ksoftirqd內核線程中執行。總的來說,中斷優先級高於軟中斷,軟中斷又高於任何一個線程。
軟中斷適度線程化,可以緩解高負載情況下系統的響應。
4.threaded_irq
在內核中,除了可以通過request_irq()、devm_request_irq()申請中斷以外,還可以通過
request_threaded_irq()和devm_request_threaded_irq()申請。這兩個函數的原型爲:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
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);
由此可見,它們比request_irq()、devm_request_irq()多了一個參數thread_fn。用這兩個API申請中
斷的時候,內核會爲相應的中斷號分配一個對應的內核線程。注意這個線程只針對這個中斷號,如果其他
中斷也通過request_threaded_irq()申請,自然會得到新的內核線程。
參數handler對應的函數執行於中斷上下文,thread_fn參數對應的函數則執行於內核線程。如果handler
結束的時候,返回值是IRQ_WAKE_THREAD,內核會調度對應線程執行thread_fn對應的函數。
request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中設置IRQF_ONESHOT標記,
這樣內核會自動幫助我們在中斷上下文中屏蔽對應的中斷號,而在內核調度thread_fn執行後,重新使能該
中斷號。對於我們無法在上半部清除中斷的情況,IRQF_ONESHOT特別有用,避免了中斷服務程序一退
出,中斷就洪泛的情況。
handler參數可以設置爲NULL,這種情況下,內核會用默認的irq_default_primary_handler()代替
handler,並會使用IRQF_ONESHOT標記。irq_default_primary_handler()定義爲:
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
10.3.4 實例:GPIO按鍵的中斷
drivers/input/keyboard/gpio_keys.c是一個放之四海皆準的GPIO按鍵驅動,爲了讓該驅動在特定的電路
板上工作,通常只需要修改arch/arm/mach-xxx下的板文件或者修改device tree對應的dts。該驅動會爲每個
GPIO申請中斷,在gpio_keys_setup_key()函數中進行。注意最後一個參數bdata,會被傳入中斷服務程
序。
代碼清單10.4 GPIO按鍵驅動中斷申請
1static int gpio_keys_setup_key(struct platform_device *pdev,
2 struct input_dev *input,
3 struct gpio_button_data *bdata,
4 const struct gpio_keys_button *button)
5{
6 ...
7
8 error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);
9 if (error < 0) {
10 dev_err(dev, "Unable to claim irq %d; error %d\n",
11 bdata->irq, error);
12 goto fail;
13 }
14 ...
15}
第8行的request_any_context_irq()會根據GPIO控制器本身的“上級”中斷是否爲threaded_irq來決定採
用request_irq()還是request_threaded_irq()。一組GPIO(如32個GPIO)雖然每個都提供一箇中斷,並
且都有中斷號,但是在硬件上一組GPIO通常是嵌套在上一級的中斷控制器上的一箇中斷。
request_any_context_irq()也有一個變體是devm_request_any_context_irq()。
在GPIO按鍵驅動的remove_key()函數中,會釋放GPIO對應的中斷,如代碼清單10.5所示。
代碼清單10.5 GPIO按鍵驅動中斷釋放
1static void gpio_remove_key(struct gpio_button_data *bdata)
2{
3 free_irq(bdata->irq, bdata);
4 if (bdata->timer_debounce)
5 del_timer_sync(&bdata->timer);
6 cancel_work_sync(&bdata->work);
7 if (gpio_is_valid(bdata->button->gpio))
8 gpio_free(bdata->button->gpio);
GPIO按鍵驅動的中斷處理比較簡單,沒有明確地分爲上下兩個半部,而只存在頂半部,如代碼清單
10.6所示。
代碼清單10.6 GPIO按鍵驅動中斷處理程序
1static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
2{
3 struct gpio_button_data *bdata = dev_id;
4
5 BUG_ON(irq != bdata->irq);
6
7 if (bdata->button->wakeup)
8 pm_stay_awake(bdata->input->dev.parent);
9 if (bdata->timer_debounce)
10 mod_timer(&bdata->timer,
11 jiffies + msecs_to_jiffies(bdata->timer_debounce));
12 else
13 schedule_work(&bdata->work);
14
15 return IRQ_HANDLED;
16}
第3行直接從dev_id取出了bdata,這就是對應的那個GPIO鍵的數據結構,之後根據情況啓動timer以進
行debounce或者直接調度工作隊列去彙報按鍵事件。在GPIO按鍵驅動初始化的時候,通過
INIT_WORK(&bdata->work,gpio_keys_gpio_work_func)初始化了bdata->work,對應的處理函數是
gpio_keys_gpio_work_func(),如代碼清單10.7所示。
代碼清單10.7 GPIO按鍵驅動的工作隊列底半部
1static void gpio_keys_gpio_work_func(struct work_struct *work)
2{
3 struct gpio_button_data *bdata =
4 container_of(work, struct gpio_button_data, work);
5
6 gpio_keys_gpio_report_event(bdata);
7
8 if (bdata->button->wakeup)
9 pm_relax(bdata->input->dev.parent);
10}
觀察其中的第3~4行,它通過container_of()再次從work_struct反向解析出了bdata。原因是
work_struct本身在定義時,就嵌入在gpio_button_data結構體內。讀者朋友們應該掌握Linux的這種可以到
處獲取一個結構體指針的技巧,它實際上類似於面向對象裏面的“this”指針。
struct gpio_button_data {
const struct gpio_keys_button *button;
struct input_dev *input;
struct timer_list timer;
struct work_struct work;
unsigned int timer_debounce; /* in msecs */
unsigned int irq;
spinlock_t lock;
bool disabled;
bool key_pressed;
};
10.4 中斷共享
多個設備共享一根硬件中斷線的情況在實際的硬件系統中廣泛存在,Linux支持這種中斷共享。下面
是中斷共享的使用方法。
1)共享中斷的多個設備在申請中斷時,都應該使用IRQF_SHARED標誌,而且一個設備以
IRQF_SHARED申請某中斷成功的前提是該中斷未被申請,或該中斷雖然被申請了,但是之前申請該中斷
的所有設備也都以IRQF_SHARED標誌申請該中斷。
2)儘管內核模塊可訪問的全局地址都可以作爲request_irq(…,void*dev_id)的最後一個參數
dev_id,但是設備結構體指針顯然是可傳入的最佳參數。
3)在中斷到來時,會遍歷執行共享此中斷的所有中斷處理程序,直到某一個函數返回
IRQ_HANDLED。在中斷處理程序頂半部中,應根據硬件寄存器中的信息比照傳入的dev_id參數迅速地判
斷是否爲本設備的中斷,若不是,應迅速返回IRQ_NONE,如圖10.5所示。
圖10.5 共享中斷的處理
代碼清單10.8給出了使用共享中斷的設備驅動程序的模板(僅包含與共享中斷機制相關的部分)。
代碼清單10.8 共享中斷編程模板
1/* 中斷處理頂半部 */
2irqreturn_t xxx_interrupt(int irq, void *dev_id)
3{
4 ...
5 int status = read_int_status(); /* 獲知中斷源 */
6 if(!is_myint(dev_id,status)) /* 判斷是否爲本設備中斷 */
7 return IRQ_NONE; /* 不是本設備中斷,立即返回 */
8
9 /* 是本設備中斷,進行處理 */
10 ...
11 return IRQ_HANDLED; /* 返回IRQ_HANDLED表明中斷已被處理 */
12}
13
14/* 設備驅動模塊加載函數 */
15int xxx_init(void)
16{
17 ...
18 /* 申請共享中斷 */
19 result = request_irq(sh_irq, xxx_interrupt,
20 IRQF_SHARED, "xxx", xxx_dev);
21 ...
22}
23
24/* 設備驅動模塊卸載函數 */
25void xxx_exit(void)
26{
27 ...
28 /* 釋放中斷 */
29 free_irq(xxx_irq, xxx_interrupt);
30 ...
31}
10.5 內核定時器
10.5.1 內核定時器編程
軟件意義上的定時器最終依賴硬件定時器來實現,內核在時鐘中斷髮生後檢測各定時器是否到期,到
期後的定時器處理函數將作爲軟中斷在底半部執行。實質上,時鐘中斷處理程序會喚起TIMER_SOFTIRQ
軟中斷,運行當前處理器上到期的所有定時器。
在Linux設備驅動編程中,可以利用Linux內核中提供的一組函數和數據結構來完成定時觸發工作或者
完成某週期性的事務。這組函數和數據結構使得驅動工程師在多數情況下不用關心具體的軟件定時器究竟
對應着怎樣的內核和硬件行爲。
Linux內核所提供的用於操作定時器的數據結構和函數如下。
1.timer_list
在Linux內核中,timer_list結構體的一個實例對應一個定時器,如代碼清單10.9所示。
代碼清單10.9 timer_list結構體
1struct timer_list {
2 /*
3 * All fields that change during normal runtime grouped to the
4 * same cacheline
5 */
6 struct list_head entry;
7 unsigned long expires;
8 struct tvec_base *base;
9
10 void (*function)(unsigned long);
11 unsigned long data;
12
13 int slack;
14
15#ifdef CONFIG_TIMER_STATS
16 int start_pid;
17 void *start_site;
18 char start_comm[16];
19#endif
20#ifdef CONFIG_LOCKDEP
21 struct lockdep_map lockdep_map;
22#endif
23};
當定時器期滿後,其中第10行的function()成員將被執行,而第11行的data成員則是傳入其中的參
數,第7行的expires則是定時器到期的時間(jiffies)。
如下代碼定義一個名爲my_timer的定時器:
struct timer_list my_timer;
2.初始化定時器
init_timer是一個宏,它的原型等價於:
void init_timer(struct timer_list * timer);
上述init_timer()函數初始化timer_list的entry的next爲NULL,並給base指針賦值。
TIMER_INITIALIZER(_function,_expires,_data)宏用於賦值定時器結構體的function、expires、
data和base成員,這個宏等價於:
#define TIMER_INITIALIZER(_function, _expires, _data) { \
.entry = { .prev = TIMER_ENTRY_STATIC }, \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.base = &boot_tvec_bases, \
}
DEFINE_TIMER(_name,_function,_expires,_data)宏是定義並初始化定時器成員的“快捷方式”,
這個宏定義爲:
#define DEFINE_TIMER(_name, _function, _expires, _data)\
struct timer_list _name =\
TIMER_INITIALIZER(_function, _expires, _data)
此外,setup_timer()也可用於初始化定時器並賦值其成員,其源代碼爲:
#define __setup_timer(_timer, _fn, _data, _flags) \
do { \
__init_timer((_timer), (_flags)); \
(_timer)->function = (_fn); \
(_timer)->data = (_data); \
} while (0)
3.增加定時器
void add_timer(struct timer_list * timer);
上述函數用於註冊內核定時器,將定時器加入到內核動態定時器鏈表中。
4.刪除定時器
int del_timer(struct timer_list * timer);
上述函數用於刪除定時器。
del_timer_sync()是del_timer()的同步版,在刪除一個定時器時需等待其被處理完,因此該函數的
調用不能發生在中斷上下文中。
5.修改定時器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函數用於修改定時器的到期時間,在新的被傳入的expires到來後纔會執行定時器函數。
代碼清單10.10給出了一個完整的內核定時器使用模板,在大多數情況下,設備驅動都如這個模板那
樣使用定時器。
代碼清單10.10 內核定時器使用模板
1/* xxx設備結構體 */
2struct xxx_dev {
3 struct cdev cdev;
4 ...
5 timer_list xxx_timer; /* 設備要使用的定時器 */
6};
7
8/* xxx驅動中的某函數 */
9xxx_func1(…)
10{
11 struct xxx_dev *dev = filp->private_data;
12 ...
13 /* 初始化定時器 */
14 init_timer(&dev->xxx_timer);
15 dev->xxx_timer.function = &xxx_do_timer;
16 dev->xxx_timer.data = (unsigned long)dev;
17 /* 設備結構體指針作爲定時器處理函數參數 */
18 dev->xxx_timer.expires = jiffies + delay;
19 /* 添加(註冊)定時器 */
20 add_timer(&dev->xxx_timer);
21 ...
22}
23
24/* xxx驅動中的某函數 */
25xxx_func2(…)
26{
27 ...
28 /* 刪除定時器 */
29 del_timer (&dev->xxx_timer);
30 ...
31}
32
33/* 定時器處理函數 */
34static void xxx_do_timer(unsigned long arg)
35{
36 struct xxx_device *dev = (struct xxx_device *)(arg);
37 ...
38 /* 調度定時器再執行 */
39 dev->xxx_timer.expires = jiffies + delay;
40 add_timer(&dev->xxx_timer);
41 ...
42}
從代碼清單第18、39行可以看出,定時器的到期時間往往是在目前jiffies的基礎上添加一個時延,若
爲Hz,則表示延遲1s。
在定時器處理函數中,在完成相應的工作後,往往會延後expires並將定時器再次添加到內核定時器鏈
表中,以便定時器能再次被觸發。
此外,Linux內核支持tickless和NO_HZ模式後,內核也包含對hrtimer(高精度定時器)的支持,它可
以支持到微秒級別的精度。內核也定義了hrtimer結構體,hrtimer_set_expires()、
hrtimer_start_expires()、hrtimer_forward_now()、hrtimer_restart()等類似的API來完成hrtimer的設
置、時間推移以及到期回調。我們可以從sound/soc/fsl/imx-pcm-fiq.c中提取出一個使用範例,如代碼清單
10.11所示。
代碼清單10.11 內核高精度定時器(hrtimer)使用模板
1static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)
2{
3 ...
4
5 hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns));
6
7 return HRTIMER_RESTART;
8}
9
10static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
11{
12 struct snd_pcm_runtime *runtime = substream->runtime;
13 struct imx_pcm_runtime_data *iprtd = runtime->private_data;
14
15 switch (cmd) {
16 case SNDRV_PCM_TRIGGER_START:
17 case SNDRV_PCM_TRIGGER_RESUME:
18 case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
19 ...
20 hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns),
21 HRTIMER_MODE_REL);
22 ...
23}
24
25static int snd_imx_open(struct snd_pcm_substream *substream)
26{
27 ...
28 hrtimer_init(&iprtd->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
29 iprtd->hrt.function = snd_hrtimer_callback;
30
31 ...
32 return 0;
33}
34static int snd_imx_close(struct snd_pcm_substream *substream)
35{
36 ...
37 hrtimer_cancel(&iprtd->hrt);
38 ...
39}
第28~29行在聲卡打開的時候通過hrtimer_init()初始化了hrtimer,並指定回調函數爲
snd_hrtimer_callback();在啓動播放(第15~21行SNDRV_PCM_TRIGGER_START)等時刻通過
hrtimer_start()啓動了hrtimer;iprtd->poll_time_ns納秒後,時間到snd_hrtimer_callback()函數在中斷上
下文被執行,它緊接着又通過hrtimer_forward_now()把hrtimer的時間前移了iprtd->poll_time_ns納秒,這
樣周而復始;直到聲卡被關閉,第37行又調用了hrtimer_cancel()取消在open時初始化的hrtimer。
10.5.2 內核中延遲的工作delayed_work
對於週期性的任務,除了定時器以外,在Linux內核中還可以利用一套封裝得很好的快捷機制,其本
質是利用工作隊列和定時器實現,這套快捷機制就是delayed_work,delayed_work結構體的定義如代碼清單
10.12所示。
代碼清單10.12 delayed_work結構體
1struct delayed_work {
2 struct work_struct work;
3 struct timer_list timer;
45
/* target workqueue and CPU ->timer uses to queue ->work */
6 struct workqueue_struct *wq;
7 int cpu;
8};
我們可以通過如下函數調度一個delayed_work在指定的延時後執行:
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
當指定的delay到來時,delayed_work結構體中的work成員work_func_t類型成員func()會被執行。
work_func_t類型定義爲:
typedef void (*work_func_t)(struct work_struct *work);
其中,delay參數的單位是jiffies,因此一種常見的用法如下:
schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));
msecs_to_jiffies()用於將毫秒轉化爲jiffies。
如果要週期性地執行任務,通常會在delayed_work的工作函數中再次調用schedule_delayed_work(),
周而復始。
如下函數用來取消delayed_work:
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
10.5.3 實例:秒字符設備
下面我們編寫一個字符設備“second”(即“秒”)的驅動,它在被打開的時候初始化一個定時器並將其
添加到內核定時器鏈表中,每秒輸出一次當前的jiffies(爲此,定時器處理函數中每次都要修改新的
expires),整個程序如代碼清單10.13所示。
代碼清單10.13 使用內核定時器的second字符設備驅動
1#include <linux/module.h>
2#include <linux/fs.h>
3#include <linux/mm.h>
4#include <linux/init.h>
5#include <linux/cdev.h>
6#include <linux/slab.h>
7#include <linux/uaccess.h>
8
9#define SECOND_MAJOR 248
10
11static int second_major = SECOND_MAJOR;
12module_param(second_major, int, S_IRUGO);
13
14struct second_dev {
15 struct cdev cdev;
16 atomic_t counter;
17 struct timer_list s_timer;
18};
19
20static struct second_dev *second_devp;
21
22static void second_timer_handler(unsigned long arg)
23{
24 mod_timer(&second_devp->s_timer, jiffies + HZ); /* 觸發下一次定時 */
25 atomic_inc(&second_devp->counter); /* 增加秒計數 */
26
27 printk(KERN_INFO "current jiffies is %ld\n", jiffies);
28}
29
30static int second_open(struct inode *inode, struct file *filp)
31{
32 init_timer(&second_devp->s_timer);
33 second_devp->s_timer.function = &second_timer_handler;
34 second_devp->s_timer.expires = jiffies + HZ;
35
36 add_timer(&second_devp->s_timer);
37
38 atomic_set(&second_devp->counter, 0); /* 初始化秒計數爲0 */
39
40 return 0;
41}
42
43static int second_release(struct inode *inode, struct file *filp)
44{
45 del_timer(&second_devp->s_timer);
46
47 return 0;
48}
49
50static ssize_t second_read(struct file *filp, char __user * buf, size_t count,
51 loff_t * ppos)
52{
53 int counter;
54
55 counter = atomic_read(&second_devp->counter);
56 if (put_user(counter, (int *)buf))/* 複製counter到userspace */
57 return -EFAULT;
58 else
59 return sizeof(unsigned int);
60}
61
62static const struct file_operations second_fops = {
63 .owner = THIS_MODULE,
64 .open = second_open,
65 .release = second_release,
66 .read = second_read,
67};
68
69static void second_setup_cdev(struct second_dev *dev, int index)
70{
71 int err, devno = MKDEV(second_major, index);
72
73 cdev_init(&dev->cdev, &second_fops);
74 dev->cdev.owner = THIS_MODULE;
75 err = cdev_add(&dev->cdev, devno, 1);
76 if (err)
77 printk(KERN_ERR "Failed to add second device\n");
78}
79
80static int __init second_init(void)
81{
82 int ret;
83 dev_t devno = MKDEV(second_major, 0);
84
85 if (second_major)
86 ret = register_chrdev_region(devno, 1, "second");
87 else {
88 ret = alloc_chrdev_region(&devno, 0, 1, "second");
89 second_major = MAJOR(devno);
90 }
91 if (ret < 0)
92 return ret;
93
94 second_devp = kzalloc(sizeof(*second_devp), GFP_KERNEL);
95 if (!second_devp) {
96 ret = -ENOMEM;
97 goto fail_malloc;
98 }
99
100 second_setup_cdev(second_devp, 0);
101
102 return 0;
103
104fail_malloc:
105 unregister_chrdev_region(devno, 1);
106 return ret;
107}
108module_init(second_init);
109
110static void __exit second_exit(void)
111{
112 cdev_del(&second_devp->cdev);
113 kfree(second_devp);
114 unregister_chrdev_region(MKDEV(second_major, 0), 1);
115}
116module_exit(second_exit);
117
118MODULE_AUTHOR("Barry Song <[email protected]>");
119MODULE_LICENSE("GPL v2");
在second的open()函數中,將啓動定時器,此後每1s會再次運行定時器處理函數,在second的
release()函數中,定時器被刪除。
second_dev結構體中的原子變量counter用於秒計數,每次在定時器處理函數中調用的atomic_inc()會
令其原子性地增1,second的read()函數會將這個值返回給用戶空間。
本書配套的Ubuntu中/home/baohua/develop/training/kernel/drivers/second/包含了second設備驅動以及
second_test.c用戶空間測試程序,運行make命令編譯得到second.ko和second_test,加載second.ko內核模塊並
創建/dev/second設備文件節點:
# mknod /dev/second c 248 0
代碼清單10.14給出了second_test.c這個應用程序,它打開/dev/second,其後不斷地讀取自/dev/second設
備文件打開以後經歷的秒數。
代碼清單10.14 second設備用戶空間測試程序
1#include ...
2
3main()
4{
5 int fd;
6 int counter = 0;
7 int old_counter = 0;
8
9 /* 打開/dev/second設備文件 */
10 fd = open("/dev/second", O_RDONLY);
11 if (fd != - 1) {
13 while (1) {
15 read(fd,&counter, sizeof(unsigned int));/* 讀目前經歷的秒數 */
16 if(counter!=old_counter) {
18 printf("seconds after open /dev/second :%d\n",counter);
19 old_counter = counter;
20 }
21 }
22 } else {
25 printf("Device open failure\n");
26 }
27}
運行second_test後,內核將不斷地輸出目前的jiffies值:
[13935.122093] current jiffies is 13635122
[13936.124441] current jiffies is 13636124
[13937.126078] current jiffies is 13637126
[13952.832648] current jiffies is 13652832
[13953.834078] current jiffies is 13653834
[13954.836090] current jiffies is 13654836
[13955.838389] current jiffies is 13655838
[13956.840453] current jiffies is 13656840
...
從上述內核的打印消息也可以看出,本書配套Ubuntu上的每秒jiffies大概走1000次。而應用程序將不
斷輸出自/dec/second打開以後經歷的秒數:
# ./second_test
seconds after open /dev/second :1
seconds after open /dev/second :2
seconds after open /dev/second :3
seconds after open /dev/second :4
seconds after open /dev/second :5
...
10.6 內核延時
10.6.1 短延遲
Linux內核中提供了下列3個函數以分別進行納秒、微秒和毫秒延遲:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延遲的實現原理本質上是忙等待,它根據CPU頻率進行一定次數的循環。有時候,人們在軟件中
進行下面的延遲:
void delay(unsigned int time)
{
while(time--);
}
ndelay()、udelay()和mdelay()函數的實現方式原理與此類似。內核在啓動時,會運行一個延遲
循環校準(Delay Loop Calibration),計算出lpj(Loops Per Jiffy),內核啓動時會打印如下類似信息:
Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
如果我們直接在bootloader傳遞給內核的bootargs中設置lpj=1327104,則可以省掉這個校準的過程,節
省約百毫秒級的開機時間。
毫秒時延(以及更大的秒時延)已經比較大了,在內核中,最好不要直接使用mdelay()函數,這將
耗費CPU資源,對於毫秒級以上的時延,內核提供了下述函數:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
上述函數將使得調用它的進程睡眠參數指定的時間爲millisecs,msleep()、ssleep()不能被打斷,
而msleep_interruptible()則可以被打斷。
受系統Hz以及進程調度的影響,msleep()類似函數的精度是有限的。
10.6.2 長延遲
在內核中進行延遲的一個很直觀的方法是比較當前的jiffies和目標jiffies(設置爲當前jiffies加上時間間
隔的jiffies),直到未來的jiffies達到目標jiffies。代碼清單10.15給出了使用忙等待先延遲100個jiffies再延遲
2s的實例。
代碼清單10.15 忙等待時延實例
1/* 延遲100個jiffies */
2unsigned long delay = jiffies + 100;
3while(time_before(jiffies, delay));
45
/* 再延遲2s */
6unsigned long delay = jiffies + 2*Hz;
7while(time_before(jiffies, delay));
與time_before()對應的還有一個time_after(),它們在內核中定義爲(實際上只是將傳入的未來時
間jiffies和被調用時的jiffies進行一個簡單的比較):
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
爲了防止在time_before()和time_after()的比較過程中編譯器對jiffies的優化,內核將其定義爲
volatile變量,這將保證每次都會重新讀取這個變量。因此volatile更多的作用還是避免這種讀合併。
10.6.3 睡着延遲
睡着延遲無疑是比忙等待更好的方式,睡着延遲是在等待的時間到來之前進程處於睡眠狀態,CPU資
源被其他進程使用。schedule_timeout()可以使當前任務休眠至指定的jiffies之後再重新被調度執行,
msleep()和msleep_interruptible()在本質上都是依靠包含了schedule_timeout()的
schedule_timeout_uninterruptible()和schedule_timeout_interruptible()來實現的,如代碼清單10.16所示。
代碼清單10.16 schedule_timeout()的使用
1void msleep(unsigned int msecs)
2{
3 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
4
5 while (timeout)
6 timeout = schedule_timeout_uninterruptible(timeout);
7}
8
9unsigned long msleep_interruptible(unsigned int msecs)
10{
11 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
12
13 while (timeout && !signal_pending(current))
14 timeout = schedule_timeout_interruptible(timeout);
15 return jiffies_to_msecs(timeout);
16}
實際上,schedule_timeout()的實現原理是向系統添加一個定時器,在定時器處理函數中喚醒與參數
對應的進程。
代碼清單10.16中第6行和第14行分別調用schedule_timeout_uninterruptible()和
schedule_timeout_interruptible(),這兩個函數的區別在於前者在調用schedule_timeout()之前置進程狀
態爲TASK_INTERRUPTIBLE,後者置進程狀態爲TASK_UNINTERRUPTIBLE,如代碼清單10.17所示。
代碼清單10.17 schedule_timeout_interruptible()和schedule_timeout_interruptible()
1signed long __sched schedule_timeout_interruptible(signed long timeout)
2{
3 __set_current_state(TASK_INTERRUPTIBLE);
4 return schedule_timeout(timeout);
5}
6
7signed long __sched schedule_timeout_uninterruptible(signed long timeout)
8{
9 __set_current_state(TASK_UNINTERRUPTIBLE);
10 return schedule_timeout(timeout);
11}
另外,下面兩個函數可以將當前進程添加到等待隊列中,從而在等待隊列上睡眠。當超時發生時,進
程將被喚醒(後者可以在超時前被打斷):
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q, unsigned long timeout);
10.7 總結

Linux的中斷處理分爲兩個半部,頂半部處理緊急的硬件操作,底半部處理不緊急的耗時操作。tasklet
和工作隊列都是調度中斷底半部的良好機制,tasklet基於軟中斷實現。內核定時器也依靠軟中斷實現。
內核中的延時可以採用忙等待或睡眠等待,爲了充分利用CPU資源,使系統有更好的吞吐性能,在對
延遲時間的要求並不是很精確的情況下,睡眠等待通常是值得推薦的,而ndelay()、udelay()忙等待
機制在驅動中通常是爲了配合硬件上的短時延遲要求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章