中斷處理函式_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();
}
硬體中斷流程:
在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