1. 程式人生 > 其它 >中斷處理函式_0013 CPU排程硬體中斷與軟中斷

中斷處理函式_0013 CPU排程硬體中斷與軟中斷

技術標籤:中斷處理函式

提到排程就會想起執行緒,程序。線上程以外,還有其他排程單位。

比如硬體中斷,硬體中斷處理程式是遮蔽中斷的,不能處理時間過長,否則其他硬體中斷就會被延誤。因此硬體中斷髮生之後,往往就設定一些標記,就返回了。而硬體中斷髮生之後需要處理,比如網絡卡,需要讀寫資料,那在硬體中斷之後處理的這些事情,就叫軟中斷。

軟中斷從硬體讀寫完資料之後,還需要進一步處理,比如網絡卡收到資料包之後,需要處理資料包裡面的協議。這個操作優先順序就沒這麼高,而讀寫硬體的軟中斷優先順序高,不處理的話就影響後續的讀寫了。那對時間不是這麼嚴格要求的,可以放到工作佇列(work queue),或者核心執行緒KTHREAD去處理。

優先順序最高的硬體中斷,其次是軟中斷,在處理軟中斷的時候會發生硬體中斷,但是不會被其他排程資源搶佔。因此軟中斷處理程式,只考慮硬體中斷跟其他cpu核心的互動的情況,不需要擔心某個資料被本CPU的其他排程資源給修改的情況。跟其他cpu互動可以用自旋鎖。如果寫關鍵資料不允許被打斷,就要關閉硬體中斷。

軟中斷也是分很多種的,有優先順序,優先順序高的不需要考慮被優先順序地的搶佔的情況。

比如時鐘軟中斷,優先順序比較高。其他低優先順序的軟中斷會被時鐘軟中斷搶佔,那如果操作關鍵資料,就需要關閉硬體中斷,暫停軟體中斷。

硬體中斷有自己獨立的棧。如何防止重入呢?

硬體中斷棧

ss #棧頂
rsp
flags
cs
ip
0
中斷號

第一次發生硬體中斷的時候。如果這次硬體中斷還沒處理完,又發生了中斷。那麼又會重新從棧頂位置壓入新的ss,rsp,flags,cs,ip。這樣就亂套了。

解決辦法一個是禁止巢狀,另外一個就是複製堆疊。禁止巢狀不科學,就來看看如何複製堆疊:

.global switchtrap
switchtrap:
  cld
  push %r15
  mov  %rsp,%r15
  incl    PER_CPU_VAR(irq_count)
  jnz 1f
  movq   PER_CPU_VAR(irq_stack_ptr), %rsp
  jmp 2f
1:
  mov  48(%r15),%rsp
  sub  $136,%rsp
2:
  push 56(%r15) #ss
  push 48(%r15) #rsp
  push 40(%r15) #flag
  push 32(%r15) #cs
  push 24(%r15)  #ip
  push 16(%r15)
  push 8(%r15)
  push (%r15)   #r15
  push %r14
  push %r13
  push %r12
  push %r11
  push %r10
  push %r9
  push %r8
  push %rdi
  push %rsi
  push %rbp
  push %rdx
  push %rcx
  push %rbx
  push %rax
  mov  %rsp, %rdi  # frame in arg1
  movq    120(%rdi), %rax
  movq    c_vectors(,%rax,8),%rax

  call *%rax
.global switchtrapret
switchtrapret:
  cli
  decl    PER_CPU_VAR(irq_count)
  pop %rax
  pop %rbx
  pop %rcx
  pop %rdx
  pop %rbp
  pop %rsi
  pop %rdi
  pop %r8
  pop %r9
  pop %r10
  pop %r11
  pop %r12
  pop %r13
  pop %r14
  pop %r15
  add $16,%rsp
  iretq

arch/trapasm64.S

switchtrap在arch/vecotor.S裡面被呼叫

vector39:
  push $0
  push $39
 jmp switchtrap
.globl vector40
vector40:
  push $0
  push $40
 jmp switchtrap
.globl vector41

壓入0跟中斷號之後就跳轉到switchtrap。

irq_count初始值-1,加一為0的時候,就是首次硬體中斷,把單獨的中斷棧棧頂irq_stack_ptr設定為新的棧頂。然後把之前壓入的ss,rsp,flags,cs,ip,0,中斷號,r15複製到新的棧。這個時候中斷還沒有開啟,因此不會發生新的中斷。之後開啟中斷之後,如果又發生硬體中斷,對中斷計數器加1就不是0了,表明嵌套了。這個時候就不是用irq_stack_ptr,而是中斷髮生時候壓入的rsp,即單獨的中斷處理棧發生硬體中斷時候的rsp,然後減去136,這是保留redzone區域。switchtrap把硬體中斷巢狀變成了中斷棧的巢狀。

因此tss裡面ist設定的硬體中斷棧不需要很大,而irq_stack需要設定大點,防止硬體中斷巢狀層數較多的時候。

同時,switchtrap也把棧從硬體中斷棧改成了軟中斷irq_stack棧,就可以直接處理軟中斷了。

switchtrap完成堆疊設定之後,就呼叫中斷處理函式。比如定時器:

register_irq(TIMER_VECTOR, timer_irq_first_handler);

初始化函式呼叫regist_irq設定處理函式

