第7章中斷和中斷處理
7.6 中斷上下文
當執行一箇中斷處理程式時,核心處於中斷上下文中。程序上下文是一種核心所處的操作模式,此時核心代表程序執行——例如,執行系統呼叫或允許核心執行緒。在程序上下文中,可以通過current巨集關聯當前程序。此外,因為程序是以程序上下文的形式連線到核心中的,因此,程序上下文可以睡眠,也可以呼叫排程程式。
中斷上下文和程序沒有瓜葛。與current巨集不相干。因為沒有後備程序,所以中斷上下文不可以睡眠,否則又怎能再對它重新排程呢?因此,不能從中斷上下文中呼叫某些函式。如果一個函式睡眠,就不能在中斷處理程式中使用它——這是對什麼樣的函式可以在中斷處理程式中使用的限制。
中斷上下文具有較為嚴格的時間限制,因為它打斷了其他程式碼。中斷上下文中的程式碼應當迅速、簡潔,儘量不要使用迴圈去處理繁重的工作。中斷處理程式打斷了其他的程式碼。正是因為這種非同步執行的特性,所以所有的中斷處理程式必須儘可能的迅速、簡潔。儘量把工作從中斷處理程式中分離出來,放在下半部執行,因為下半部可以在更合適的時間執行。
中斷處理程式棧的設定是一個配置選項。曾經,中斷處理程式並不具有自己的棧。相反,它們共享所中斷程序的核心棧。核心棧的大小是兩頁,在32位體系結構上是8KB,在64位體系結構上是16KB。因為在這種設定中,中斷處理程式共享別人的堆疊,所以它們在棧中獲取空間時必須非常節約。當然,核心棧本來就很有限,因此,所有的核心程式碼都應該謹慎利用它。
在2.6版本早期的核心中,增加了一個選項,把棧的大小從兩頁減到一頁,也就是在32位的系統上只提供4KB的棧。這就減輕了記憶體的壓力,因為系統中每個程序原先都需要兩頁連續,且不可換出的核心記憶體。為了應對棧大小的減少,中斷處理程式擁有了自己的棧,每個處理器一個,大小為一頁。這個棧稱為中斷棧,儘管中斷棧的大小是原先共享棧的一半,但平均可用棧空間大得多,因為中斷處理程式把這一整頁佔為己有。
中斷處理程式不必關心棧如何設定,或者核心棧的大小是多少。總之,儘量節約核心棧空間。
7.7 中斷處理機制的實現
中斷處理系統在Linux中的實現依賴於體系結構的。實現依賴於處理器、所使用的中斷控制器的型別、體系結構的設計及機器本身。
圖7-1是中斷從硬體到核心的路由。
裝置產生中斷,通過匯流排把電訊號傳送給中斷控制器。如果中斷線是啟用的,那麼中斷控制器就會把中斷髮往處理器。在大多數體系結構中,這個工作就是通過電訊號給處理器的特定管腳傳送一個訊號。除非在處理器上禁止該中斷,否則,處理器會立即停止它正在做的事,關閉中斷系統,然後跳到記憶體中預定義的位置開始執行那裡的程式碼。這個預定義的位置是由核心設定的,是中斷處理程式的入口點。
在核心中,中斷的旅程開始於預定義入口點,這類似於系統呼叫通過預定義的異常控制代碼進入核心。對於每條中斷線,處理器都會跳到對應的一個唯一的位置。這樣,核心就可知道所接收中斷的IRQ號了。初始入口點只是在棧中儲存這個號,並存放當前暫存器的值;然後,核心呼叫函式do_IRQ()。從這裡開始,大多數中斷處理程式碼是用C編寫的——但它們依然與體系結構相關。
do_IRQ()宣告如下:
unsigned int do_IRQ(struct pt_regs *regs);
因為C的呼叫慣例是要把函式引數放在棧的頂部,因此pt_regs結構包含原始暫存器的值,這些值是以前在彙編入口例程中儲存在棧中的。中斷的值也會得以儲存,所以,do_IRQ()可以將它提取出來。
計算出中斷號後,do_IRQ()對所接收的中斷進行應答,禁止這條線上的中斷傳遞。在普通的PC機上,這些操作由mask_and_ack_8259A()來完成的。
接下來,do_IRQ()需要確保在這條中斷線上有一個有效的處理程式,而且這個程式已經啟動,但是當前並沒有執行。如果是這樣的話,do_IRQ()就呼叫handle_IRQ_event()來執行為這條中斷線所安裝的中斷處理程式。handle_IRQ_event()方法被定義在kernel/irq/handle.c中。
/** * handle_IRQ_event - irq action chain handler * @irq: the interrupt number * @action: the interrupt action chain for this irq * * Handles the action chain of an irq event */ irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action) { irqreturn_t ret, retval = IRQ_NONE; unsigned int status = 0;
if (!(action->flags & IRQF_DISABLED)) local_irq_enable_in_hardirq();
do { trace_irq_handler_entry(irq, action); ret = action->handler(irq, action->dev_id); trace_irq_handler_exit(irq, action, ret);
switch (ret) { case IRQ_WAKE_THREAD: /* * Set result to handled so the spurious check * does not trigger. */ ret = IRQ_HANDLED;
/* * Catch drivers which return WAKE_THREAD but * did not set up a thread function */ if (unlikely(!action->thread_fn)) { warn_no_thread(irq, action); break; }
/* * Wake up the handler thread for this * action. In case the thread crashed and was * killed we just pretend that we handled the * interrupt. The hardirq handler above has * disabled the device interrupt, so no irq * storm is lurking. */ if (likely(!test_bit(IRQTF_DIED, &action->thread_flags))) { set_bit(IRQTF_RUNTHREAD, &action->thread_flags); wake_up_process(action->thread); }
/* Fall through to add to randomness */ case IRQ_HANDLED: status |= action->flags; break;
default: break; }
retval |= ret; action = action->next; } while (action);
if (status & IRQF_SAMPLE_RANDOM) add_interrupt_randomness(irq); local_irq_disable();
return retval; }
首先,因為處理器禁止中斷,這裡要把它們開啟,就必須在處理程式註冊期間指定IRQF_DISABLED標誌。IRQF_DISABLED表示處理程式必須在中斷禁止的情況下執行。接下來,每個潛在的處理程式在迴圈中依次執行。如果這條線不是共享的,第一次執行後就退出迴圈。否則,所有的處理程式都要被執行。之後,如果在註冊期間指定了IRQF_SAMPLE_RANDOM標誌,則還要呼叫函式add_interrupt_randomness()。這個函式使用中斷間隔時間為隨機數產生器產生熵。最後,再將中斷禁止,函式返回。回到do_IRQ(),該函式做清理工作並返回到初始入口點,然後再從這個入口點跳到函式ret_from_intr()。
ret_from_intr()例程類似於初始入口程式碼,以組合語言編寫。這個例程檢查重新排程是否正在掛起。如果重新排程正在掛起,而且核心正在返回使用者空間,那麼,schedule()被呼叫。如果核心正在返回核心空間,只有在preempt_count為0時,schedule()才會被呼叫,否則,搶佔核心是不安全的。在schedule()返回之後,或者如果沒有掛起的工作,那麼,原來的暫存器被恢復,核心恢復到曾經中斷的點。
在x86上,初始的彙編例程位於arch/x86/kernel/entry_64.S(檔案entry_32.S 對應32位的x86體系架構),C方法位於arch/x86/kernel/irq.c。
7.8 /proc/interrupts
procfs是一個虛擬檔案系統,存在於核心記憶體,一般安裝在/proc目錄。在procfs中讀寫檔案都要呼叫核心函式,這些函式模擬從真實檔案中讀寫。與此相關的例子是/proc/interrupts檔案,該檔案存放的是系統中與中斷相關的統計資訊。如下是從單處理器PC上輸出的資訊:
第1列是中斷號。在這個系統中,現有的中斷號為0~2、4、5、12及15。第2列是一個接收中斷數目的計數器。系統中的每個處理器都存在這樣的列,但是,這個機器只有一個處理器。看到,時鐘中斷已接收3602371次中斷,這裡,音效卡(EMU10K1)沒有接收一次中斷。第3列是處理這個中斷的中斷控制器。XT-PIC對應於標準的PC可程式設計中斷控制器。最後一列是與這個中斷相關的裝置名字。這個名字是通過引數devname提供給函式request_irq()的。如果中斷是共享的,則這條中斷線上註冊的所有裝置都會列出來。
procfs程式碼位於fs/proc中。提供/proc/interrupts的函式是與體系結構相關的,叫做show_interrupts()。
在x86體系結構下:
int show_interrupts(struct seq_file *p, void *v) { unsigned long flags, any_count = 0; int i = *(loff_t *) v, j, prec; struct irqaction *action; struct irq_desc *desc;
if (i > nr_irqs) return 0;
for (prec = 3, j = 1000; prec < 10 && j <= nr_irqs; ++prec) j *= 10;
if (i == nr_irqs) return show_other_interrupts(p, prec);
/* print header */ if (i == 0) { seq_printf(p, "%*s", prec + 8, ""); for_each_online_cpu(j) seq_printf(p, "CPU%-8d", j); seq_putc(p, '\n'); }
desc = irq_to_desc(i); if (!desc) return 0;
raw_spin_lock_irqsave(&desc->lock, flags); for_each_online_cpu(j) any_count |= kstat_irqs_cpu(i, j); action = desc->action; if (!action && !any_count) goto out;
seq_printf(p, "%*d: ", prec, i); for_each_online_cpu(j) seq_printf(p, "%10u ", kstat_irqs_cpu(i, j)); seq_printf(p, " %8s", desc->chip->name); seq_printf(p, "-%-8s", desc->name);
if (action) { seq_printf(p, " %s", action->name); while ((action = action->next) != NULL) seq_printf(p, ", %s", action->name); }
seq_putc(p, '\n'); out: raw_spin_unlock_irqrestore(&desc->lock, flags); return 0; }