1. 程式人生 > >Linux中斷 - tasklet

Linux中斷 - tasklet

timer類 local handler 不同 ask store 圖片 軟硬件 ()

一、前言

對於中斷處理而言,linux將其分成了兩個部分,一個叫做中斷handler(top half),屬於不那麽緊急需要處理的事情被推遲執行,我們稱之deferable task,或者叫做bottom half,。具體如何推遲執行分成下面幾種情況:

1、推遲到top half執行完畢

2、推遲到某個指定的時間片(例如40ms)之後執行

3、推遲到某個內核線程被調度的時候執行

對於第一種情況,內核中的機制包括softirq機制和tasklet機制。第二種情況是屬於softirq機制的一種應用場景(timer類型的softirq),在本站的時間子系統的系列文檔中會描述。第三種情況主要包括threaded irq handler以及通用的workqueue機制,當然也包括自己創建該驅動專屬kernel thread(不推薦使用)。本文主要描述tasklet這種機制,第二章描述一些背景知識和和tasklet的思考,第三章結合代碼描述tasklet的原理。

註:本文中的linux kernel的版本是4.0

二、為什麽需要tasklet?

1、基本的思考

我們的驅動程序或者內核模塊真的需要tasklet嗎?每個人都有自己的看法。我們先拋開linux kernel中的機制,首先進行一番邏輯思考。

將中斷處理分成top half(cpu和外設之間的交互,獲取狀態,ack狀態,收發數據等)和bottom half(後段的數據處理)已經深入人心,對於任何的OS都一樣,將不那麽緊急的事情推遲到bottom half中執行是OK的,具體如何推遲執行分成兩種類型:有具體時間要求的(對應linux kernel中的低精度timer和高精度timer)和沒有具體時間要求的。對於沒有具體時間要求的又可以分成兩種:

(1)越快越好型,這種實際上是有性能要求的,除了中斷top half可以搶占其執行,其他的進程上下文(無論該進程的優先級多麽的高)是不會影響其執行的,一言以蔽之,在不影響中斷延遲的情況下,OS會盡快處理。

(2)隨遇而安型。這種屬於那種沒有性能需求的,其調度執行依賴系統的調度器。

本質上講,越快越好型的bottom half不應該太多,而且tasklet的callback函數不能執行時間過長,否則會產生進程調度延遲過大的現象,甚至是非常長而且不確定的延遲,對real time的系統會產生很壞的影響。

2、對linux中的bottom half機制的思考

在linux kernel中,“越快越好型”有兩種,softirq和tasklet,“隨遇而安型”也有兩種,workqueue和threaded irq handler。“越快越好型”能否只留下一個softirq呢?對於崇尚簡單就是美的程序員當然希望如此。為了回答這個問題,我們先看看tasklet對於softirq而言有哪些好處:

(1)tasklet可以動態分配,也可以靜態分配,數量不限。

(2)同一種tasklet在多個cpu上也不會並行執行,這使得程序員在撰寫tasklet function的時候比較方便,減少了對並發的考慮(當然損失了性能)。

對於第一種好處,其實也就是為亂用tasklet打開了方便之門,很多撰寫驅動的軟件工程師不會仔細考量其driver是否有性能需求就直接使用了tasklet機制。對於第二種好處,本身考慮並發就是軟件工程師的職責。因此,看起來tasklet並沒有引入特別的好處,而且和softirq一樣,都不能sleep,限制了handler撰寫的方便性,看起來其實並沒有存在的必要。在4.0 kernel的代碼中,grep一下tasklet的使用,實際上是一個很長的列表,只要對這些使用進行簡單的歸類就可以刪除對tasklet的使用。對於那些有性能需求的,可以考慮並入softirq,其他的可以考慮使用workqueue來取代。Steven Rostedt試圖進行這方面的嘗試(http://lwn.net/Articles/239484/),不過這個patch始終未能進入main line。

三、tasklet的基本原理

1、如何抽象一個tasklet

內核中用下面的數據結構來表示tasklet:

struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};

每個cpu都會維護一個鏈表,將本cpu需要處理的tasklet管理起來,next這個成員指向了該鏈表中的下一個tasklet。func和data成員描述了該tasklet的callback函數,func是調用函數,data是傳遞給func的參數。state成員表示該tasklet的狀態,TASKLET_STATE_SCHED表示該tasklet以及被調度到某個CPU上執行,TASKLET_STATE_RUN表示該tasklet正在某個cpu上執行。count成員是和enable或者disable該tasklet的狀態相關,如果count等於0那麽該tasklet是處於enable的,如果大於0,表示該tasklet是disable的。在softirq文檔中,我們知道local_bh_disable/enable函數就是用來disable/enable bottom half的,這裏就包括softirq和tasklet。但是,有的時候內核同步的場景不需disable所有的softirq和tasklet,而僅僅是disable該tasklet,這時候,tasklet_disable和tasklet_enable就派上用場了。

