Linux核心軟中斷

令狐蔥dennis 發表於 2021-05-04

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 關於核心棧和中斷棧的說明