咱們知道,Linux中斷的上半部用於處理很是緊急的任務,而延時處理的任務一般須要放到中斷的下半部去處理。軟中斷(Softirq)是Linux內核中斷下半部的一部分,是中斷下半部tasklet的組成基礎。設計軟中斷,是爲了儘快釋放中斷上半部,使得軟中斷中處理的耗時任務不去阻塞中斷上半部的執行,從而提高系統的響應。程序員
那麼,軟中斷是如何設計的?又是如何被調度執行的?如何實現新的軟中斷?軟中斷的使用須要注意些什麼?清楚了這些邏輯,咱們也就清楚了軟中斷的框架結構。數組
1,軟中斷是如何設計的?數據結構
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */框架
NR_SOFTIRQS
};socket
struct softirq_action
{
void (*action)(struct softirq_action *);
};函數
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;性能
從上面的定義能夠看出:內核預約義了一組軟中斷類型,每種類型的軟中斷的數據結構都是一個 softirq_action,一個softirq_action就是一個回調函數。那麼這個數組是如何被調度執行的?this
2,軟中斷如何被調度執行?線程
咱們知道,中斷上半部執行完以後(好比,從網卡接收了數據包存放在內存某處),後續的耗時任務(如,對數據包的解析,本地遞交或轉發等)須要調度軟中斷來處理,那麼內核是如何調度軟中斷來處理的呢?咱們來看其中一個路徑,咱們知道中斷上半部執行完以後會調用 irq_exit()函數:設計
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
WARN_ON_ONCE(!irqs_disabled());
#endif
account_irq_exit_time(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending()) // 此處判斷是否從中斷上下文退出,並判斷是否有軟中斷任務掛起待處理
invoke_softirq(); // 啓用,調度軟中斷處理
tick_irq_exit();
rcu_irq_exit();
trace_hardirq_exit(); /* must be last! */
}
從上面的調用路徑,咱們可以看出來:當內核路徑從中斷上下文退出,而且有軟中斷任務等待處理時,內核主動調用 invoke_softirq(),調度軟中斷處理。in_interrupt() 判斷是否退出全部嵌套的中斷上下文。local_softirq_pending()用於判斷是否有軟中斷任務待處理:
#define local_softirq_pending() this_cpu_read(irq_stat.__softirq_pending)
中斷上半部在處理完後,若是須要在軟中斷中做後續處理,經過set_softirq_pending(x)設置便可:
#define set_softirq_pending(x) \
this_cpu_write(irq_stat.__softirq_pending, (x))
再看 invoke_softirq():
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 {
wakeup_softirqd(); // 軟中斷線程中執行軟中斷任務
}
}
以上代碼能夠看出:軟中斷任務能夠在兩個環境下執行:一種是軟中斷上下文,另一種是在軟中斷內核線程中執行。二者的區別是,軟中斷上下文中不能睡眠,不能被調度;軟中斷內核線程能夠睡眠,能夠被調度。軟中斷被設計在兩種上下文中執行,是Linux內核對系統運行性能策略的折衷。優先在軟中斷上下文執行必定的時間,若是任務還未完成,再喚醒軟中斷內核線程調度軟中斷任務執行。這樣在保證任務實時性的同時,也不至於系統被軟中斷任務掛死。咱們分析軟中斷上下文的處理過程:
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME; // 最大處理時間:2毫秒
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART; // 最大循環次數:10次
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;
pending = local_softirq_pending(); // 獲取本地CPU上等待處理的軟中斷掩碼
account_irq_enter_time(current);
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0); // 清除本地CPU上等待處理的軟中斷掩碼
local_irq_enable(); // 開中斷狀態下處理軟中斷
h = softirq_vec; // h指向軟中斷處理函數數組首元素
while ((softirq_bit = ffs(pending))) { // 依次處理軟中斷,軟中斷編號越小,越優先處理,優先級越高
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
vec_nr = h - softirq_vec;
prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action(h); // 調用軟中斷回調處理函數
trace_softirq_exit(vec_nr);
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++;
pending >>= softirq_bit;
}
rcu_bh_qs();
local_irq_disable(); // 關中斷,判斷在處理上次軟中斷期間,硬中斷處理函數是否又調度了軟中斷
pending = local_softirq_pending();
if (pending) { // 軟中斷再次被調度
if (time_before(jiffies, end) && !need_resched() &&
--max_restart) // 沒有達到超時時間,也不須要被調度,而且調度次數也沒有超過10次
goto restart; // 從新執行軟中斷
wakeup_softirqd(); // 不然喚醒軟中斷內核線程處理剩下的軟中斷,當前CPU退出軟中斷上下文
}
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
current_restore_flags(old_flags, PF_MEMALLOC);
}
這段代碼展現了軟中斷上下文的處理策略:一次處理全部等待的軟中斷的,循環處理最多10次,而且最大處理時間爲2ms。不管是循環超過10次,仍是總處理時間超過2ms,CPU都要退出軟中斷上下文,調度軟中斷內核線程處理軟中斷,給其餘進程/線程運行機會,避免系統響應過慢。
3,如何加入新的軟中斷?
咱們知道,內核預約義了好幾種軟中斷:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
內核提供了open_softirq()接口供各相關模塊註冊相應的軟中斷處理函數:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action; // 註冊軟中斷處理函數對應相應的軟中斷類型
}
4,軟中斷的使用有哪些注意事項?
(1)軟中斷是內核靜態編譯的,添加新的軟中斷,須要修改內核軟中斷編號枚舉結構。
(2)同一類型的軟中斷能夠在不一樣CPU上同時運行(由於軟中斷的掩碼是per-cpu變量),同一類型的軟中斷處理函數只有一個。所以,軟中斷的處理函數必須是可重入的,須要程序員保證資源的互斥訪問,這無疑增長了用戶的負擔,從而使得使用軟中斷的複雜度變高。後續咱們將看到,tasklet機制的出現將會有效的解決這個問題:不一樣類型的tasklet能夠同時運行在不一樣CPU上,但同一類型的tasklet同時只能運行在一個CPU上,這就使得用戶無需考慮函數重入問題,減輕了程序員的負擔。