static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);-------給tasklet的count加一
tasklet_unlock_wait(t);-----如果該tasklet處於running狀態,那麽需要等到該tasklet執行完畢
smp_mb();
}

static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);-------給tasklet的count減一
}

tasklet_disable和tasklet_enable支持嵌套,但是需要成對使用。

2、系統如何管理tasklet?

系統中的每個cpu都會維護一個tasklet的鏈表,定義如下:

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

linux kernel中,和tasklet相關的softirq有兩項,HI_SOFTIRQ用於高優先級的tasklet,TASKLET_SOFTIRQ用於普通的tasklet。對於softirq而言,優先級就是出現在softirq pending register(__softirq_pending)中的先後順序,位於bit 0擁有最高的優先級,也就是說,如果有多個不同類型的softirq同時觸發,那麽執行的先後順序依賴在softirq pending register的位置,kernel總是從右向左依次判斷是否置位,如果置位則執行。HI_SOFTIRQ占據了bit 0,其優先級甚至高過timer,需要慎用(實際上,我grep了內核代碼,似乎沒有發現對HI_SOFTIRQ的使用)。當然HI_SOFTIRQ和TASKLET_SOFTIRQ的機理是一樣的,因此本文只討論TASKLET_SOFTIRQ,大家可以舉一反三。

3、如何定義一個tasklet?

你可以用下面的宏定義來靜態定義tasklet:

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

這兩個宏都可以靜態定義一個struct tasklet_struct的變量,只不過初始化後的tasklet一個是處於eable狀態,一個處於disable狀態的。當然,也可以動態分配tasklet,然後調用tasklet_init來初始化該tasklet。

4、如何調度一個tasklet

為了調度一個tasklet執行,我們可以使用tasklet_schedule這個接口:

static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}

程序在多個上下文中可以多次調度同一個tasklet執行(也可能來自多個cpu core),不過實際上該tasklet只會一次掛入首次調度到的那個cpu的tasklet鏈表,也就是說,即便是多次調用tasklet_schedule,實際上tasklet只會掛入一個指定CPU的tasklet隊列中(而且只會掛入一次),也就是說只會調度一次執行。這是通過TASKLET_STATE_SCHED這個flag來完成的,我們可以用下面的圖片來描述:

技術分享圖片

我們假設HW block A的驅動使用的tasklet機制並且在中斷handler(top half)中將靜態定義的tasklet(這個tasklet是各個cpu共享的,不是per cpu的)調度執行(也就是調用tasklet_schedule函數)。當HW block A檢測到硬件的動作(例如接收FIFO中數據達到半滿)就會觸發IRQ line上的電平或者邊緣信號,GIC檢測到該信號會將該中斷分發給某個CPU執行其top half handler,我們假設這次是cpu0,因此該driver的tasklet被掛入CPU0對應的tasklet鏈表(tasklet_vec)並將state的狀態設定為TASKLET_STATE_SCHED。HW block A的驅動中的tasklet雖已調度,但是沒有執行,如果這時候,硬件又一次觸發中斷並在cpu1上執行,雖然tasklet_schedule函數被再次調用,但是由於TASKLET_STATE_SCHED已經設定,因此不會將HW block A的驅動中的這個tasklet掛入cpu1的tasklet鏈表中。

下面我們再仔細研究一下底層的__tasklet_schedule函數:

void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;

local_irq_save(flags);-------------------(1)
t->next = NULL;---------------------(2)
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);----------(3)
local_irq_restore(flags);
}

(1)下面的鏈表操作是per-cpu的,因此這裏禁止本地中斷就可以攔截所有的並發。

(2)這裏的三行代碼就是將一個tasklet掛入鏈表的尾部

(3)raise TASKLET_SOFTIRQ類型的softirq。

5、在什麽時機會執行tasklet?

上面描述了tasklet的調度,當然調度tasklet不等於執行tasklet,系統會在適合的時間點執行tasklet callback function。由於tasklet是基於softirq的,因此,我們首先總結一下softirq的執行場景:

(1)在中斷返回用戶空間(進程上下文)的時候,如果有pending的softirq,那麽將執行該softirq的處理函數。這裏限定了中斷返回用戶空間也就是意味著限制了下面兩個場景的softirq被觸發執行:

