標籤:
非強制中斷概念在嵌入式開發可以有兩個不同的解釋:
其一,非強制中斷在處理器設計中是處理器異常之一,程式軟體使用指定指令(如arm的SWI指令)引發該異常從而陷入核心態執行,最典型的軟體應用就是系統調用。
其二,在kernel代碼中實現了一套非強制中斷機制,區別於硬體中斷的硬體觸發軟體處理,而是軟體觸發軟體處理。
今天來學習的是kernel下的非強制中斷機制,
學習最常用的硬體中斷,我們最關心的是中斷觸發(硬體)-中斷分發-中斷處理這個流程如何完成,對於非強制中斷我們也需要搞明白這幾點。
首先來看下kernel中跟非強制中斷相關的結構體變數,在kernel/softirq.c中,如下:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;softirq_vec數組表示kernel下所有註冊的非強制中斷。struct softirq_action{ void (*action)(struct softirq_action *);};數組下標表示非強制中斷號,softirq_action則代表非強制中斷處理函數。softirq_vec類似於kernel下表徵硬體中斷的irq_desc。/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high frequency threaded job scheduling. For almost all the purposes tasklets are more than enough. F.e. all serial device BHs et al. should be converted to tasklets, not to 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};kernel下定義了10種非強制中斷,其中包括應用於定時器 tasklet 網路收發(NAPI) 塊裝置讀寫等。
根據注釋可以看出,kernel並不希望開發人員來添加新的softirq,如果有需求,可以利用由softirq實現的tasklet實現需求。
下面來看softirq的註冊 觸發 和 分發處理
1 非強制中斷註冊
softirq註冊使用函數open_softirq,類似於硬體中斷的register_irq。定義在kernel/softirq.c中
void open_softirq(int nr, void (*action)(struct softirq_action *)){ softirq_vec[nr].action = action;}很簡單,添加softirq_vec對應下標的處理函數即可。
在kernel啟動start_kernel中就註冊了定時器的softirq,如下:
void __init init_timers(void){ int err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE, (void *)(long)smp_processor_id()); init_timer_stats(); BUG_ON(err != NOTIFY_OK); register_cpu_notifier(&timers_nb); open_softirq(TIMER_SOFTIRQ, run_timer_softirq);}定時器處理函數為run_timer_softirq,後面學習定時器實現還會分析。
2 非強制中斷觸發
區別於硬體中斷由硬體觸發,非強制中斷由軟體觸發,由函數raise_softirq和raise_softirq_irqoff觸發,實現如下:
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();}void raise_softirq(unsigned int nr){ unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags);}void __raise_softirq_irqoff(unsigned int nr){ trace_softirq_raise(nr); or_softirq_pending(1UL << nr);}2個函數區別在於raise_softirq在觸發softirq時會屏蔽irq。
觸發softirq都是調用or_softirq_pending,將核心變數__softirq_pending的nr位置位。
所以__softirq_pending變數類似於硬體中斷中intc的中斷狀態寄存器。我們可以稱之為非強制中斷狀態變數!
3 非強制中斷的分發處理
硬體中斷是非同步於處理器,非同步於軟體程式的。硬體中斷觸發-中斷異常入口-中斷分發處理,硬體中斷分發處理時機取決於硬體中斷的時機,軟體不需要控制。
而非強制中斷不能非同步觸發調用,非強制中斷的分發處理函數是do_softirq,分發處理時機主要有3種:
(1)在處理完一個硬體中斷以後。
(2)在ksoftirqd核心線程中。
(3)在那些顯式檢查和執行待處理的非強制中斷的代碼中,如網路子系統中(顯式調用do_softirq來收發資料包)。
我們主要來看前2種情況
(1) 硬體中斷處理函數退出時
中斷處理函數退出時會調用irq_exit,在kernel/softirq.c中如下:
static inline void invoke_softirq(void){ if (!force_irqthreads) {#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED __do_softirq();#else do_softirq();#endif } else { __local_bh_disable((unsigned long)__builtin_return_address(0), SOFTIRQ_OFFSET); wakeup_softirqd(); __local_bh_enable(SOFTIRQ_OFFSET); }}/* * Exit an interrupt context. Process softirqs if needed and possible: */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();#ifdef CONFIG_NO_HZ /* Make sure that timer wheel updates are propagated */ if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched()) tick_nohz_irq_exit();#endif rcu_irq_exit(); sched_preempt_enable_no_resched();}檢查__softirq_pending是否有置位,有則invoke_softirq調用do_softirq。
(2) ksoftirqd核心線程
該核心線程的實現也在kernel/softirq.c中,這裡不貼代碼了。
根據ksoftirqd線程實現可以看出,當核心中出現大量非強制中斷的時候,核心進程ksoftirqd就會調用do_softirq處理它們。
非強制中斷分發處理時機清楚了,下面來看do_softirq如何處理,在kernel/softirq.c中:
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);}local_softirq_pending擷取__softirq_pending非強制中斷狀態變數,如果有softirq的pending則調用__do_softirq,如下:
#define MAX_SOFTIRQ_RESTART 10asmlinkage 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); __local_bh_disable((unsigned long)__builtin_return_address(0), SOFTIRQ_OFFSET); lockdep_softirq_enter(); cpu = smp_processor_id();restart: /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0); local_irq_enable(); h = softirq_vec; do { if (pending & 1) { unsigned int vec_nr = h - softirq_vec; int 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())) { printk(KERN_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() = prev_count; } rcu_bh_qs(cpu); } h++; pending >>= 1; } while (pending); local_irq_disable(); pending = local_softirq_pending(); if (pending && --max_restart) goto restart; if (pending) wakeup_softirqd(); lockdep_softirq_exit(); account_system_vtime(current); __local_bh_enable(SOFTIRQ_OFFSET);}__do_softirq處理softirq之前擷取並清空__softirq_pending,然後根據pending遍曆softirq_vec,調用對應pending的action處理函數。
遍曆完成後,會再次擷取__softirq_pending,如果在遍曆softirq_vec期間又產生了softirq,__do_softirq會跳轉到restart再次遍曆處理softirq。
最多反覆處理MAX_SOFTIRQ_RESTART次。
如果還有softirq的pending,則喚醒ksoftirqd核心線程來處理剩餘的softirq。
非強制中斷的註冊 觸發 處理就是這樣,非強制中斷是kernel實現的純軟體機制,依託於中斷退出和核心線程時機來做分發處理。
基於非強制中斷,kernel實現了定時器 tasklet,下面來重點來看下核心定時器的實現原理。
一 核心定時器
核心定時器是通過核心時鐘系統和非強制中斷結合實現的。
在driver中對於定時器的使用,一般是這樣:
static struct timer_list test_timer; init_timer(&test_timer); test_timer.function = (void *)test_handle_function; test_timer.data = (u32) test; test_timer.expires = CHECK_TIME + jiffies; add_timer(&test_timer);
該段代碼初始化timer_list各個成員,timer_list代表一個定時器對象,最後將該timer_list添加到核心定時器鏈表tvec_bases中。
下面來看下核心如何?的定時器。
1 註冊定時器非強制中斷
前面非強制中斷中講過,start_kernel中init_timers註冊了定時器非強制中斷處理函數run_timer_softirq,非強制中斷號是TIMER_SOFTIRQ。
2 觸發定時器非強制中斷
定時器非強制中斷的觸發是在kernel的時鐘中斷中,根據前一篇博文《linux kernel時鐘系統的前世今生》連結地址:
對於tickless系統,中斷處理函數是tick_nohz_handler,在kernel/time/tick-sched.c中,該函數除了更新xtime以及設定下次觸發時間外,還會調用update_process_times。
該函數調用了run_local_timers,如下
void run_local_timers(void){ hrtimer_run_queues(); raise_softirq(TIMER_SOFTIRQ);}其中調用raise_softirq觸發了定時器非強制中斷。
3 處理定時器非強制中斷
根據前面處理非強制中斷的講解,硬體中斷退出或ksoftirqd核心線程中do_softirq遍曆softirq_vec。
因觸發了定時器非強制中斷,所以遍曆softirq_vec會調用softirq_action[TIMER_SOFTIRQ].action,就是run_timer_softirq。
run_timer_softirq遍曆tvec_bases中所有定時器timer_list,對比timer_list.expires與當前的jiffies,如果一致,說明定時到期,調用timer_list.function。
核心定時器的實作類別似於硬體中斷的二級中斷,根據intc的中斷狀態寄存器分發中斷後,我們還會根據模組內的中斷狀態寄存器來確定模組內具體引起中斷的子模組。
核心定時器是在時鐘中斷中觸發定時器非強制中斷,在所有硬體中斷退出時分發處理定時器非強制中斷,判斷時間到期,則執行定時器處理函數,最後清除掉定時器非強制中斷。
二 tasklet
1 註冊tasklet非強制中斷
在start_kernel中softirq_init中註冊了tasklet非強制中斷處理函數:
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
2 註冊tasklet
可以使用<linux/interrupt.h>中定義的兩個宏中的一個DECLARE_TASKLET或 DECLARE_TASKLET_DISABLED來靜態建立tasklet,前者把建立的tasklet的引用計數器設定為0,該tasklet處於啟用狀態。另一個把引入計數器設為1,所以該tasklet處于禁止狀態。
還可以使用tasklet_init()動態建立一個tasklet。
3 觸發和處理tasklet非強制中斷
觸發tasklet非強制中斷的操作通過tasklet_schedule()函數間接調用raise_softirq()函數來實現。
此後則等待任何的硬體中斷處理函數退出後調用do_softirq來執行TASKLET_SOFTIRQ非強制中斷的處理函數tasklet_action。
tasket_action中遍曆tasklet_vec鏈表,對於enable的tasklet執行其處理函數。
核心定時器 tasklet實現都類似於硬體中斷的二級中斷。
由於定時器 tasklet都會在硬體中斷退出的時機去檢查執行處理,所以會應用於中斷底半部的處理。
中斷處理要求儘可能的簡潔迅速,因此對於時間要求不是很緊迫,允許稍後執行的部分操作可以放在tasklet和定時器處理函數中進行執行。
linux kernel非強制中斷及其衍生品-定時器 tasklet的實現