參考:
http://bbs.chinaunix.net/thread-2333484-1-1.html
http://liu1227787871.blog.163.com/blog/static/20536319720129210112658/
1、軟中斷
一般來說,一次中斷服務的過程通常能夠分為兩個部分。
開頭的 部分往往必須在關中斷的條件下執行,這樣才幹在不受干擾的條件下“原子”地完畢一些關鍵性操作。同一時候這部分操作的時間性又往往非常強。必須在中斷請求發生後馬上或至少在一定時間限制中執行,並且相繼的多次中斷請求也不能合併在一起來處理。
而後半部分,通常能夠並且應該在開中斷的條件下執行。這樣才不至於因中斷關閉過久而造成其它中斷的丟失,同一時候,這些操作經常同意延時到稍後才來執行。並且有可能多次中斷的相關部分合並在一起處理。
這些不同的性質經常使中斷服務的前後兩半明顯地區分開來,能夠並且應該分別加以不同的實現。這裡的後半部分就稱為"bottom half",在核心程式碼中往往寫成bf 。而bf的這部分就能夠通過軟體中斷來實現。
由於軟體中斷的啟用是通過程式碼來實現的,而不是硬體,所以就能夠自己或由系統來決定啟用的時機!
1.1 註冊
還是以我最熟悉的兩個老朋友做為開篇:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
open_softirq向核心註冊一個軟中斷。事實上質是設定軟中斷向量表對應槽位。註冊其處理函式:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
softirq_vec是整個軟中斷的向量表:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{ softirq_vec[nr].data = data; softirq_vec[nr].action = action;
};
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
NR_SOFTIRQS是最大軟中斷向量數。核心支援的全部軟中斷例如以下:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
好像後為為RPS新增了一個。只是這我的核心版本號偏低。
1.2 啟用
當須要呼叫軟中斷時,須要呼叫raise_softirq函式啟用軟中斷。這裡使用術語“啟用”而非“呼叫”。
是由於在非常多情況下不能直接呼叫軟中斷。
所以僅僅能高速地將其標誌為“可執行”。等待未來某一時刻呼叫。
為什麼“在非常多情況下不能直接呼叫軟中斷”?試想一下下半部引入的理念。就是為了讓上半部更快地執行。
假設在中斷程式程式碼中直接呼叫軟中斷函式,那麼就失去了上半部與下半部的差別,也就是失去了其存在的意義。
核心使用一個名為__softirq_pending的點陣圖來描寫敘述軟中斷,每個位相應一個軟中斷,點陣圖包括在結構irq_stat中:
typedef struct {
unsigned int __softirq_pending;
……
} ____cacheline_aligned irq_cpustat_t;
DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);
巨集or_softirq_pending用於設定對應的位(位或操作):
#define or_softirq_pending(x) percpu_or(irq_stat.__softirq_pending, (x))
local_softirq_pending用於取得整個點陣圖(而非某一位):
#define local_softirq_pending() percpu_read(irq_stat.__softirq_pending)
巨集__raise_softirq_irqoff是or_softirq_pending的包裹:
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
raise_softirq_irqoff通過呼叫__raise_softirq_irqoff實現啟用軟中斷。它的引數nr即位軟中斷相應的點陣圖槽位:
/*
* This function must run with irqs disabled!
*/
inline void raise_softirq_irqoff(unsigned int nr)
{
//置點陣圖,即標記為可執行狀態
__raise_softirq_irqoff(nr);
/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
//設定了點陣圖後。能夠推斷是否已經沒有在中斷上下文中了,假設沒有,則是一個馬上呼叫軟中斷的好時機。
//in_interrupt還有一個作用是推斷軟中斷是否被禁用。 //wakeup_softirqd喚醒軟中斷的守護程式ksoftirq。
if (!in_interrupt()) wakeup_softirqd(); } 複製程式碼 如今能夠來看"啟用"軟中斷的全部含義了,raise_softirq函式完畢這一操作: void raise_softirq(unsigned int nr) { unsigned long flags; //全部操作,應該關閉中斷,避免巢狀呼叫 local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags); }
可見,啟用的操作,主要是兩點:
<1>、最重要的,就是置對應的點陣圖。等待將來被處理;
<2>、假設此時已經沒有在中斷上下文中,則馬上呼叫(事實上是核心執行緒的喚醒操作),如今就是將來;
2、排程時機
是的。除了raise_softirq在,可能會(嗯,重要的是“可能”)通過wakeup_softirqd喚醒ksoftirqd外,還得明確軟中斷的其他呼叫時機。
A、當do_IRQ完畢了I/O中斷時呼叫irq_exit:
#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq() __do_softirq()
#else
# define invoke_softirq() do_softirq()
#endif
void irq_exit(void)
{
account_system_vtime(current);
trace_hardirq_exit();
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); //呼叫軟中斷
B、假設系統使用I/O APIC,在處理完本地時鐘中斷時:
void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
{
……
irq_exit();
……
}
C、local_bh_enable
local_bh_enable就是開啟下半部。當然重中之中就是軟中斷了:
void local_bh_enable(void)
{
_local_bh_enable_ip((unsigned long)__builtin_return_address(0));
}
static inline void _local_bh_enable_ip(unsigned long ip)
{
……
if (unlikely(!in_interrupt() && local_softirq_pending()))
do_softirq();
……
}
D、在SMP中,當CPU處理完被CALL_FUNCTION_VECTOR處理器間中斷所觸發的函式時:
唔。對多核中CPU的之間的通訊不熟。不太清楚這個機制……
3、do_softirq
不論是哪種呼叫方式,終於都會觸發到軟中斷的核心處理函式do_softirq,它處理當前CPU上的全部軟中斷。
核心將軟中斷設計儘量與平臺無關。可是在某些情況下。它們還是會有差異,先來看一個x86 32位的do_softirq版本號:
asmlinkage void do_softirq(void)
{
unsigned long flags;
struct thread_info *curctx;
union irq_ctx *irqctx;
u32 *isp;
//軟中斷不能在中斷上下文內巢狀呼叫。中斷處理程式或下半部採用的是"啟用"方式。
if (in_interrupt())
return;
//禁止中斷。儲存中斷標誌
local_irq_save(flags);
//核心使用一個CPU點陣圖,確實幾個軟中斷能夠同一時候在不同的CPU上執行,包含同樣的軟中斷。比如,
//NET_RX_SOFTIRQ能夠同一時候跑在多個處理器上。
//local_softirq_pending用於確定當前CPU的全部點陣圖是否被設定。
即是否有軟中斷等待處理。
//回憶一下常常發生的網路卡接收資料處理:當網路卡中斷落在哪一個CPU上時,與之對應的軟中斷函式就會在其上執行。 //從這裡來看,實質就是哪個網路卡中斷落在對應的CPU上,CPU置其軟中斷點陣圖,這裡做對應的檢測(這裡local_softirq_pending僅僅 //是一個總的推斷。後面還有按位的推斷)。檢測到有對應的位,執行之 if (local_softirq_pending()) { //取得執行緒描寫敘述符 curctx = current_thread_info(); //構造中斷上下文結構,softirq_ctx是每一個CPU的軟中斷上下文 //static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx); //這裡先取得當前CPU的軟中斷上下文,然後為其賦初始值——儲存當前程式和棧指標 irqctx = __get_cpu_var(softirq_ctx); irqctx->tinfo.task = curctx->task; irqctx->tinfo.previous_esp = current_stack_pointer; /* build the stack frame on the softirq stack */ //構造中斷棧幀 isp = (u32 *) ((char *)irqctx + sizeof(*irqctx)); //call_on_stack切換核心棧,並在中斷上下文上執行函式__do_softirq call_on_stack(__do_softirq, isp); /* * Shouldnt happen, we returned above if in_interrupt(): */ WARN_ON_ONCE(softirq_count()); } //恢復之 local_irq_restore(flags); }
當配置了CONFIG_4KSTACKS。每一個程式的thread_union僅僅有4K,而非8K。發生中斷時,核心棧將不使用程式的核心棧。而使用每一個 cpu的中斷請求棧。
核心棧將使用每一個 cpu的中斷請求棧。而非程式的核心棧來執行軟中斷函式:
static void call_on_stack(void *func, void *stack)
{
asm volatile("xchgl %%ebx,%%esp \n" //交換棧指標,中斷棧幀的指標stack做為傳入引數(%ebx)。交換後esp是irq_ctx的棧頂,ebx是程式核心棧的棧
"call *%%edi \n" //呼叫軟中斷函式
"movl %%ebx,%%esp \n" //恢復之,直接使用movl,而非xchgl是由於函式執行完成,中斷的棧幀指標已經沒實用處了
: "=b" (stack)
: "0" (stack),
"D"(func)
: "memory", "cc", "edx", "ecx", "eax");
}
PS:全部的這些執行,應該都是在定義4K棧的基礎上的:
#ifdef CONFIG_4KSTACKS
/*
* per-CPU IRQ handling contexts (thread information and stack)
*/
union irq_ctx {
struct thread_info tinfo;
u32 stack[THREAD_SIZE/sizeof(u32)];
} __attribute__((aligned(PAGE_SIZE)));
static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
……
static void call_on_stack(void *func, void *stack)
……
是的,這個版本號相對複雜,可是假設看了複雜的,再來看簡單的,就easy多了,當平臺未定義do_softirq函式時(__ARCH_HAS_DO_SOFTIRQ)。
核心提供了一個通用的:
#ifndef __ARCH_HAS_DO_SOFTIRQ
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);
}
#endif
無需很多其它的解釋,它很的簡潔。
不論是哪個版本號,都將呼叫__do_softirq函式:
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
//儲存點陣圖
pending = local_softirq_pending();
//程式記帳
account_system_vtime(current);
//關閉本地CPU下半部。
為了保證同一個CPU上的軟中斷以序列方式執行。 __local_bh_disable((unsigned long)__builtin_return_address(0)); lockdep_softirq_enter(); //獲取本地CPU cpu = smp_processor_id(); restart: /* Reset the pending bitmask before enabling irqs */ //清除點陣圖 set_softirq_pending(0); //鎖中斷,僅僅是為了保持點陣圖的相互排斥,點陣圖處理完成。後面的程式碼能夠直接使用儲存的pending, //而中斷處理程式在啟用的時候,也能夠放心地使用irq_stat.__softirq_pending。
//所以。能夠開中斷了 local_irq_enable(); //取得軟中斷向量 h = softirq_vec; //迴圈處理全部的軟中斷 do { //逐步取點陣圖的每一位。推斷該位上是否有軟中斷被設定。
若有,處理之 if (pending & 1) { //儲存搶佔計數器 int prev_count = preempt_count(); kstat_incr_softirqs_this_cpu(h - softirq_vec); trace_softirq_entry(h, softirq_vec); //呼叫軟中斷 h->action(h); trace_softirq_exit(h, softirq_vec); //推斷軟中斷是否被搶佔,假設是,則輸出一段錯誤資訊 if (unlikely(prev_count != preempt_count())) { printk(KERN_ERR "huh, entered softirq %td %s %p" "with preempt_count %08x," " exited with %08x?\n", h - softirq_vec, softirq_to_name[h - softirq_vec], h->action, prev_count, preempt_count()); preempt_count() = prev_count; } //??qsctr,這個是啥東東 rcu_bh_qsctr_inc(cpu); } //指向下一個軟中斷槽位 h++; //移位,取下一個軟中斷位 pending >>= 1; } while (pending); //當軟中斷處理完成後,由於前面已經開了中斷了。所以有可能新的軟中斷已經又被設定。 //軟中斷排程程式會嘗試又一次軟中斷。其最大重新啟動次數由max_restart決定。 //所以,這裡必須再次關閉中斷。再來一次…… local_irq_disable(); //取點陣圖 pending = local_softirq_pending(); //有軟中斷被設定,且沒有超過最大重新啟動次數,再來一次先 if (pending && --max_restart) goto restart; //超過最大重新啟動次數。還有軟中斷待處理。呼叫wakeup_softirqd。其任處是喚醒軟中斷守護程式ksoftirqd。 if (pending) wakeup_softirqd(); lockdep_softirq_exit(); account_system_vtime(current); //恢復下半部 _local_bh_enable(); }
中斷跟蹤
假設中斷跟蹤CONFIG_TRACE_IRQFLAGS被定義。lockdep_softirq_enter/lockdep_softirq_exit用於遞增/遞減當前程式的軟中斷上下文計數器softirq_context:
# define lockdep_softirq_enter() do { current->softirq_context++; } while (0)
# define lockdep_softirq_exit() do { current->softirq_context--; } while (0)
複製程式碼
trace_softirq_entry與trace_softirq_exit配合使用。能夠用於推斷軟中斷的延遲。
好像軟中斷不太難,沒有很多其它的內容了。歡迎大家回貼補充。