系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念
任何的作業系統都會希望,運行的 process 的數量超過電腦實際擁有的 CPUs 的數量。例如說我的筆記型電腦只有 4 個 CPU,但是可能會希望跑幾十,甚至幾百個 process。
想要達成這個目的,我們需要把 CPUs 的時間分享給多個 process。
理想上來說,CPUs 時間的分享對於 user processes 來說是 “transparent ( 透明 )” 的,也就是 user processes 看不到這個分享的過程,不會被分享的過程干擾,不會感知到目前正在進行 CPUs 時間的分享。
A common approach is to provide each process with the illusion that it has its own virtual CPU by multiplexing the processes onto the hardware CPUs.
xv6-riscv 會在下面兩個情況來切換在某一個特定 CPU 上運行的 process ( 同一個 CPU,從 process A 換成執行 process B )
Implementing multiplexing poses a few challenges.
The term context switch refers to the steps involved in a CPU leaving off execution of one kernel thread (usually for later resumption), and resuming execution of a different kernel thread. This switching is the heart of multiplexing.
xv6-riscv 並不會直接從一個 process 的 kernel thread context switch 到另外一個 process 的 kernel thread。當 xv6-riscv 在進行 context-switch 的時候,會先切換到 CPU 的 scheduler thread,這個 scheduler thread 會選擇下一個要執行的 process,並且 context switch 到該 process。
user space 可能會因為 system call 或是 interrupt ( e.g. timer interrupt ) 而 trap 進入到 kernel space
{ Figure 8.1 }
// Saved registers for kernel context switches.
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
context switch 發生的時候,就是會將舊的 struct-context 存到 ram 上的 struct-context,並且將新的 struct-context 載入到 CPU 的 CSRs。# 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
context switch
這邊舉一個例子,並用單單一個 register ra ( Return Address ) 來當作範例。假設我們現在運行在 CPU-1 上,並且想要從 process-A context switch 到 process-B。
scheduler thread 會選擇下一個要運行的 process,這邊假設選中了 process-B。swtch(&c->context, &p->context),嘗試切換到 process-B 的 context// Per-CPU state.
struct cpu {
struct proc *proc; // The process running on this cpu, or null.
struct context context; // swtch() here to enter scheduler().
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};
scheduler thread 的 contextintena 的值來決定要不要 enable interrupt。push_off 的時候,interrupt 是否為打開的狀態。// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// prepare_return() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
/* 72 */ uint64 t0;
/* 80 */ uint64 t1;
/* 88 */ uint64 t2;
/* 96 */ uint64 s0;
/* 104 */ uint64 s1;
/* 112 */ uint64 a0;
/* 120 */ uint64 a1;
/* 128 */ uint64 a2;
/* 136 */ uint64 a3;
/* 144 */ uint64 a4;
/* 152 */ uint64 a5;
/* 160 */ uint64 a6;
/* 168 */ uint64 a7;
/* 176 */ uint64 s2;
/* 184 */ uint64 s3;
/* 192 */ uint64 s4;
/* 200 */ uint64 s5;
/* 208 */ uint64 s6;
/* 216 */ uint64 s7;
/* 224 */ uint64 s8;
/* 232 */ uint64 s9;
/* 240 */ uint64 s10;
/* 248 */ uint64 s11;
/* 256 */ uint64 t3;
/* 264 */ uint64 t4;
/* 272 */ uint64 t5;
/* 280 */ uint64 t6;
};
kernel/proc.c/proc_pagetable,全部 process 的 struct-proc->trapframe 都會放到同樣的 virtual address : TRAPFRAME。// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
procinit 初始化 struct-proc pool 的時候,會先把每一個 pool 內的 struct-proc 標示為 UNUSEDallocproc 來 allocate 一個 struct-proc 的時候,會將該 struct-proc 標示為 USED
kexit ),會進入 ZOMBIE 狀態,直到 parent 呼叫 wait。wait 接住已經被 kill 的這個 process,這個 process 要回傳給 parent-process 的 wait 的值。regular file, directory, device。