1 軟中斷概述
軟中斷是實現中斷下半部的一種手段,與2.5以前版本的下半段機制不同。軟中斷可以同時執行在不同的CPU上。
1.1 軟中斷的表示
核心中用結構體softirq_action表示一個軟中斷。軟中斷是一組靜態定義的介面,有32個。但是核心(2.6.34)中只實現了10個。可用的軟中斷的個數用NR_SOFTIRQ表示,NR_SOFTIRQ=10,軟中斷的型別用一個列舉體表示。這裡需要注意的是,32個軟中斷體現在位掩碼是unsigned int 型別。
static struct softirq_action softirq_vec[NR_SOFTIRQS] ;
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
};
struct softirq_action
{
void (*action)(struct softirq_action *);
};
2 軟中斷相關的資料結構
2.1 thread_info的preempt_count欄位
preempt_count 是一個32位的int型,共分為5個欄位
巨集in_interrupt檢測軟中斷計數器 硬中斷計數器 和 NMI掩碼,只要這三個欄位任意一個欄位不為0,就表示程式處於中斷上下文。
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
2.2 pending位掩碼
每個CPU上都有一個irq_stat結構,irq_stat中的__softirq_pending是一個32位的掩碼,為1表示該軟中斷已經啟用,正等待處理。為0表示軟中斷被禁止。在do_irq中被使用。
核心使用local_softirq_pending得到當前CPU上的位掩碼
#define local_softirq_pending() percpu_read(irq_stat.__softirq_pending)
#define set_softirq_pending(x) percpu_write(irq_stat.__softirq_pending, (x))
#define or_softirq_pending(x) percpu_or(irq_stat.__softirq_pending, (x))
irq_cpustat_t irq_stat[NR_CPUS]
typedef struct {
...
unsigned int __softirq_pending;
...
}irq_cpustat_t;
2.3 軟中斷棧
程式的核心棧的大小根據編譯時選項不同,可以是4K或者8K。如果是8K堆疊,中斷,異常和軟中斷(softirq) 共享這個堆疊。如果選擇4K堆疊,則核心堆疊 硬中斷堆疊 軟中斷堆疊各自使用一個4K空間。關於軟中斷堆疊,後面在軟中斷處理時再詳細說明。
#ifdef CONFIG_4KSTACKS
static DEFINE_PER_CPU_PAGE_ALIGNED(union irq_ctx, softirq_stack);
union irq_ctx {
struct thread_info tinfo;
u32 stack[THREAD_SIZE/sizeof(u32)];
} __attribute__((aligned(PAGE_SIZE)));
3 軟中斷的初始化
核心使用open_softirq初始化一個軟中斷,nr是代表軟中斷型別的常量,action指向一個軟中斷處理函式
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
4 軟中斷的觸發(raise softirq)
觸發就是將位掩碼pending的相應位 置1的過程。核心使用raise_softirq完成觸發軟中斷,nr是要觸發的軟中斷型別。值的注意的是,中斷的觸發 發生關閉硬中斷的情況下。
觸發軟中斷的過程中,如果該程式未處於中斷上下文,說明當前程式處於程式上下文中,那麼我們直接呼叫wakeup_softirqd排程ksoftirqd即可。
反之,如果當前處於中斷上下文中或軟中斷被禁止使用,那麼就不必排程核心執行緒,在中斷處理後期irq_exit中,會呼叫invoke_softirq()處理軟中斷。
實際的工作是交給or_softirq_pending(1UL << (nr)); 完成的,該函式通過位操作將指定為和pending相加。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
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.
* 如果不在中斷上下文中
*/
if (!in_interrupt())
wakeup_softirqd();
}
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); }
5. 軟中斷的處理
5.1 處理軟中斷的時機
1 在do_irq的末期(irq_exit)
如果當前程式沒有處於中斷上下文中並且本地CPU上還有沒有處理的軟中斷,那麼就呼叫invoke_softirq()處理軟中斷。
#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();
rcu_irq_exit();
#ifdef CONFIG_NO_HZ
/* Make sure that timer wheel updates are propagated */
if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
tick_nohz_stop_sched_tick(0);
#endif
preempt_enable_no_resched();
}
#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq() __do_softirq()
#else
# define invoke_softirq() do_softirq()
#endif
程式5-1 irq_exit
2 當軟中斷被重複觸發超過10次時,核心會呼叫wakeup_softirqd()喚醒核心執行緒ksoftirqd去處理軟中斷。
5.2 軟中斷的處理
1 do_softirq
根據核心堆疊大小,有兩種do_softirq,一種是通用do_irq,另一種是架構相關的do_irq(arch/x86/kernel/irq_32.c)。
通用的do_irq的工作流程
1 判斷當前是否處於硬中斷上下文中或者軟中斷被禁用,如果是那麼直接返回
2 儲存Eflags 然後關閉本地硬體中斷
3 獲取本地CPU的位掩碼pending 如果有待處理的軟中斷就呼叫__do_irq
4 從__do_irq返回恢復Eflags
asmlinkage void do_softirq(void)
{
//用來儲存位掩碼的區域性變數
__u32 pending;
//儲存Eflags暫存器的區域性變數
unsigned long flags;
//如果do_softirq在中斷上下文中被呼叫 或 軟中斷被禁止使用 那麼不處理軟中斷
//直接返回
if (in_interrupt())
return;
//將Eflags儲存到flags中 然後關硬體中斷
local_irq_save(flags);
//獲取本地CPU上的位掩碼
pending = local_softirq_pending();
if (pending)
__do_softirq();
//將flags
local_irq_restore(flags);
}
x86_32架構下使用4K軟中斷堆疊的do_softirq處理流程
1 類似於通用的do_softirq,如果在中斷上下文中或者軟中斷被禁止使用就立即返回。然後關外部中斷
2 如果本地CPU上存在待處理的軟中斷就開始對軟中斷堆疊的處理,關鍵是令isp指向軟中斷堆疊的棧底。然後在軟中斷棧上呼叫call_on_stack。call_on_stack是一段內聯彙編,其主要目的是完成從核心棧到軟中斷棧的切換。先將esp儲存到ebx中,使用call指令跳轉到__do_softirq子例程,子例程返回時再恢復esp。
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);
if (local_softirq_pending()) {
//curctx指向當前程式的thread_info結構
curctx = current_thread_info();
//irqctx包含一個軟中斷堆和thread_info結構
irqctx = __get_cpu_var(softirq_ctx);
//觸發硬中斷和軟中斷是同一個程式所以將threadinfo的task指標統一
irqctx->tinfo.task = curctx->task;
//從核心堆疊切換到軟中斷堆疊 需要儲存核心堆疊的棧指標暫存器內容
irqctx->tinfo.previous_esp = current_stack_pointer;
/* build the stack frame on the softirq stack */
//isp指向軟中斷棧底
isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
//在軟中斷堆疊上呼叫__do_softirq
call_on_stack(__do_softirq, isp);
}
local_irq_restore(flags);
}
static void call_on_stack(void *func, void *stack)
{
//call *%%edi 間接絕對近呼叫 偏移地址儲存在edi暫存器中
//指令執行時 先將eip入棧 然後將edi-->eip
//先交換ebx and esp
//然後呼叫__do_softirq
//然後將ebx-->esp 恢復xchgl之前的esp
//輸出約束將ebx --> stack
asm volatile("xchgl %%ebx,%%esp \n"
"call *%%edi \n"
"movl %%ebx,%%esp \n"
: "=b" (stack)
: "0" (stack),
"D"(func)
: "memory", "cc", "edx", "ecx", "eax");
}
2 __do_softirq
軟中斷的處理實際上是由 __do_softirq完成,整體的思路是遍歷pending,如果某一位不為空表示本地CPU上有待處理的軟中斷,然出呼叫軟中斷的處理函式。
開始處理軟中斷前,核心要呼叫__local_bh_disable(通過將preempt_count的軟中斷計數器加1)關閉下半部。如前所說,處理軟中斷的時機不止一個,核心要保證在本地CPU上軟中斷的處理是序列的。
另外在處理軟中斷的迴圈結束時,核心還要檢測是否有重複觸發的軟中斷。先呼叫local_softirq_pending()獲取位掩碼pending,然後根據pending繼續處理軟中斷,不過這種重複處理不能超過10次(MAX_SOFTIRQ_RESTART),一旦超過10次,核心就會喚醒ksoftirqd
#define MAX_SOFTIRQ_RESTART 10
asmlinkage void __do_softirq(void)
{
// softirq_action表示一個軟中斷
struct softirq_action *h;
// 區域性變數pending 儲存待處理軟中斷點陣圖
__u32 pending;
// 軟中斷的重啟次數
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
//獲取本地CPU上所有待處理的軟中斷
pending = local_softirq_pending();
account_system_vtime(current);
//關閉下半部中斷
__local_bh_disable((unsigned long)__builtin_return_address(0));
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
//pending已經儲存了所有帶出軟中斷的狀態 所以將pending bitmask clear
set_softirq_pending(0);
// 開中斷
local_irq_enable();
//h指向第一類軟中斷
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;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
//關閉本地CPU上的硬中斷
local_irq_disable();
//獲取位掩碼
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
lockdep_softirq_exit();
account_system_vtime(current);
//將軟中斷計數器加1,啟用軟中斷
_local_bh_enable();
}
6 ksoftirqd核心執行緒
6.1 ksoftirqd
在核心處理類似NET_RX_SOFTIRQ的軟中斷時,如果有大量等待處理的資料包。就會不斷的呼叫
__raise_softirq_irqoff(NET_RX_SOFTIRQ)重複觸發軟中斷NET_RX_SOFTIRQ。這樣做會導致使用者程式的” 飢餓問題 “ (長時間無法獲得CPU)。
針對這種問題核心使用核心執行緒ksoftirqd去處理自行觸發次數超過10次的軟中斷。
6.2 ksoftirqd的實現
static int ksoftirqd(void * __bind_cpu)
{
//ksoftirqd的優先順序最低(nice = 19)
set_user_nice(current, 19);
//將ksoftirqd設定為不可凍結
current->flags |= PF_NOFREEZE;
//設定ksoftirqd為可中斷狀態
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
//如果沒有待處理的軟中斷則 排程別的程式
if (!local_softirq_pending())
schedule();
__set_current_state(TASK_RUNNING);
while (local_softirq_pending()) {
/* Preempt disable stops cpu going offline.
If already offline, we'll be on wrong CPU:
don't process */
//關閉核心搶佔
preempt_disable();
//處理軟中斷
do_softirq();
//開啟核心搶佔
preempt_enable();
//cond_resched()的目的是提高系統實時性, 主動放棄cpu供優先順序更高的任務使用
cond_resched();
}
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}
程式6-1 ksoftirqd主功能函式
out:
local_irq_enable();
return;
softnet_break:
__get_cpu_var(netdev_rx_stat).time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
程式6-2 net_rx_action
void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __get_cpu_var(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
程式6-3 wakeup_softirqd
還未解決的問題
set_current_state和 __set_current_state的區別
PF_NOFREEZE
cond_resched()
為什麼在do_softirq開始處理軟中斷時要關閉硬體中斷
參考
ULK
http://blog.csdn.net/hardy_2009/article/details/7383729 關於核心棧和中斷棧的說明