1. 程式人生 > >Linux內核中的中斷棧與內核棧的補充說明【轉】

Linux內核中的中斷棧與內核棧的補充說明【轉】

desc 試圖 調度 php == over 註冊 tex 存在

轉自:http://blog.chinaunix.net/uid-12461657-id-3487463.html

原文地址:Linux內核中的中斷棧與內核棧的補充說明 作者:MagicBoy2010

中斷棧與內核棧的話題更多地屬於內核的範疇,所以在《深入Linux設備驅動程序內核機制》第5章“中斷處理”當中,基本上沒怎麽涉及到上述內容,只是在5.4節有些許的文字討論中斷棧在中斷嵌套情形下可能的溢出問題。

本貼在這個基礎上對內核棧與中斷棧的話題做些補充,討論基於x86 32位系統,因為64位系統下Linux內核關於棧的支持原理上是相同的,不過也有些特性屬於64位特有的,比如IST(Interrupt Stack Table),如果可能將來會在processor版塊發個帖子專門討論。


1. x86下內核棧與中斷棧是否共享的問題

我們知道Linux系統下每個用戶進程都有個task_struct對象來表示,同時在處理器層面還對應一個TSS(Task State Segment),當中斷發生時,用戶進程或者處於用戶態(ring 3)或者處於內核態(ring 0),如果是在用戶態,那麽會發生棧的切換問題,也就是會切換到內核態的棧,如果是在內核態,那麽就沒有棧切換的問題。但是x86處理器在ring 0上只有一個ESP,這意味著中斷發生後,只能使用一個棧,這個棧就是內核棧(kernel stack)。處理器的硬件邏輯會將被中斷進程的下條指令(CS,EIP)以及EFLAG壓入棧,當然如果發生用戶態棧向內核態棧的切換,處理器還會把用戶態的(SS, ESP)也壓入棧,此時使用的就是內核棧。這個行為屬於處理器的硬件邏輯範疇,不是系統軟件的行為。

至於x86下內核棧與中斷棧是否共享的問題,其實是個內核設計的問題,換言之,中斷棧可與內核棧共享,也可重新分配一個獨立的中斷棧。2.4的內核版本似乎采用中斷棧與內核棧共享的設計,因為這種設計的好處是代碼相對簡單,如前所述,直接使用ESP0就可以了,但是負面因素是中斷棧如果發生嵌套,可能破壞內核棧的一些數據,因為畢竟共享,所以棧空間有時候難免會捉襟見肘。所以在2.5內核版本開發中,來自IBM的一位大俠曾提交過一個補丁(詳見http://lwn.net/Articles/21846/),試圖在中斷發生時,從內核棧switch到一個獨立的中斷棧中,後來也不知道被內核社區采納了沒有,總之我現在在3.2的內核源碼中沒有看到那位仁兄的補丁代碼了,當然也可能是那個補丁已經長成現在的代碼樣子了。

現在的Linux內核中采用的是內核棧與中斷棧分離的設計,下面我們從源碼層面來看一看這種分離是如何完成的。

內核棧與中斷棧分離的核心代碼發生在do_IRQ() --> handle_irq() --> execute_on_irq_stack()
最後一個函數字面上的意思大約是在中斷棧中執行中斷處理例程,也就是說中斷的處理函數會在獨立於被中斷進程的上下文中執行。execute_on_irq_stack的函數實現為:

<arch/x86/kernel/irq_32.c>


  1. static inline int

  2. execute_on_irq_stack(int overflow, struct irq_desc *desc, int irq)

  3. {
  4. union irq_ctx *curctx, *irqctx;
  5. u32 *isp, arg1, arg2;

  6. curctx = (union irq_ctx *) current_thread_info();
  7. irqctx = __this_cpu_read(hardirq_ctx);
  8. /*
  9. * this is where we switch to the IRQ stack. However, if we are
  10. * already using the IRQ stack (because we interrupted a hardirq
  11. * handler) we can‘t do that and just have to keep using the
  12. * current stack (which is the irq stack already after all)
  13. */

  14. if (unlikely(curctx == irqctx))
  15. return 0;

  16. /* build the stack frame on the IRQ stack */
  17. isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
  18. irqctx->tinfo.task = curctx->tinfo.task;
  19. irqctx->tinfo.previous_esp = current_stack_pointer;

  20. /*
  21. * Copy the softirq bits in preempt_count so that the
  22. * softirq checks work in the hardirq context.
  23. */

  24. irqctx->tinfo.preempt_count =
  25. (irqctx->tinfo.preempt_count & ~SOFTIRQ_MASK) |
  26. (curctx->tinfo.preempt_count & SOFTIRQ_MASK);

  27. if (unlikely(overflow))
  28. call_on_stack(print_stack_overflow, isp);

  29. asm volatile("xchgl %%ebx,%%esp \n"
  30. "call *%%edi \n"
  31. "movl %%ebx,%%esp \n"
  32. : "=a" (arg1), "=d" (arg2), "=b" (isp)
  33. : "0" (irq), "1" (desc), "2" (isp),
  34. "D" (desc->handle_irq)
  35. : "memory", "cc", "ecx");

  36. return 1;
  37. }
代碼中的curctx=(union irq_ctx *) current_thread_info()用來獲得當前被中斷進程的上下文,irqctx = __this_cpu_read(hardirq_ctx)用來獲得hardirq的上下文,其實就是獲得獨立的中斷棧起始地址。中斷棧的大小與layout與內核棧是完全一樣的。接下來isp指向中斷棧棧頂,最後的堆棧切換發生在那段匯編代碼中:當前進程的內核棧ESP指針保存在EBX中,而中斷棧的isp則賦值給了ESP,這樣接下來的代碼就將使用中斷棧了。call語句負責調用desc->handle_irq()函數,這裏會進行中斷處理,設備驅動程序註冊的中斷處理函數會被調用到。當中斷處理例程結束返回時,ESP將重新指向被中斷進程的內核棧。(此處我們應該註意到內核棧中還保留著中斷發生時處理器硬件邏輯所壓入的CS, EIP等寄存器,所以在內核棧中做中斷返回是完全正確的)。

2. 中斷棧的分配

獨立的中斷棧所在內存空間的分配發生在arch/x86/kernel/irq_32.c的irq_ctx_init函數中(如果是多處理器系統,那麽每個處理器都會有一個獨立的中斷棧),函數使用__alloc_pages在低端內存區分配2個物理頁面(2的THREAD_ORDER次方),也就是8KB大小的空間。有趣的是,這個函數還會為softirq分配一個同樣大小的獨立堆棧,如此說來,softirq將不會在hardirq的中斷棧上執行,而是在自己的上下文中執行。

總結一下,系統中每個進程都會擁有屬於自己的內核棧,而系統中每個CPU都將為中斷處理準備了兩個獨立的中斷棧,分別是hardirq棧和softirq棧。草圖如下:


技術分享

最後,關於設備驅動程序的中斷處理例程中調用可能引起阻塞函數的問題,可以簡單歸結為在中斷處理上下文中能否進行調度的問題。現實中,絕對不應該這樣做,因為這會引起很多問題。但是從理論實現的角度,如果調度器願意,它找到被中斷進程的上下文並不存在技術上的障礙,這意味著在中斷處理函數中如果發生進程切換,被中斷進程被再次調度是可能的,如果調度器願意這麽做的話。

(原文首發:http://www.embexperts.com/forum.php/forum.php?mod=viewthread&tid=499&extra=page%3D1,略有改動)

Linux內核中的中斷棧與內核棧的補充說明【轉】