Linux中斷(interrupt)子系統之五:軟體中斷(softIRQ)
阿新 • • 發佈:2019-02-05
軟體中斷(softIRQ)是核心提供的一種延遲執行機制,它完全由軟體觸發,雖然說是延遲機制,實際上,在大多數情況下,它與普通程序相比,能得到更快的響應時間。軟中斷也是其他一些核心機制的基礎,比如tasklet,高解析度timer等。
/*****************************************************************************************************/
宣告:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/
點選(此處)摺疊或開啟
-
struct softirq_action
-
{
-
void (*action)(struct
softirq_action *);
- };
非常簡單,只有一個用於回撥的函式指標。軟體中斷的資源是有限的,核心目前只實現了10種類型的軟體中斷,它們是:
點選(此處)摺疊或開啟
-
enum
-
{
-
HI_SOFTIRQ=0,
-
TIMER_SOFTIRQ,
-
NET_TX_SOFTIRQ,
-
NET_RX_SOFTIRQ,
-
BLOCK_SOFTIRQ,
-
BLOCK_IOPOLL_SOFTIRQ,
-
TASKLET_SOFTIRQ,
-
SCHED_SOFTIRQ,
-
HRTIMER_SOFTIRQ,
-
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
-
NR_SOFTIRQS
- };
核心的開發者們不建議我們擅自增加軟體中斷的數量,如果需要新的軟體中斷,儘可能把它們實現為基於軟體中斷的tasklet形式。與上面的列舉值相對應,核心定義了一個softirq_action的結構陣列,每種軟中斷對應陣列中的一項:
點選(此處)摺疊或開啟
- static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
1.2 irq_cpustat_t 多個軟中斷可以同時在多個cpu執行,就算是同一種軟中斷,也有可能同時在多個cpu上執行。核心為每個cpu都管理著一個待決軟中斷變數(pending),它就是irq_cpustat_t:
點選(此處)摺疊或開啟
-
typedef struct {
-
unsigned int __softirq_pending;
- } ____cacheline_aligned irq_cpustat_t;
點選(此處)摺疊或開啟
- irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
__softirq_pending欄位中的每一個bit,對應著某一個軟中斷,某個bit被置位,說明有相應的軟中斷等待處理。
1.3 軟中斷的守護程序ksoftirqd 在cpu的熱插拔階段,核心為每個cpu建立了一個用於執行軟體中斷的守護程序ksoftirqd,同時定義了一個per_cpu變數用於儲存每個守護程序的task_struct結構指標:
點選(此處)摺疊或開啟
- DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
大多數情況下,軟中斷都會在irq_exit階段被執行,在irq_exit階段沒有處理完的軟中斷才有可能會在守護程序中執行。
2. 觸發軟中斷 要觸發一個軟中斷,只要呼叫api:raise_softirq即可,它的實現很簡單,先是關閉本地cpu中斷,然後呼叫:raise_softirq_irqoff
點選(此處)摺疊或開啟
-
void raise_softirq(unsigned int nr)
-
{
-
unsigned long flags;
-
local_irq_save(flags);
-
raise_softirq_irqoff(nr);
-
local_irq_restore(flags);
- }
再看看raise_softirq_irqoff:
點選(此處)摺疊或開啟
-
inline void raise_softirq_irqoff(unsigned int nr)
-
{
-
__raise_softirq_irqoff(nr);
-
......
-
if (!in_interrupt())
-
wakeup_softirqd();
- }
先是通過__raise_softirq_irqoff設定cpu的軟中斷pending標誌位(irq_stat[NR_CPUS] ),然後通過in_interrupt判斷現在是否在中斷上下文中,或者軟中斷是否被禁止,如果都不成立,則喚醒軟中斷的守護程序,在守護程序中執行軟中斷的回撥函式。否則什麼也不做,軟中斷將會在中斷的退出階段被執行。
3. 軟中斷的執行 基於上面所說,軟中斷的執行既可以守護程序中執行,也可以在中斷的退出階段執行。實際上,軟中斷更多的是在中斷的退出階段執行(irq_exit),以便達到更快的響應,加入守護程序機制,只是擔心一旦有大量的軟中斷等待執行,會使得核心過長地留在中斷上下文中。
3.1 在irq_exit中執行 看看irq_exit的部分:
點選(此處)摺疊或開啟
-
void irq_exit(void)
-
{
-
......
-
sub_preempt_count(IRQ_EXIT_OFFSET);
-
if (!in_interrupt() && local_softirq_pending())
-
invoke_softirq();
-
......
- }
如果中斷髮生巢狀,in_interrupt()保證了只有在最外層的中斷的irq_exit階段,invoke_interrupt才會被呼叫,當然,local_softirq_pending也會實現判斷當前cpu有無待決的軟中斷。程式碼最終會進入__do_softirq中,核心會保證呼叫__do_softirq時,本地cpu的中斷處於關閉狀態,進入__do_softirq:
點選(此處)摺疊或開啟
-
asmlinkage void __do_softirq(void)
-
{
-
......
-
pending = local_softirq_pending();
-
__local_bh_disable((unsigned long)__builtin_return_address(0),
-
SOFTIRQ_OFFSET);
-
restart:
-
/* Reset the pending bitmask before enabling irqs */
-
set_softirq_pending(0);
-
local_irq_enable();
-
h = softirq_vec;
-
do {
-
if (pending & 1) {
-
......
-
trace_softirq_entry(vec_nr);
-
h->action(h);
-
trace_softirq_exit(vec_nr);
-
......
-
}
-
h++;
-
pending >>= 1;
-
} while (pending);
-
local_irq_disable();
-
pending = local_softirq_pending();
-
if (pending && --max_restart)
-
goto restart;
-
if (pending)
-
wakeup_softirqd();
-
lockdep_softirq_exit();
-
__local_bh_enable(SOFTIRQ_OFFSET);
- }
- 首先取出pending的狀態;
- 禁止軟中斷,主要是為了防止和軟中斷守護程序發生競爭;
- 清除所有的軟中斷待決標誌;
- 開啟本地cpu中斷;
- 迴圈執行待決軟中斷的回撥函式;
-
如果迴圈完畢,發現新的軟中斷被觸發,則重新啟動迴圈,直到以下條件滿足,才退出:
- 沒有新的軟中斷等待執行;
- 迴圈已經達到最大的迴圈次數MAX_SOFTIRQ_RESTART,目前的設定值時10次;
- 如果經過MAX_SOFTIRQ_RESTART次迴圈後還未處理完,則啟用守護程序,處理剩下的軟中斷;
- 推出前恢復軟中斷;
3.2 在ksoftirqd程序中執行 從前面幾節的討論我們可以看出,軟中斷也可能由ksoftirqd守護程序執行,這要發生在以下兩種情況下:
- 在irq_exit中執行軟中斷,但是在經過MAX_SOFTIRQ_RESTART次迴圈後,軟中斷還未處理完,這種情況雖然極少發生,但畢竟有可能;
- 核心的其它程式碼主動呼叫raise_softirq,而這時正好不是在中斷上下文中,守護程序將被喚醒;
4. tasklet 因為核心已經定義好了10種軟中斷型別,並且不建議我們自行新增額外的軟中斷,所以對軟中斷的實現方式,我們主要是做一個簡單的瞭解,對於驅動程式的開發者來說,無需實現自己的軟中斷。但是,對於某些情況下,我們不希望一些操作直接在中斷的handler中執行,但是又希望在稍後的時間裡得到快速地處理,這就需要使用tasklet機制。 tasklet是建立在軟中斷上的一種延遲執行機制,它的實現基於TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷型別。 4.1 tasklet_struct 在軟中斷的初始化函式softirq_init的最後,核心註冊了TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷:
點選(此處)摺疊或開啟
-
void __init softirq_init(void)
-
{
-
......
-
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
-
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
- }
核心用一個tasklet_struct來表示一個tasklet,它的定義如下:
點選(此處)摺疊或開啟
-
struct tasklet_struct
-
{
-
struct tasklet_struct *next;
-
unsigned long state;
-
atomic_t count;
-
void (*func)(unsigned
long);
-
unsigned long data;
- };
next用於把同一個cpu的tasklet連結成一個連結串列,state用於表示該tasklet的當前狀態,目前只是用了最低的兩個bit,分別用於表示已經準備被排程執行和已經在另一個cpu上執行:
點選(此處)摺疊或開啟
-
enum
-
{
-
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
-
TASKLET_STATE_RUN /* Tasklet is running (SMP
only) */
- };
原子變數count用於tasklet對tasklet_disable和tasklet_enable的計數,count為0時表示允許tasklet執行,否則不允許執行,每次tasklet_disable時,該值加1,tasklet_enable時該值減1。func是tasklet被執行時的回撥函式指標,data則用作回撥函式func的引數。 4.2 初始化一個tasklet 有兩種辦法初始化一個tasklet,第一種是靜態初始化,使用以下兩個巨集,這兩個巨集定義一個tasklet_struct結構,並用相應的引數對結構中的欄位進行初始化:
-
DECLARE_TASKLET(name, func, data);定義名字為name的tasklet,預設為enable狀態,也就是count欄位等於0。
-
DECLARE_TASKLET_DISABLED(name, func, data);定義名字為name的tasklet,預設為enable狀態,也就是count欄位等於1。
點選(此處)摺疊或開啟
-
struct tasklet_struct tasklet_xxx;
-
......
- tasklet_init(&tasklet_xxx, func, data);
4.3 tasklet的使用方法 使能和禁止tasklet,使用以下函式:
-
tasklet_disable() 通過給count欄位加1來禁止一個tasklet,如果tasklet正在執行中,則等待執行完畢才返回(通過TASKLET_STATE_RUN標誌)。
-
tasklet_disable_nosync() tasklet_disable的非同步版本,它不會等待tasklet執行完畢。
-
tasklet_enable() 使能tasklet,只是簡單地給count欄位減1。
-
tasklet_schedule(struct tasklet_struct *t) 如果TASKLET_STATE_SCHED標誌為0,則置位TASKLET_STATE_SCHED,然後把tasklet掛到該cpu等待執行的tasklet連結串列上,接著發出TASKLET_SOFTIRQ軟體中斷請求。
-
tasklet_hi_schedule(struct tasklet_struct *t) 效果同上,區別是它發出的是HI_SOFTIRQ軟體中斷請求。
-
tasklet_kill(struct tasklet_struct *t) 如果tasklet處於TASKLET_STATE_SCHED狀態,或者tasklet正在執行,則會等待tasklet執行完畢,然後清除TASKLET_STATE_SCHED狀態。
4.4 tasklet的內部執行機制 核心為每個cpu用定義了一個tasklet_head結構,用於管理每個cpu上的tasklet的排程和執行:
點選(此處)摺疊或開啟
-
struct tasklet_head
-
{
-
struct tasklet_struct *head;
-
struct tasklet_struct **tail;
-
};
-
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
- static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
回到4.1節,我們知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷來實現的,兩個軟中斷只是有優先順序的差別,所以我們只討論TASKLET_SOFTIRQ的實現,TASKLET_SOFTIRQ的中斷回撥函式是tasklet_action,我們看看它的程式碼:
點選(此處)摺疊或開啟
-
static void tasklet_action(struct softirq_action *a)
-
{
-
struct tasklet_struct *list;
-
local_irq_disable();
-
list = __this_cpu_read(tasklet_vec.head);
-
__this_cpu_write(tasklet_vec.head, NULL);
-
__this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
-
local_irq_enable();
-
while (list) {
-
struct tasklet_struct *t = list;
-
list = list->next;
-
if (tasklet_trylock(t)) {
-
if (!atomic_read(&t->count)) {
-
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
-
BUG();
-
t->func(t->data);
-
tasklet_unlock(t);
-
continue;
-
}
-
tasklet_unlock(t);
-
}
-
local_irq_disable();
-
t->next = NULL;
-
*__this_cpu_read(tasklet_vec.tail) = t;
-
__this_cpu_write(tasklet_vec.tail, &(t->next));
-
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
-
local_irq_enable();
-
}
- }
解析如下:
- 關閉本地中斷的前提下,移出當前cpu的待處理tasklet連結串列到一個臨時連結串列後,清除當前cpu的tasklet連結串列,之所以這樣處理,是為了處理當前tasklet連結串列的時候,允許新的tasklet被排程進待處理連結串列中。
-
遍歷臨時連結串列,用tasklet_trylock判斷當前tasklet是否已經在其他cpu上執行,而且tasklet沒有被禁止:
- 如果沒有執行,也沒有禁止,則清除TASKLET_STATE_SCHED狀態位,執行tasklet的回撥函式。
- 如果已經在執行,或者被禁止,則把該tasklet重新新增會當前cpu的待處理tasklet連結串列上,然後觸發TASKLET_SOFTIRQ軟中斷,等待下一次軟中斷時再次執行。
- 同一個tasklet只能同時在一個cpu上執行,但不同的tasklet可以同時在不同的cpu上執行;
- 一旦tasklet_schedule被呼叫,核心會保證tasklet一定會在某個cpu上執行一次;
- 如果tasklet_schedule被呼叫時,tasklet不是出於正在執行狀態,則它只會執行一次;
-
如果tasklet_schedule被呼叫時,tasklet已經正在執行,則它會在稍後被排程再次被執行;
- 兩個tasklet之間如果有資源衝突,應該要用自旋鎖進行同步保護;