Linux核心:中斷、軟中斷、tasklet
http://blog.csdn.net/jansonzhe/article/details/48786207
在之前我所寫的Linux驅動程式中,會經常使用到中斷機制,像CC1100高頻驅動、倒車雷達驅動等等。但所用到的中斷機制都基本上是用到中斷的頂半部,即:編寫中斷處理函式,通過request_irq函式申請中斷,這樣當中斷來臨的時候,就會自動執行中斷處理程式裡面的內容。之所以沒有使用到中斷的底半部,是因為我們這些驅動程式中,中斷處理函式一般都能被很快執行完,同時也不會存在有任何休眠的動作,因此使用中斷的頂半部對於我們這些驅動程式來說,反而相對簡單一些。因此這也就得出,並不是任何中斷程式都一定會使用到中斷的底半部。
中斷頂半部
對於中斷的頂半部,我想大部分的關於Linux驅動的書上都會有詳細的講解,並且這一塊理解和實踐起來都比較容易,但這裡我需要講解的是關於共享中斷的這一部分,因為這一塊可能對於一些初學者會有一點難度。
共享中斷是指多個裝置共享一根中斷線(中斷線在這裡可以理解為中斷號,也就是說多個裝置共享一箇中斷號),為什麼會有這種情況發生,因為在Linux核心中,中斷線的數目是有限的,如果每一個裝置都使用一根中斷線的話,中斷線肯定是不夠的,所以聰明的Linux核心設計師們就提出了共享中斷這一理念。這裡理念的主要目的就是可以在一根中斷線上搭載多箇中斷裝置。那麼好了,現在問題也來了,既然都在同一個中斷線上,如果中斷來了的話,要如何判斷該中斷來自於哪一個裝置呢?其實對於Linux核心來說,要判斷其來自哪一個裝置,其需要做兩步工作。當一箇中斷來臨時,Linux核心會遍歷該中斷線上所有註冊了的中斷處理程式,在該中斷處理程式中,就會迅速判斷到底是來自於哪一個硬體裝置。而在中斷程式中如何來判斷呢?這就需要相應產生中斷的硬體裝置來支援了。例如可能中斷處理程式會檢查一下該處理程式對應的硬體裝置的某一暫存器的狀態來判斷是否該裝置發生了中斷,如果是該裝置發出的中斷,就執行接下來的處理函式。如果不是,就立即返回(應該返回IRQ_RETVAL(IRQ_NONE)
首先我們來看一下在申請共享中斷的過程與一般申請中斷有哪些不同。
我們知道申請註冊中斷的函式是:
- request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
- constcharchar *name, voidvoid *dev)
- {
- return request_threaded_irq(irq, handler, NULL, flags, name, dev);
- }
- 如果我們要申請共享中斷函式的話,flag標誌位必須還要指定一個IRQF_SHARED(即flag再“ | ”上一個IRQF_SHARED),注意,該中斷線的每一箇中斷裝置在申請中斷的時候都必須要加上該標誌位。
- 對於每一個註冊的中斷處理程式來說,最後一個引數dev必須是唯一的(這是共享中斷所明確要求的)。為了確保dev引數值是唯一的。可以將dev引數的值設為指向申請中斷函式的裝置結構體指標即可(我們的CC1101和倒車雷達驅動都是這麼幹的)。而且由於中斷處理函式可能會用到裝置結構體的資料,因此這是一個一箭雙鵰的方法。對於共享中斷處理程式,dev的引數值不能為NULL。
中斷上下文
在這裡順便提一下中斷上下文,當我們執行一箇中斷處理函式時,核心就會處於中斷上下文(Interrupt Context)中。與程序上下文不同,中斷上下文與程序並沒有什麼關係。與current巨集也沒有任何關係,儘管此時若使用的current標誌位的話,其任然是指向被中斷的程序。由於中斷上下文不依賴與程序,因此中斷上下文不能休眠,不能在中斷上下文中呼叫某些可能引起休眠的函式。
由於中斷上下文可以打斷其他正在執行的程式碼,因此,中斷上下文在執行時間上由嚴格的時間限制。中斷上下文中的程式碼需要儘可能簡潔,儘量不要使用迴圈或者是耗時比較長的函式來處理中斷任務。這是由於中斷上下文已經打斷了其他正在執行的程式碼,甚至可能是其他的中斷處理程式,因此中斷處理程式應該快速地執行完,否則可能會使其他被打斷的程式長時間等待而造成系統性能下降甚至崩潰。當然,在中斷上下文中處理複雜耗時的任務也在所難免,但最好將這部分任務放在中斷的底半部(主要因為中斷的底半部,可以被其他甚至是同類型的中斷打斷,並且中斷底半部函式是非同步執行。)。這樣既可以很快地執行完中斷處理程式(儘快回覆被中斷的程式碼),又可以在中斷程式中完成很複雜的任務。後面的軟中斷或者是tasklet都屬於中斷的上下文中。
在Linux2.6核心中,中斷處理程式擁有自己的棧,每一個處理器一個,大小為一頁(4KB),儘管中斷棧並不算大,但平均可用棧空間要比Linux核心的其他程式大得多。因為中斷程式把這一頁據為己有。在我們編寫中斷處理程式時,並不需要關心如何設定中斷棧或核心棧的大小,總之,儘量節約中斷棧的空間就行了。
中斷的底半部
下面我們來開始講解中斷底半部,如果用一個詞來形容底半部的功能,就是“延遲執行”,為什麼要這樣說呢,後面分析過後就會深刻理解這一點了。在中斷的上半部,即中斷處理程式結束前,當前的中斷線在所有的處理器上都會被遮蔽,如果在申請中斷線時使用了IRQF_DISABLED,那麼情況會更加糟糕,在中斷處理程式執行時會禁止所有的本地中斷。因此儘可能地縮短中斷被遮蔽的時間對系統的響應能力和效能都至關重要。因此,要將耗時較長的任務放到底半部延遲執行。因為底半部並不禁止其他中斷上半部的執行(哪怕是自己的中斷處理函式)。對於中斷底半部的實現方式一共有三種;
- 採用軟中斷的方式
- 採用tasklet微執行緒
- 採用工作佇列
我們可以看到,所有的中斷線上的中斷都需要經過do_IRQ函式,執行該函式,需要一個irq的引數,也就是中斷號。然後do_IRQ函式便會輪詢得執行該中斷線上的所以中斷處理程式,該實現方式主要是在do_IRQ函式中呼叫handle_IRQ_event函式實現。handle_IRQ_event會執行irq所在的中斷線上的所有的中斷處理函式。好了,現在如果我們執行完了handle_IRQ_event函式後,我們會在do_IRQ中呼叫irq_exit函式,該函式才是負責呼叫和處理待決的軟中斷。首先我們來看一下這個函式的原始碼:
- void irq_exit(void)
- {
- account_system_vtime(current);
- trace_hardirq_exit();
- sub_preempt_count(IRQ_EXIT_OFFSET);
- if (!in_interrupt() && local_softirq_pending())
- //判斷是否有軟中斷被請求,主要是看是否有執行raise_softirq函式,
- invoke_softirq(); //用於喚醒軟中斷,即會啟用do_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();
- }
下面我將詳細介紹有關do_softirq函式的實現原理和過程,因為只有瞭解該過程,才能充分理解軟中斷的工作原理。 軟中斷由一個softirq_action結構體表示,在該結構體中只定義了一個函式指標
- struct softirq_action
- {
- void (*action)(structsoftirq_action *); //函式指標名為action,其中引數型別為一個
- };
- void open_softirq(int nr, void (*action)(structsoftirq_action *))
- {
- softirq_vec[nr].action = action; //指定軟中斷處理函式指標。
- }
- enum
- {
- HI_SOFTIRQ=0, //優先順序最高的軟中斷,用於tasklet
- TIMER_SOFTIRQ,
- NET_TX_SOFTIRQ, //傳送網路資料的軟中斷
- NET_RX_SOFTIRQ,
- BLOCK_SOFTIRQ,
- BLOCK_IOPOLL_SOFTIRQ,
- TASKLET_SOFTIRQ, //tasklet軟中斷
- SCHED_SOFTIRQ,
- HRTIMER_SOFTIRQ,
- RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
- NR_SOFTIRQS //該列舉值就是當前Linux核心允許註冊的最大軟中斷數
- };
當我們編寫了含有softirq_action引數的軟中斷處理程式,並且通過open_softirq函式註冊完之後(open_softirq函式的功能其實很簡單,就是根據nr指定的軟中斷型別定位當前驅動的softirq_action陣列的相應元素,然後將action指定的軟中斷處理函式的指標賦給softirq_vec[nr].action,注意這裡的softirq_vec是全域性的。),我們就可以使用軟中斷了,一般怎麼使用軟中斷呢? 前面我們知道,如果在還沒有開啟Ksoftirq執行緒的情況下,我們會在中斷頂半部的do_IRQ中呼叫irq_exit函式,而該函式會喚醒軟中斷,即會執行do_softirq函式,所以歸根結底,要想執行軟中斷,必須得要執行do_IRQ函式,但是是不是隻要我們在呼叫do_IRQ函式之前,註冊了軟中斷函式,我們的軟中斷函式就會被執行了呢?答案當然是否定的。因為如果這樣的話,那每一次中斷結束時,所有的軟中斷處理程式都得執行了,不管這個軟中斷處理程式是否跟這個中斷線有關。所以呀,要想do_softirq函式只執行我們想要的軟中斷處理程式的話,應該需要用raise_softirq函式將我們想要執行的軟中斷處理程式掛起。
- void raise_softirq(unsigned int nr)
- {
- unsigned long flags;
- local_irq_save(flags); //儲存中斷狀態,禁止中斷
- raise_softirq_irqoff(nr); //掛起相應的中斷型別
- local_irq_restore(flags); //恢復中斷。
- }
- asmlinkage void do_softirq(void)
- {
- __u32 pending;
- unsigned long flags;
- if (in_interrupt())
- return;
- local_irq_save(flags);
- pending = local_softirq_pending(); //再獲取pending標誌位,是否有軟中斷處理程式被raise
- if (pending)
- __do_softirq(); //如果有,則執行_do_softirq函式
- local_irq_restore(flags);
- }
- asmlinkage void __do_softirq(void)
- {
- structsoftirq_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) { //將pending不同的為與1相"&",來確定哪種型別的軟中斷被掛起了
- unsigned int vec_nr = h - softirq_vec; //獲取softirq_vec陣列的下標值,該下標值也就確定了軟中斷屬於什麼型別了
- int prev_count = preempt_count();
- kstat_incr_softirqs_this_cpu(vec_nr);
- trace_softirq_entry(vec_nr);
- h->action(h); //在這裡執行軟中斷的處理函式action
- trace_softirq_exit(vec_nr); //執行完該軟中斷處理程式之後,就應該將掛起的標誌位重新置0,將相應的_softirq_pending
- 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) //重新獲取的pending,如果其不為0,說明又有新的軟中斷處理程式被掛起,如果待處理的軟中斷程式過多,就應該開啟Ksoftirq執行緒。從而達到延時目的
- wakeup_softirqd(); //開啟Ksoftirq執行緒(軟中斷處理執行緒),即將Ksoftirq執行緒加入至可執行佇列
- lockdep_softirq_exit();
- account_system_vtime(current);
- __local_bh_enable(SOFTIRQ_OFFSET);
- }
tasklet微執行緒 軟中斷是將操作推遲到將來某一個時刻執行的最有效的方法。由於該延遲機制處理複雜,多個處理器可以同時並且獨立得處理(即do_softirq函式可以被多個CPU同時執行),並且一個軟中斷的處理程式可以在多個CPU上同時執行,因此處理程式必須要被設計為完全可重入和執行緒安全的。此外臨界區必須用自旋鎖保護。由於軟中斷因為這些原因就顯得太過於麻煩,因此引入tasklet機制,就變得很有必要了。tasklet是基於軟中斷實現的,在我們上面講軟中斷的時候知道,tasklet確切的說應該是軟中斷的一個型別,所以根據軟中斷的性質,一個軟中斷型別對應一個軟中斷處理程式action。同理,也可以推出tasklet也會對應於一個唯一的action。可能講到這裡會有讀者覺得,既然一個tasklet型別的軟中斷只對應一個軟中斷處理程式,那麼我可能在一個驅動程式中使用多個tasklet怎麼辦?或者是有多個驅動程式裡面都要使用tasklet又怎麼辦?要回答這個問題,我們就要了解tasklet另一個重要的性質。那就是,每一個CPU都會有自己獨立的tasklet佇列,雖然一個tasklet型別的軟中斷只對應一個action處理程式,但是我們可以在該處理程式中輪詢執行一個tasklet佇列,佇列裡面的每一個tasklet_struct都會對應一個tasklet處理函式,這樣當我們的驅動程式中需要使用到tasklet的時候,只要往這個tasklet佇列加入我們自定義的tasklet_struct物件就可以了。同時,由於每一個CPU都會有一個tasklet佇列,並且每一個CPU只會執行自己tasklet佇列裡面的tasklet_struct物件,因此tasklet並不需要自旋鎖的保護(當然這隻能是對同一個tasklet而言,如果多個不同的tasklet需要使用同一資源的話,仍需要自旋鎖的保護,後面瞭解了tasklet機制之後就會明白這一點),因此這樣就降低了對tasklet處理函式的要求。 對於一個tasklet物件是通過一個tasklet_struct結構體來描述的,該結構體定義在include\linux\interrupt.h檔案中
- struct tasklet_struct
- {
- structtasklet_struct *next; //連結下一個tasklet_struct物件,以構成一個tasklet佇列
- unsigned long state; //該tasklet的執行狀態標誌位
- atomic_t count; //該tasklet被引用的次數標誌位,當count為0時,表示已啟用可用
- void (*func)(unsigned long); //該tasklet的處理函式指標,也是tasklet的核心所在
- unsigned long data; //給上面的處理函式傳的引數。
- };
理解這個tasklet佇列非常有用,這樣我們就可以充分理解tasklet的工作機制了。從上面這個佇列,我們可以看到這個佇列的頭是一個名叫tasklet_vec的tasklet_head結構體,我們來看看tasklet_head結構體體
- struct tasklet_head
- {
- structtasklet_struct *head;
- structtasklet_struct **tail;
- };
通過這兩個示意圖,我們也知道了如何往該佇列增加tasklet_struct物件了,那就是將最後一個tasklet_sturct的next指標指向新加的tasklet_struct物件,同時將列表頭的tail指標的指標指向新加的tasklet_struct結構體的next指標的地址。下面我們看看具體新增的程式碼:
- tasklet_struct * t;
- * _get_cpu_var(tasklet_vec).tail = t;
- _get_cpu_var(tasklet_vec).tail = &(t->next);
將t所指向的地址賦給指標的指標tail,我們知道 *tail 表示的是其指向指標所指向的地址(在這裡有*tail == head 或者是 *tail == tasklet_struct ->next),而對於tail我們知道,其有兩種情況,第一種是指向最後一個tasklet_struct物件的next指標的地址,因此如果我們執行了*_get_cpu_var(tasklet_vec).tail = t;就表示了最後一個tasklet_struct物件的next的指向也發生變化了,即next指向了新的tasklet_struct;如果是第二種情況的話,即tail指向的是head指標的地址,那麼也可以知道執行*_get_cpu_var(tasklet_vec).tail = t;之後,head便會指向新的tasklet_struct結構體t。 第二行程式碼: _get_cpu_var(tasklet_vec).tail = &(t->next);
這行程式碼很簡單,就是將tail指標指向新的tasklet_struct的next地址。
瞭解了上面的新增tasklet_struct物件之後,下面我們就可以分析tasklet的工作呼叫機制了。我們知道每一個軟中斷型別都會對應一個action(軟中斷處理程式),所以tasklet型別的軟中斷同樣也有其唯一對應的action(一般其他型別軟中斷的action都是由使用者自己編寫,但是tasklet不一樣,Linux設計師已經幫我們實現了。所以也是因為這樣,tasklet被廣泛應用於驅動程式中。)
- staticvoid tasklet_action(structsoftirq_action *a)
- {
- structtasklet_struct *list;
- local_irq_disable(); //禁止本地中斷
- list = __this_cpu_read(tasklet_vec.head); //獲取本地中斷的tasklet_vec.head指標的指向
- __this_cpu_write(tasklet_vec.head, NULL); //將tasklet_vec.head賦值為null
- __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head); //將tasklet_vec.tail賦值為head的地址
- local_irq_enable();
- while (list) {
- structtasklet_struct *t = list;
- list = list->next;
- if (tasklet_trylock(t)) { //主要是判斷該tasklet是否處於run狀態,如果處於run狀態的話,就從新將其放入tasklet_vec佇列中
- if (!atomic_read(&t->count)) {
- if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
- BUG();
- t->func(t->data); //執行tasklet的處理函式
- tasklet_unlock(t);
- continue;
- }
- tasklet_unlock(t);
- }
- local_irq_disable();
- t->next = NULL;
- *__this_cpu_read(tasklet_vec.tail) = t; //如果tasklet正在被其他CPU執行,那麼就將該tasklet重新裝入佇列現在再來看這兩行程式碼就應該熟悉了吧
- __this_cpu_write(tasklet_vec.tail, &(t->next));
- __raise_softirq_irqoff(TASKLET_SOFTIRQ); //將tasklet掛起,等待下一次呼叫do_softirq函式的時候,這些加入tasklet佇列的tasklet_struct物件就會被執行。
- local_irq_enable();
- }
- }
接下來我們再來看tasklet一個非常重要的函式,就是tasklet_schedule,這個函式通常用於中斷處理程式中,用於將tasklet_struct加入所在CPU的tasklet佇列,同時將tasklet軟中斷掛起。因為我們知道,在中斷的上半部中的irq_exit函式中,會啟用do_softirq函式,所以在中斷處理程式中使用tasklet_schedule函式就顯得特別必要。下面我們來看一下tasklet_schedule函式的原始碼:
- staticinlinevoid tasklet_schedule(structtasklet_struct *t)
- {
- if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
- __tasklet_schedule(t); //呼叫_tasklet_schedule函式
- }
- void __tasklet_schedule(structtasklet_struct *t)
- {
- unsigned long flags;
- local_irq_save(flags); //禁止本地中斷,因為tasklet_vec是本地CPU的公共資源,在一個程式正在使用時,肯定不能被其他程式同時使用,這樣被導致安全問題。
- t->next = NULL;
- *__this_cpu_read(tasklet_vec.tail) = t;
- __this_cpu_write(tasklet_vec.tail, &(t->next)); //這兩行程式碼很熟悉吧
- raise_softirq_irqoff(TASKLET_SOFTIRQ); //後面當然也很熟悉
- local_irq_restore(flags); //恢復本地中斷
- }
- DECLARE_TASKLET(name, func, data) //count = 0;處於啟用狀態
- DECLARE_TASKLET_DISABLED(name, func, data) //count = 1;處於未啟用狀態
- staticstruct tasklet_struct my_tasklet;
- tasklet_init(&my_tasklet, tasklet_handler, 0); //count = 0,處於啟用狀態。
- void tasklet_init(structtasklet_struct *t,
- void (*func)(unsigned long), unsigned long data)
- {
- t->next = NULL;
- t->state = 0;
- atomic_set(&t->count, 0);
- t->func = func;
- t->data = data;
- }