(a)中斷返回hard interrupt context,也就是中斷嵌套的場景

(b)中斷返回software interrupt context,也就是中斷搶占軟中斷上下文的場景

(2)上面的描述缺少了一種場景:中斷返回內核態的進程上下文的場景,這裏我們需要詳細說明。進程上下文中調用local_bh_enable的時候,如果有pending的softirq,那麽將執行該softirq的處理函數。由於內核同步的要求,進程上下文中有可能會調用local_bh_enable/disable來保護臨界區。在臨界區代碼執行過程中,中斷隨時會到來,搶占該進程(內核態)的執行(註意:這裏只是disable了bottom half,沒有禁止中斷)。在這種情況下,中斷返回的時候是否會執行softirq handler呢?當然不會,我們disable了bottom half的執行,也就是意味著不能執行softirq handler,但是本質上bottom half應該比進程上下文有更高的優先級,一旦條件允許,要立刻搶占進程上下文的執行,因此,當立刻離開臨界區,調用local_bh_enable的時候,會檢查softirq pending,如果bottom half處於enable的狀態,pending的softirq handler會被執行。

(3)系統太繁忙了,不過的產生中斷,raise softirq,由於bottom half的優先級高,從而導致進程無法調度執行。這種情況下,softirq會推遲到softirqd這個內核線程中去執行。

對於TASKLET_SOFTIRQ類型的softirq,其handler是tasklet_action,我們來看看各個tasklet是如何執行的:

static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;

local_irq_disable();--------------------------(1)
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable();

while (list) {---------遍歷tasklet鏈表
struct tasklet_struct *t = list;

list = list->next;

if (tasklet_trylock(t)) {-----------------------(2)
if (!atomic_read(&t->count)) {------------------(3)
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;-----處理下一個tasklet
}
tasklet_unlock(t);----清除TASKLET_STATE_RUN標記
}

local_irq_disable();-----------------------(4)
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ); ------再次觸發softirq,等待下一個執行時機
local_irq_enable();
}
}

(1)從本cpu的tasklet鏈表中取出全部的tasklet,保存在list這個臨時變量中,同時重新初始化本cpu的tasklet鏈表,使該鏈表為空。由於bottom half是開中斷執行的,因此在操作tasklet鏈表的時候需要使用關中斷保護

(2)tasklet_trylock主要是用來設定該tasklet的state為TASKLET_STATE_RUN,同時判斷該tasklet是否已經處於執行狀態,這個狀態很重要,它決定了後續的代碼邏輯。

static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

你也許會奇怪:為何這裏從tasklet的鏈表中摘下一個本cpu要處理的tasklet list,而這個list中的tasklet已經處於running狀態了,會有這種情況嗎?會的,我們再次回到上面的那個軟硬件結構圖。同樣的,HW block A的驅動使用的tasklet機制並且在中斷handler(top half)中將靜態定義的tasklet 調度執行。HW block A的硬件中斷首先送達cpu0處理,因此該driver的tasklet被掛入CPU0對應的tasklet鏈表並在適當的時間點上開始執行該tasklet。這時候,cpu0的硬件中斷又來了,該driver的tasklet callback function被搶占,雖然tasklet仍然處於running狀態。與此同時,HW block A硬件又一次觸發中斷並在cpu1上執行,這時候,該driver的tasklet處於running狀態,並且TASKLET_STATE_SCHED已經被清除,因此,調用tasklet_schedule函數將會使得該driver的tasklet掛入cpu1的tasklet鏈表中。由於cpu0在處理其他硬件中斷,因此,cpu1的tasklet後發先至,進入tasklet_action函數調用,這時候,當從cpu1的tasklet摘取所有需要處理的tasklet鏈表中,HW block A對應的tasklet實際上已經是在cpu0上處於執行狀態了。

我們在設計tasklet的時候就規定,同一種類型的tasklet只能在一個cpu上執行,因此tasklet_trylock就是起這個作用的。

(3)檢查該tasklet是否處於enable狀態,如果是,說明該tasklet可以真正進入執行狀態了。主要的動作就是清除TASKLET_STATE_SCHED狀態,執行tasklet callback function。

(4)如果該tasklet已經在別的cpu上執行了,那麽我們將其掛入該cpu的tasklet鏈表的尾部,這樣,在下一個tasklet執行時機到來的時候,kernel會再次嘗試執行該tasklet,在這個時間點,也許其他cpu上的該tasklet已經執行完畢了。通過這樣代碼邏輯,保證了特定的tasklet只會在一個cpu上執行,不會在多個cpu上並發。

Linux中斷 - tasklet