xv6Kernel - ccc-sp/riscv2os GitHub Wiki
xv6: 核心 kernel -- 從 entry.S 開始追蹤
核心啟動
xv6 作業系統的核心,啟動點位於 entry.S
kernel/entry.S
# qemu -kernel loads the kernel at 0x80000000
# and causes each CPU to jump there.
# kernel.ld causes the following code to
# be placed at 0x80000000.
.section .text
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0
li a0, 1024*4
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
# jump to start() in start.c
call start
spin:
j spin
啟動時會準備好 C 語言所需要的堆疊,然後呼叫 call start,進入第一個 C 語言函數 start()
kernel/start.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"
void main();
void timerinit();
// entry.S needs one stack per CPU.
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
// a scratch area per CPU for machine-mode timer interrupts.
uint64 timer_scratch[NCPU][5];
// assembly code in kernelvec.S for machine-mode timer interrupt.
extern void timervec();
// entry.S jumps here in machine mode on stack0.
void
start()
{
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main);
// disable paging for now.
w_satp(0);
// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// ask for clock interrupts.
timerinit();
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
// switch to supervisor mode and jump to main().
asm volatile("mret");
}
start() 除了設定時間中斷之外,會透過 w_mepc((uint64)main) 這行指令,設定機器模式的 mret 返回點,在最後在 asm volatile("mret") 這行指令執行時,進入 main() 函數。
main.c
進入 main() 之後,核心會開始一連串的系統設定動作:
kernel/main.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"
volatile static int started = 0;
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if(cpuid() == 0){ // 第 0 的 hart 用來跑核心
consoleinit(); // 準備好 console
printfinit(); // 準備好 printf
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator // 準備好實體分頁
kvminit(); // create kernel page table // 準備好核心分頁表
kvminithart(); // turn on paging // 啟動分頁表
procinit(); // process table // 準備好行程表
trapinit(); // trap vectors // 設定好 trap 中斷
trapinithart(); // install kernel trap vector // 安裝核心的中斷向量
plicinit(); // set up interrupt controller // 設定中斷控制器
plicinithart(); // ask PLIC for device interrupts // 設定裝置中斷
binit(); // buffer cache // 檔案系統: 緩衝快取
iinit(); // inode cache // 檔案系統: inode快取
fileinit(); // file table // 檔案系統: 設置檔案表
virtio_disk_init(); // emulated hard disk // 檔案系統: 設置 virtio 虛擬硬碟
userinit(); // first user process // 啟動第一個使用者行程 init
__sync_synchronize();
started = 1; // 啟動已完成
} else { // 其他的 hart 用來跑一般程式
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging // 啟動分頁表
trapinithart(); // install kernel trap vector // 安裝核心的中斷向量
plicinithart(); // ask PLIC for device interrupts // 設定裝置中斷
}
scheduler(); // 進入排程系統 (無窮迴圈)
}
最後會呼叫 scheduler() 進入排程系統。
scheduler()
scheduler() 函數會將處理器核心 (hart) 交給第一個可執行 (RUNNABLE) 的行程。
kernel/proc.c
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns. It loops, doing:
// - choose a process to run.
// - swtch to start running that process.
// - eventually that process transfers control
// via swtch back to the scheduler.
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
}
release(&p->lock);
}
}
}
透過 swtch(&c->context, &p->context) 函數,控制權會從 scheduler 手上,轉交給行程 p。
kernel/swtch.S
# Context switch
#
# void swtch(struct context *old, struct context *new);
#
# Save current registers in old. Load from new.
.globl swtch
swtch:
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret
xv6 是個 multi-tasking 的多工系統,控制權交給行程之後,有兩種方式可以取回來
- 該行程主動交還控制權。
- 硬體引發外部中斷,於是交還控制權給作業系統。
- 時間片段用盡,透過時間中斷強制取回控制權。
這三種情況的處理,最後都集中到 usertrap() 這個函數中:
kernel/trap.c
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0) // 是否來自 user mode ?
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec); // 設定中斷向量為 kernelvec ?
struct proc *p = myproc(); // 取得目前 process
// save user program counter.
p->trapframe->epc = r_sepc(); // 儲存程式計數器 sepc
if(r_scause() == 8){ // 如果是系統呼叫
// system call
if(p->killed) // 如果行程已經被殺死
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4; // 得儲存 ecall 的下一個指令
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on(); // 允許裝置中斷
syscall(); // 執行系統呼叫
} else if((which_dev = devintr()) != 0){ // 如果是裝置中斷,呼叫 devintr() 處理之。
// ok
} else { // 否則,錯誤處理
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) // 如果是時間中斷,禮讓給其他 process
yield();
usertrapret(); // 從 usertrap 返回
}
對於第 1 種方式,使用者行程會在進行輸出入等動作時,透過系統呼叫交回控制權給作業系統,這些系統呼叫是由 usys.pl 程式產生出來的組合語言:
kernel/usys.pl
#!/usr/bin/perl -w
# Generate usys.S, the stubs for syscalls.
print "# generated by usys.pl - do not edit\n";
print "#include \"kernel/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
entry("fork");
entry("exit");
...
kernel/usys.pl 執行後會產生的 kernel/usys.S 如下:
# generated by usys.pl - do not edit
#include "kernel/syscall.h"
.global fork
fork:
li a7, SYS_fork
ecall
ret
.global exit
exit:
li a7, SYS_exit
ecall
ret
# ...
當我們呼叫 fork/exit/.... 等系統呼叫時,會將其編號放入 a7 暫存器中,然後執行 ecall 指令。
ecall 指令會導致軟體中斷 Software Interrupt (trap) 的發生,軟體中斷時會跳到 trampoline.S 的 uservec 組合語言程式:
.globl uservec
uservec:# 進入使用者中斷
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->trapframe is
# mapped into user space, at TRAPFRAME.
#
# ------------ 以下是儲存 user process 暫存器的程式段落 ------------------
# swap a0 and sscratch # 原本 sscratch = p->trapframe, 現在兩者交換 swap(sscratch, a0)
# so that a0 is TRAPFRAME # 所以現在 a0 = p->trapframe
csrrw a0, sscratch, a0
# save the user registers in TRAPFRAME # 儲存所有 user process 的暫存器到 p->trapframe
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)
# save the user a0 in p->trapframe->a0
csrr t0, sscratch # sscratch 就是剛剛和 a0 交換的,所以就是 p->trapframe
sd t0, 112(a0) # 將 p->trapframe 存入 112(p->trapframe)
# ------- 以下是恢復核心暫存器的處理程式 --------------------
# restore kernel stack pointer from p->trapframe->kernel_sp
ld sp, 8(a0) # 恢復堆疊
# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0) # tp = kernel.hartid
# load the address of usertrap(), p->trapframe->kernel_trap
ld t0, 16(a0) # t0 = usertrap
# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1 # 恢復核心的分頁表
sfence.vma zero, zero # 清除 TLB 讓緩存失效
# a0 is no longer valid, since the kernel page
# table does not specially map p->tf.
# jump to usertrap(), which does not return
jr t0 # 跳到 usertrap()
由於 uservec 最後呼叫了 jr t0 (t0 是 usertrap),於是就會跳到 kernel/trap.c 中的 usertrap 函數去: 對於系統呼叫的情況,最後會執行 kernel/syscall.c 中的 syscall() 函數:
kernel/syscall.c
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};
void
syscall(void) // 系統呼叫
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7; // a7 中放了系統呼叫代號
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { // 系統呼叫代號正確
p->trapframe->a0 = syscalls[num](); // 呼叫該系統呼叫
} else { // 系統呼叫代號錯誤
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
所以完成了系統呼叫的動作,同時交還 CPU 控制權給作業系統。
對於第 2 種情況的裝置中斷,usertrap() 會呼叫 devintr() 去處理。
由於 xv6 目前支援的裝置主要是 UART (console, 鍵盤+螢幕) 和 VIRTIO 磁碟,因此 devintr() 中目前只處理這兩種裝置的中斷。
// check if it's an external interrupt or software interrupt,
// and handle it.
// returns 2 if timer interrupt,
// 1 if other device,
// 0 if not recognized.
int
devintr() // 裝置中斷
{
uint64 scause = r_scause();
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){ // 1. 硬體外部中斷
// this is a supervisor external interrupt, via PLIC.
// irq indicates which device interrupted.
int irq = plic_claim();
if(irq == UART0_IRQ){ // UART 中斷
uartintr();
} else if(irq == VIRTIO0_IRQ){ // 磁碟中斷
virtio_disk_intr();
} else if(irq){
printf("unexpected interrupt irq=%d\n", irq);
}
// the PLIC allows each device to raise at most one
// interrupt at a time; tell the PLIC the device is
// now allowed to interrupt again.
if(irq)
plic_complete(irq); // 可以允許再次中斷了。
return 1;
} else if(scause == 0x8000000000000001L){ // 2. 時間中斷
// software interrupt from a machine-mode timer interrupt,
// forwarded by timervec in kernelvec.S.
if(cpuid() == 0){
clockintr();
}
// acknowledge the software interrupt by clearing
// the SSIP bit in sip.
w_sip(r_sip() & ~2); // 註:sip 是 software interrupt-pending
return 2;
} else {
return 0;
}
}
對於第 3 種情況的時間中斷,也會跳入 kerneltrap() 進而呼叫 devintr(),因此 devintr() 中也有時間中斷的 clockintr() 處理程式。
void
clockintr() // 時間中斷
{
acquire(&tickslock);
ticks++;
wakeup(&ticks); // 喚醒等待時間中斷的行程
release(&tickslock);
}
clockintr() 會呼叫 wakeup(&ticks) 以喚醒那些執行過久被換出的行程,讓他們能再次回到 RUNNABLE 狀態,這樣才有機會再次被排程系統執行。
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
if(p != myproc()){
acquire(&p->lock);
if(p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
release(&p->lock);
}
}
}
而對於那個被中斷的行程而言,usertrap() 會呼叫 yield() 讓出控制權
kernel/proc.c
// Give up the CPU for one scheduling round.
void
yield(void)
{
struct proc *p = myproc();
acquire(&p->lock);
p->state = RUNNABLE;
sched();
release(&p->lock);
}
yield() 會呼叫 sched() ,進而透過 swtch(&p->context, &mycpu()->context) 交還控制權給核心。
// Switch to scheduler. Must hold only p->lock
// and have changed proc->state. Saves and restores
// intena because intena is a property of this
// kernel thread, not this CPU. It should
// be proc->intena and proc->noff, but that would
// break in the few places where a lock is held but
// there's no process.
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get())
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}
現在,我們已經說明完了《軟體、硬體,時間等三種中斷》的處理方式。
在 usertrap() 的最後一行,會透過 usertrapret() 將控制權交回給使用者行程 (程式碼如下),這樣就能在該行程完成之後,恢復行程的執行了。
kernel/trap.c
//
// return to user space
//
void
usertrapret(void)
{
struct proc *p = myproc();
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off(); // 禁止裝置中斷
// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
// 保存 kernel 的相關暫存器
// set up trapframe values that uservec will need when
// the process next re-enters the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// set up the registers that trampoline.S's sret will use
// to get to user space.
// 回復到使用者模式
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc); // 設定 sepc 準備返回
// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable); // 切換回使用者分頁表
// jump to trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 fn = TRAMPOLINE + (userret - trampoline); // 呼叫 trampoline.S 切換回使用者行程。
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}
必須注意的是,那些中斷的處理程式,都是在作業系統一開始時就設定好的,舉例而言,時間中斷強制取回的方式,是由以下程式中的 w_mtvec((uint64)timervec) 指令所設定的。(而且同時會設定時間片段的大小,一旦時間用盡就會引發中斷)
kernel/start.c
// set up to receive timer interrupts in machine mode,
// which arrive at timervec in kernelvec.S,
// which turns them into software interrupts for
// devintr() in trap.c.
void
timerinit()
{
// each CPU has a separate source of timer interrupts.
int id = r_mhartid();
// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
// prepare information in scratch[] for timervec.
// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id);
scratch[4] = interval;
w_mscratch((uint64)scratch);
// set the machine-mode trap handler.
w_mtvec((uint64)timervec);
// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);
// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
}
當時間片段用盡時,會觸發 timervec() 函數,
kernel/kernelvec.S
#
# machine-mode timer interrupt. 機器模式的時間中斷
#
.globl timervec
.align 4
timervec:
# start.c has set up the memory that mscratch points to:
# scratch[0,8,16] : register save area.
# scratch[24] : address of CLINT's MTIMECMP register.
# scratch[32] : desired interval between interrupts.
csrrw a0, mscratch, a0 # 儲存到暫存區
sd a1, 0(a0)
sd a2, 8(a0)
sd a3, 16(a0)
# schedule the next timer interrupt # 設定下一次中斷時間
# by adding interval to mtimecmp.
ld a1, 24(a0) # CLINT_MTIMECMP(hart)
ld a2, 32(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)
# raise a supervisor software interrupt.
# 引發特權軟體中斷,跳到 kernelvec=>kerneltrap()
li a1, 2
csrw sip, a1
ld a3, 16(a0) # 從暫存區取回暫存器
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0
mret
以上 timervec 當中的 csrw sip, a1 指令,會引發核心中斷
# raise a supervisor software interrupt.
# 引發核心中斷,跳到 kernelvec=>kerneltrap()
li a1, 2
csrw sip, a1
於是程式跳到 kernelvec() 中
#
# interrupts and exceptions while in supervisor
# mode come here.
#
# push all registers, call kerneltrap(), restore, return.
#
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
// make room to save registers. # 分配堆疊以儲存暫存器
addi sp, sp, -256
// save the registers.
sd ra, 0(sp)
sd sp, 8(sp)
sd gp, 16(sp)
sd tp, 24(sp)
sd t0, 32(sp)
sd t1, 40(sp)
sd t2, 48(sp)
sd s0, 56(sp)
sd s1, 64(sp)
sd a0, 72(sp)
sd a1, 80(sp)
sd a2, 88(sp)
sd a3, 96(sp)
sd a4, 104(sp)
sd a5, 112(sp)
sd a6, 120(sp)
sd a7, 128(sp)
sd s2, 136(sp)
sd s3, 144(sp)
sd s4, 152(sp)
sd s5, 160(sp)
sd s6, 168(sp)
sd s7, 176(sp)
sd s8, 184(sp)
sd s9, 192(sp)
sd s10, 200(sp)
sd s11, 208(sp)
sd t3, 216(sp)
sd t4, 224(sp)
sd t5, 232(sp)
sd t6, 240(sp)
// call the C trap handler in trap.c # 呼叫 C 語言的 kerneltrap() 函數。
call kerneltrap
// restore registers. // 恢復暫存器
ld ra, 0(sp)
ld sp, 8(sp)
ld gp, 16(sp)
// not this, in case we moved CPUs: ld tp, 24(sp)
ld t0, 32(sp)
ld t1, 40(sp)
ld t2, 48(sp)
ld s0, 56(sp)
ld s1, 64(sp)
ld a0, 72(sp)
ld a1, 80(sp)
ld a2, 88(sp)
ld a3, 96(sp)
ld a4, 104(sp)
ld a5, 112(sp)
ld a6, 120(sp)
ld a7, 128(sp)
ld s2, 136(sp)
ld s3, 144(sp)
ld s4, 152(sp)
ld s5, 160(sp)
ld s6, 168(sp)
ld s7, 176(sp)
ld s8, 184(sp)
ld s9, 192(sp)
ld s10, 200(sp)
ld s11, 208(sp)
ld t3, 216(sp)
ld t4, 224(sp)
ld t5, 232(sp)
ld t6, 240(sp)
addi sp, sp, 256 # 恢復堆疊指標
// return to whatever we were doing in the kernel.
sret
kernelvec() 又用 call kerneltrap 呼叫 C 語言的中斷處理函數 kerneltrap()。
// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void
kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();
if((sstatus & SSTATUS_SPP) == 0)
panic("kerneltrap: not from supervisor mode");
if(intr_get() != 0)
panic("kerneltrap: interrupts enabled");
if((which_dev = devintr()) == 0){ // 1. 裝置中斷
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// give up the CPU if this is a timer interrupt.
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING) // 1. 時間中斷,禮讓給別人
yield(); // 註:時間中斷對 user mode 與 kernel mode 都是有效的,都必須禮讓給別人。
// the yield() may have caused some traps to occur,
// so restore trap registers for use by kernelvec.S's sepc instruction.
w_sepc(sepc);
w_sstatus(sstatus);
}
kerneltrap() 會根據是哪一種中斷分別處理。
- 如果是裝置中斷,那麼交給 devintr() 函數。
- 如果是時間中斷,則呼叫 yield() 強制交還控制權。
kerneltrap() 和 usertrap() 稍有差異,因為 kerneltrap() 當中不需要處理軟體中斷 (系統呼叫) 的狀況。
結語
當核心準備好一切,進入排程系統 scheduler() 時,已經設定好了《軟體中斷,裝置中斷與時間中斷》,因此以下的 scheduler() 函數才能正常的執行排程工作。
kernel/proc.c
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns. It loops, doing:
// - choose a process to run.
// - swtch to start running that process.
// - eventually that process transfers control
// via swtch back to the scheduler.
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
}
release(&p->lock);
}
}
}
問題是,到底哪些程式需要被排程呢?第一個被排程的程式是誰呢?
其實,所有的使用者行程都需要被排程,而第一個使用者行程,稱為 init ,將是我們下一篇文章的主題!