static void timer_irq_first_handler(int n)
{
    ASSERT(msec_count == 0);
    ack_lapic_irq();
    cmostime(&mtime);
    printk("cmos:%d-%d-%d %d:%d:%dn", mtime.year, mtime.month, mtime.day,
           mtime.hour, mtime.minute, mtime.second);

    register_irq(TIMER_VECTOR, timer_irq_handler);
    register_irq(LOCAL_TIMER_VECTOR, local_timer_irq_handler);

}

第一次執行時鐘中斷之後,重新註冊新的處理函式timer_irq_handler

static void timer_irq_handler(int n)
{
    void run_local_timers(void);

    ack_lapic_irq(); //響應中斷

    barrier();
    ++msec_count;
    barrier();
    run_local_timers();//硬體中斷處理完成,呼叫kernel/timer.c裡面的中斷處理
   
}
//run_local_timers就啟動定時器軟中斷
void run_local_timers(void)
{
    raise_softirq(TIMER_SOFTIRQ);
}

//arch/irq.c
void default_irq_trap(struct trapframe *tf)
{
    irq_enter();
    ((irq_handler_t) (irq_vectors[tf->trapno])) ((int)tf->trapno);
    irq_exit();

}

硬體中斷流程:

1d059cbb4ff79c2808c544ff8fcf2940.png

在irq_exist裡面進行判斷,是否當前在軟中斷中(即在處理軟中斷的過程中發生硬體中斷重入),如果已經在軟中斷中,就會直接訪回,返回之後會執行沒有處理完成的軟中斷,而軟中斷處理完成之後,最後又會呼叫irq_exist,這次判斷就會發現已經不是重入狀態了,就會檢查軟中斷佇列,有沒有需要處理的軟中斷。如果有的話就會處理軟中斷再返回第一次被硬體中斷打斷的程式。

void irq_exit(void)
{
    local_irq_disable();
    if (!in_tasklet() && local_softirq_pending())
        invoke_softirq();

}

在irq_exist返回之前,如果有軟中斷需要執行,並且當前環境不是在tasklet中,即在執行tasklet的過程中發生了硬體中斷,處理處理軟中斷,invoke_softirq。invoke_softirq會呼叫

__do_softirq,處理軟中斷。為什麼in_tasklet就不處理軟中斷了呢?

那是因為in_tasklet在tasklet任務裡面,而這個任務的優先順序是最高的,不會被其他任務搶佔,這個任務就負責處理軟中斷。因此即便這裡返回了沒處理,中斷退出之後也會恢復到tasklet任務繼續處理軟中斷。

那為啥要單獨搞個tasklet任務出來呢,直接在軟中斷堆疊裡面處理軟中斷不好嗎?

這是防止軟中斷處理時間太長,一直搶佔任務。而這個軟中斷是沒有自己的任務的,有可能在普通任務的上下文裡面處理。處理時間過長會導致其他高優先順序任務無法得不到及時執行。因此軟中斷處理函式__do_softirq就試圖處理幾次軟中斷迴圈,超過了次數或者時間就啟動tasklet任務進行繼續處理軟中斷。

#define MAX_SOFTIRQ_TIME 2
//最長處理時間2ms
#define MAX_SOFTIRQ_RESTART 10 
//最多迴圈10次,即處理一遍佇列之後又發生了硬體中斷,然後設定了軟中斷標記。
void __do_softirq(void)
{
    unsigned long end = msec_count + MAX_SOFTIRQ_TIME;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    __u32 pending;
    int softirq_bit;
    pending = local_softirq_pending();
    disable_tasklet();
  restart:
    /* Reset the pending bitmask before enabling irqs */
    set_softirq_pending(0);

    local_irq_enable();

    h = softirq_vec;

    while ((softirq_bit = ffs(pending))) {

        h += softirq_bit - 1;
        //呼叫軟中斷處理函式
        h->action(h);
        h++;
        pending >>= softirq_bit;
    }

    local_irq_disable();
    pending = local_softirq_pending();
    if (pending) {
        if (msec_count < end && --max_restart)
            goto restart;

        wakeup_taskletd();
    }
    enable_tasklet();
}

現在來實驗一下:

kernel/test.c

#include <types.h>
#include <yaos/init.h>
#include <yaos/time.h>
#include <yaos/printk.h>
static void test (u64 nowmsec)
{
    printk("test timeout: %lxn",nowmsec);
}
static int test_init(bool isbp)
{
    set_timeout(11000,test);
    return 0;

}
late_initcall(test_init);

執行結果:

cmos:2020-5-22 15:24:58
cpu:0,ticks:1000
cpu:0,ticks:2000
cpu:0,ticks:3000
cpu:0,ticks:4000
cpu:0,ticks:5000
cpu:0,ticks:6000
cpu:0,ticks:7000
cpu:0,ticks:8000
cpu:0,ticks:9000
cpu:3,ticks:1000
cpu:1,ticks:1000
cpu:2,ticks:1000
cpu:0,ticks:10000
cpu:0,ticks:11000
test timeout: 2af9,cpu:0
test timeout: 2af9,cpu:3
test timeout: 2af9,cpu:1
test timeout: 2af9,cpu:2
cpu:0,ticks:12000
cpu:0,ticks:13000
cpu:0,ticks:14000
cpu:0,ticks:15000
cpu:0,ticks:16000

會看到在11秒以後列印4個test timeout: 2af8

那是因為每個cpu都呼叫了set_timeout函式。

時鐘軟中斷並沒有啟動tasklet任務,是直接在軟中斷上下文裡面呼叫的。

下一篇再進行tasklet實驗。

執行本例:

git clone https://github.com/saneee/x86_64_kernel.git
cd 0013
make qemuimg