linux把處理硬件中斷的過程分爲兩部分。上半部簡單快速,執行時禁止部分或全部中斷。下半部稍後執行,並且執行期間可以響應所有的中斷。這樣的設計會使系統處於中斷屏蔽的狀態儘可能的短,從而提高系統的響應能力。
下半部的處理方式主要有soft_irq,tasklet,workqueue三種,他們在使用方式和適用情況上各有不同。soft_irq用在對底半執行時間要求比較緊急或者非常重要的場合,在中斷上下文執行。tasklet和work queue在普通的driver裏用的相對較多,主要區別是tasklet是在中斷上下文執行,而workqueue是在process上下文,因此可以執行可能sleep的操作。
softirq和tasklet都運行在中斷上下文,那麼他們有什麼區別呢?
1.softirq是在編譯期間靜態分配的,它不像tasklet那樣可以動態的分配和刪除。
每個軟中斷在內核中以softirq_action表示。在kernel/softirq.c中定義了一個包含有32個該結構體的數組。每種軟中斷對應數組的一項,所以軟中斷最多有32項。
內核目前實現了10中軟中斷,定義在linux/interrupt.h中。
enum
{
HI_SOFTIRQ=0, /* 高優先級tasklet */ /* 優先級最高 */
TIMER_SOFTIRQ, /* 時鐘相關的軟中斷 */
NET_TX_SOFTIRQ, /* 將數據包傳送到網卡 */
NET_RX_SOFTIRQ, /* 從網卡接收數據包 */
BLOCK_SOFTIRQ, /* 塊設備的軟中斷 */
BLOCK_IOPOLL_SOFTIRQ, /* 支持IO輪詢的塊設備軟中斷 */
TASKLET_SOFTIRQ, /* 常規tasklet */
SCHED_SOFTIRQ, /* 調度程序軟中斷 */
HRTIMER_SOFTIRQ, /* 高精度計時器軟中斷 */
RCU_SOFTIRQ, /* RCU鎖軟中斷,該軟中斷總是最後一個軟中斷 */
NR_SOFTIRQS /* 軟中斷數,爲10 */
};
2.同一類型的softirq可以在不同的cpu上併發執行,而tasklet在使用時不需要考慮重入,因此tasklet更佳易用,使用softirq更傾向於性能。
softirq的使用
註冊軟中斷
通過調用open_softirq接口函數,將action函數指針指向向該軟中斷應該執行的函數。
/* 開啓軟中斷 */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
在start_kernel()進行系統初始化中,就調用了softirq_init()函數對HI_SOFTIRQ和TASKLET_SOFTIRQ兩個軟中斷進行了初始化
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
/* 開啓常規tasklet */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
/* 開啓高優先級tasklet */
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
當使用open_softirq設置好某個軟中斷的action指針後,該軟中斷就會開始可以使用了。
觸發軟中斷
調用raise_softirq這個接口函數來觸發本地CPU上的softirq。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
先是關閉本地cpu中斷,然後調用:raise_softirq_irqoff。
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
......
if (!in_interrupt())
wakeup_softirqd();
}
通過in_interrupt判斷現在是否在中斷上下文中,或者軟中斷是否被禁止,如果都不成立,否則,我們必須要調用wakeup_softirqd函數用來喚醒本CPU上的softirqd這個內核線程。
處理一個被觸發的軟中斷
1.在中斷返回現場時候調度softirq
在中斷處理程序中觸發軟中斷是最常見的形式,一個硬件中斷處理完成之後。下面的函數在處理完硬件中斷之後退出中斷處理函數,在irq_exit中會觸發軟件中斷的處理。
中斷處理模型:
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
...
irq_enter();
//handle external interrupt (ISR)
...
irq_exit();
return 1;
}
硬中斷執行完畢後
void irq_exit(void)
{
……
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
……
}
invoke_irq :
static inline void invoke_softirq(void)
{
if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/
/* 軟中斷處理函數 */
__do_softirq();
#else
/*
* Otherwise, irq_exit() is called on the task stack that can
* be potentially deep already. So call softirq in its own stack
* to prevent from any overrun.
*/
do_softirq_own_stack();
#endif
} else {
/* 如果強制使用軟中斷線程進行軟中斷處理,會通知調度器喚醒軟中斷線程ksoftirqd */
wakeup_softirqd();
}
}
可以看出,如果中斷髮生嵌套,in_interrupt()保證了只有在最外層的中斷的irq_exit階段,invoke_interrupt纔會被調用
最終調用do_softirq:
asmlinkage __visible void __do_softirq(void)
{
/* 爲了防止軟中斷執行時間太長,設置了一個軟中斷結束時間 */
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
/* 保存當前進程的標誌 */
unsigned long old_flags = current->flags;
/* 軟中斷循環執行次數: 10次 */
int max_restart = MAX_SOFTIRQ_RESTART;
/* 軟中斷的action指針 */
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;
/*
* Mask out PF_MEMALLOC s current task context is borrowed for the
* softirq. A softirq handled such as network RX might set PF_MEMALLOC
* again if the socket is related to swap
*/
current->flags &= ~PF_MEMALLOC;
/* 獲取此CPU的__softirq_pengding變量值 */
pending = local_softirq_pending();
/* 用於統計進程被軟中斷使用時間 */
account_irq_enter_time(current);
/* 增加preempt_count軟中斷計數器,也表明禁止了調度 */
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
/* 循環10次的入口,每次循環都會把所有掛起需要執行的軟中斷執行一遍 */
restart:
/* 該CPU的__softirq_pending清零,當前的__softirq_pending保存在pending變量中 */
/* 這樣做就保證了新的軟中斷會在下次循環中執行 */
set_softirq_pending(0);
/* 開中斷 */
local_irq_enable();
/* h指向軟中斷數組頭 */
h = softirq_vec;
/* 每次獲取最高優先級的已掛起軟中斷 */
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
/* 獲取此軟中斷描述符地址 */
h += softirq_bit - 1;
/* 減去軟中斷描述符數組首地址,獲得軟中斷號 */
vec_nr = h - softirq_vec;
/* 獲取preempt_count的值 */
prev_count = preempt_count();
/* 增加統計中該軟中斷髮生次數 */
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
/* 執行該軟中斷的action操作 */
h->action(h);
trace_softirq_exit(vec_nr);
/* 之前保存的preempt_count並不等於當前的preempt_count的情況處理,也是簡單的把之前的複製到當前的preempt_count上,這樣做是防止最後軟中斷計數不爲0導致系統不能夠執行調度 */
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
/* h指向下一個軟中斷,但下個軟中斷並不一定需要執行,這裏只是配合softirq_bit做到一個處理 */
h++;
pending >>= softirq_bit;
}
rcu_bh_qs();
/* 關中斷 */
local_irq_disable();
/* 循環結束後再次獲取CPU的__softirq_pending變量,爲了檢查是否還有軟中斷未執行 */
pending = local_softirq_pending();
/* 還有軟中斷需要執行 */
if (pending) {
/* 在還有軟中斷需要執行的情況下,如果時間片沒有執行完,並且循環次數也沒到10次,繼續執行軟中斷 */
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
/* 這裏是有軟中斷掛起,但是軟中斷時間和循環次數已經用完,通知調度器喚醒軟中斷線程去執行掛起的軟中斷,軟中斷線程是ksoftirqd,這裏只起到一個通知作用,因爲在中斷上下文中是禁止調度的 */
wakeup_softirqd();
}
lockdep_softirq_end(in_hardirq);
/* 用於統計進程被軟中斷使用時間 */
account_irq_exit_time(current);
/* 減少preempt_count中的軟中斷計數器 */
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
/* 還原進程標誌 */
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
• 首先取出pending的狀態;
• 禁止軟中斷,主要是爲了防止和軟中斷守護進程發生競爭;
• 清除所有的軟中斷待決標誌;
• 打開本地cpu中斷;
• 循環執行待決軟中斷的回調函數;
• 如果循環完畢,發現新的軟中斷被觸發,則重新啓動循環,直到以下條件滿足,才退出:
o 沒有新的軟中斷等待執行;
o 循環已經達到最大的循環次數MAX_SOFTIRQ_RESTART,目前的設定值時10次;
• 如果經過MAX_SOFTIRQ_RESTART次循環後還未處理完,則激活守護進程,處理剩下的軟中斷;
• 退出前恢復軟中斷;
2.在守護線程ksoftirq中執行。
雖然大部分的softirq是在中斷退出的情況下執行,但是有幾種情況會在ksoftirq中執行。
a.從上文看出,raise softirq主動觸發,而此時正好不是在中斷上下文中,ksoftirq進程將被喚醒。
b.在irq_exit中執行軟中斷,但是在經過MAX_SOFTIRQ_RESTART次循環後,軟中斷還未處理完,這種情況,ksoftirq進程也會被喚醒。
所以加入守護線程這一機制,主要是擔心一旦有大量的軟中斷等待執行,會使得內核過長地留在中斷上下文中。
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_sof tirq();
local_irq_enable();
cond_resched_rcu_qs();
return;
}
守護進程最終也會調用__do_softirq執行軟中斷的回調。