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,
- const charchar *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微線程
- 採用工作隊列
- 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函數的實現原理和過程,因爲只有瞭解該過程,才能充分理解軟中斷的工作原理。
- struct softirq_action
- {
- void (*action)(struct softirq_action *); //函數指針名爲action,其中參數類型爲一個
- };
- void open_softirq(int nr, void (*action)(struct softirq_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是全局的。),我們就可以使用軟中斷了,一般怎麼使用軟中斷呢?
- 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)
- {
- 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) { //將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);
- }
軟中斷是將操作推遲到將來某一個時刻執行的最有效的方法。由於該延遲機制處理複雜,多個處理器可以同時並且獨立得處理(即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處理函數的要求。
- struct tasklet_struct
- {
- struct tasklet_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
- {
- struct tasklet_struct *head;
- struct tasklet_struct **tail;
- };
- 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。
這行代碼很簡單,就是將tail指針指向新的tasklet_struct的next地址。
瞭解了上面的添加tasklet_struct對象之後,下面我們就可以分析tasklet的工作調用機制了。我們知道每一個軟中斷類型都會對應一個action(軟中斷處理程序),所以tasklet類型的軟中斷同樣也有其唯一對應的action(一般其他類型軟中斷的action都是由用戶自己編寫,但是tasklet不一樣,Linux設計師已經幫我們實現了。所以也是因爲這樣,tasklet被廣泛應用於驅動程序中。)
- static void tasklet_action(struct softirq_action *a)
- {
- struct tasklet_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) {
- struct tasklet_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函數的源碼:
- static inline void tasklet_schedule(struct tasklet_struct *t)
- {
- if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
- __tasklet_schedule(t); //調用_tasklet_schedule函數
- }
- void __tasklet_schedule(struct tasklet_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;處於未激活狀態
- static struct tasklet_struct my_tasklet;
- tasklet_init(&my_tasklet, tasklet_handler, 0); //count = 0,處於激活狀態。
- void tasklet_init(struct tasklet_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;
- }