# XV6 Ch5 Scheduling ## Multiplexing - 如果 process 需要等待 I/O 或是子 process 結束,xv6 讓其進入睡眠狀態,接著將處理器 switch 給其他 process。 - 此機制使 process 有獨佔 CPU 的假象。 - 完成 switch 的動作由 context switch 完成: - 透明化 -> 使用 **timer interrupt handler** 驅動 context switch。 - 同時多個 process 需要 switch -> lock --- ## Code: Context switch - 從 process 的 kernel stack -> schedluler kernel stack (CPU) -> 另一個 process 的 kernel stack。 - 永遠不會從 process 的 kernel stack -> process 的 kernel stack。 ![image alt](https://th0ar.gitbooks.io/xv6-chinese/content/pic/f5-1.png) - 每個 process 都有自己的 kernel stack 及暫存器集合,每個 CPU 有自己的 scheduler stack。 - context 即 process 的暫存器集合,用一個 `struct context *` 表示。 :::success **File:** proc.h ::: ```c=44 struct context { uint edi; uint esi; uint ebx; uint ebp; uint eip; }; ``` - trap 有可能會呼叫 `yield`,`yield` 會呼叫 `sched`,最後 `sched` 呼叫 `swtch(&proc->context, cpu->scheduler);`。 ### File: swtch.S - *swtch* 有兩個參數:`struct context ** old`、`struct context * new`。 ```c= # Context switch # # void swtch(struct context **old, struct context *new); # # Save current register context in old # and then load register context from new. .globl swtch swtch: movl 4(%esp), %eax movl 8(%esp), %edx ``` - 將 `%eax` 指向 `struct context ** old`,`%ebx` 指向 `struct context * new`。 ![](https://i.imgur.com/0QWW7gZ.png) ```c=+ # Save old callee-save registers pushl %ebp pushl %ebx pushl %esi pushl %edi ``` - 依序將 context push 進堆疊 ![](https://i.imgur.com/CXUNIYY.png) ```c=+ # Switch stacks movl %esp, (%eax) movl %edx, %esp ``` - 將 `struct context ** old`(`%eax`) 指向 `%esp`(當前堆疊的 **top**) - 將 `%esp` 指向 `struct context * new`(`%ebx`) ![](https://i.imgur.com/AB38T7t.png) ```c=+ # Load new callee-save registers popl %edi popl %esi popl %ebx popl %ebp ret ``` - 恢復保存的暫存器,用`ret` 指令跳回 --- ## Scheduling - process 要讓出 CPU 前需要先取得 ptable.lock,釋放其他擁有的鎖,修改 p->state,呼叫 `sched`。 - `sched` 會再次檢查以上動作,並且確定此時中斷是關閉的,最後呼叫 `swtch`,將 process 的暫存器保存在 proc->context,switch 到 cpu->scheduler。 ### `sched()` :::success **File:** proc.c ::: ```c=292 void sched(void) { int intena; if(!holding(&ptable.lock)) panic("sched ptable.lock"); if(cpu->ncli != 1) panic("sched locks"); if(proc->state == RUNNING) panic("sched running"); if(readeflags()&FL_IF) panic("sched interruptible"); intena = cpu->intena; swtch(&proc->context, cpu->scheduler); cpu->intena = intena; } ``` - `swtch` 會 return 回 scheduler 的堆疊上,scheduler 繼續執行迴圈:找到一個 process 運行,`swtch` 到該 process,repeat。 ### `scheduler()` ```c=249 void scheduler(void) { struct proc *p; for(;;){ // Enable interrupts on this processor. sti(); // Loop over process table looking for process to run. acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->state != RUNNABLE) continue; // Switch to chosen process. It is the process's job // to release ptable.lock and then reacquire it // before jumping back to us. proc = p; switchuvm(p); p->state = RUNNING; swtch(&cpu->scheduler, proc->context); switchkvm(); // Process is done running for now. // It should have changed its p->state before coming back. proc = 0; } release(&ptable.lock); } } ``` - `scheduler` 找到一個 `RUNNABLE` 的 process,將 per-cpu 設為此 process,呼叫 `switchuvm` 切換到該 process 的頁表,更新狀態為`RUNNING`,`swtch` 到該 process 中運行。 --- ## Code: mycpu and myproc - CPU 需要辨識目前正在執行的 process,xv6 有一個 struct cpu 的陣列,裡面包含了一些 CPU 的資訊,及當前 process 的資訊。 - `mycpu` 尋找當前的 `lapicid` 是哪顆 CPU 的。 ### `mycpu()` ```c=37 struct cpu* mycpu(void) { int apicid, i; if(readeflags()&FL_IF) panic("mycpu called with interrupts enabled\n"); apicid = lapicid(); // APIC IDs are not guaranteed to be contiguous. Maybe we should have // a reverse map, or reserve a register to store &cpus[i]. for (i = 0; i < ncpu; ++i) { if (cpus[i].apicid == apicid) return &cpus[i]; } panic("unknown apicid\n"); } ``` - `myproc` 呼叫 `mycpu`,從正確的 CPU 上尋找當前的 process。 ```c=57 struct proc* myproc(void) { struct cpu *c; struct proc *p; pushcli(); c = mycpu(); p = c->proc; popcli(); return p; } ``` --- ## 睡眠與喚醒(例子) ```c struct q { void *ptr; }; void* send(struct q *q, void *p) { while(q->ptr != 0) ; q->ptr = p; } void* recv(struct q * q) { void *p; while((p = q->ptr) == 0) ; q->ptr = 0; return p; } ``` - `send` 直到隊伍 `q` 為空時,將要傳送的資料 `p` 放入隊中,`recv` 直到 `q` 有東西時將資料取出,把 `q` 再次設為 `0` - 這允許不同的 process 交換資料,但是很耗資源。 ### 方案 1 - 加入 `sleep` 及 `wakeup` 機制,`sleep(chan)` 將 process 在 `chan` 中睡眠(一個 wait channel),`wakeup(chan)` 將 `chan` 上睡眠的 process 喚醒。 ```diff void* send(struct q *q, void *p) { while(q->ptr != 0) ; q->ptr = p; + wakeup(q); /*wake recv*/ } void* recv(struct q *q) { void *p; while((p = q->ptr) == 0) + sleep(q); q->ptr = 0; return p; } ``` - 如此一來 `recv` 能讓出 CPU,直到有人(`send`)將它喚醒。 - **問題**:遺失的喚醒: ![image alt](https://th0ar.gitbooks.io/xv6-chinese/content/pic/f5-2.png) - 假設 `recv` 在 215 行查看隊伍,發現需要睡眠,就在要呼叫 `sleep` 之前發生中斷,`send` 在其他的 CPU 將資料放進隊伍中,呼叫 `wakeup`,發現沒有正在休眠的 process,於是不做事;接著 `recv` 終於呼叫 `sleep` 進入睡眠。 - 此時,`revc` 正在等待 `send` 將它喚醒,但是 `send` 正在等待隊伍清空,清空的動作須由 `recv` 完成(休眠中),於是就產生了 **deadlock**。 ### 方案 2 - 讓 `send` 及 `recv` 在睡眠及喚醒前都持有鎖。 ```diff struct q { struct spinlock lock; void *ptr; }; void * send(struct q *q, void *p) { + acquire(&q->lock); while(q->ptr != 0) ; q->ptr = p; wakeup(q); + release(&q->lock); } void* recv(struct q *q) { void *p; + acquire(&q->lock); while((p = q->ptr) == 0) sleep(q); q->ptr = 0; + release(&q->lock); return p; } ``` - 但這一樣有問題:`recv` 帶著鎖進入睡眠,`send` 也同時需要鎖來喚醒,於是就產生了 **deadlock**。 ### 方案 3 - 在 `sleep` 時一併釋放鎖,也就是將鎖當成參數傳進去。 ```diff struct q { struct spinlock lock; void *ptr; }; void * send(struct q *q, void *p) { acquire(&q->lock); while(q->ptr != 0) ; q->ptr = p; wakeup(q); release(&q->lock); } void* recv(struct q *q) { void *p; acquire(&q->lock); while((p = q->ptr) == 0) + sleep(q, &q->lock); q->ptr = 0; release(&q->lock); return p; } ``` --- ## Code: 睡眠與喚醒 | 副程式 | 目標 | | -------- | -------------------------------------------- | | `sleep` | 將狀態改成 `SLEEPING`,呼叫 `sched` 釋放 CPU | | `wakeup` | 尋找狀態為 `SLEEPING` 的 process,改成 `RUNNABLE`| ### `sleep()` ```c=418 void sleep(void *chan, struct spinlock *lk) { struct proc *p = myproc(); if(p == 0) panic("sleep"); if(lk == 0) panic("sleep without lk"); ``` - 首先確保 process 存在,及持有鎖。 ```c=+ if(lk != &ptable.lock){ //DOC: sleeplock0 acquire(&ptable.lock); //DOC: sleeplock1 release(lk); } ``` - 接著檢查是否持有 `ptable->lock`,如果沒有則要求一個,把 `lk` 釋出。 ```c=+ // Go to sleep. p->chan = chan; p->state = SLEEPING; sched(); ``` - 睡眠,並呼叫 `sched` ```c=+ // Tidy up. p->chan = 0; // Reacquire original lock. if(lk != &ptable.lock){ //DOC: sleeplock2 release(&ptable.lock); acquire(lk); } } ``` ### `wakeup1()` ```c=458 static void wakeup1(void *chan) { struct proc *p; for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) if(p->state == SLEEPING && p->chan == chan) p->state = RUNNABLE; } ``` ### `wakeup()` ```c=468 void wakeup(void *chan) { acquire(&ptable.lock); wakeup1(chan); release(&ptable.lock); } ``` - `wakeup` 找到 `chan` 上在睡眠的 process,並喚醒。 --- ## Code: Pipes :::success **File:** pipe.c ::: - pipe 為一個結構,包含一個鎖,一個 `buf` 等。 - 當 pipe 為空時,`nread == nwrite` - 當 pipe 為滿時,`nwrite == nread % PIPESIZE` ```c=12 struct pipe { struct spinlock lock; char data[PIPESIZE]; uint nread; // number of bytes read uint nwrite; // number of bytes written int readopen; // read fd is still open int writeopen; // write fd is still open }; ``` ### `pipewrite()` ```c=77 int pipewrite(struct pipe *p, char *addr, int n) { int i; acquire(&p->lock); for(i = 0; i < n; i++){ while(p->nwrite == p->nread + PIPESIZE){ //DOC: pipewrite-full if(p->readopen == 0 || myproc()->killed){ release(&p->lock); return -1; } wakeup(&p->nread); sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep } p->data[p->nwrite++ % PIPESIZE] = addr[i]; } wakeup(&p->nread); //DOC: pipewrite-wakeup1 release(&p->lock); return n; } ``` - 首先須取得鎖。 - `pipewrite` 從 0 開始將資料讀入 `buf`,更新 `nwrite` 計數器,當 `buf` 滿了,則喚醒 `piperead` 並睡眠;或是讀入完畢,一樣喚醒 `pipiread`。 ### `piperead()` ```c=99 int piperead(struct pipe *p, char *addr, int n) { int i; acquire(&p->lock); while(p->nread == p->nwrite && p->writeopen){ //DOC: pipe-empty if(myproc()->killed){ release(&p->lock); return -1; } sleep(&p->nread, &p->lock); //DOC: piperead-sleep } for(i = 0; i < n; i++){ //DOC: piperead-copy if(p->nread == p->nwrite) break; addr[i] = p->data[p->nread++ % PIPESIZE]; } wakeup(&p->nwrite); //DOC: piperead-wakeup release(&p->lock); return i; } ``` - 首先須取得鎖。 - `piperead` 確認 pipe 是否為空,為空則進入睡眠。 - 當 pipe 不為空時,寫入資料,更新 `nread` 計數器。 - 讀取完畢後,喚醒 `pipewrite`。 --- ## Code: Wait, exit and kill :::success **File:** proc.c ::: ### `wait()` ```c=208 int wait(void) { struct proc *p; int havekids, pid; struct proc *curproc = myproc(); acquire(&ptable.lock); for(;;){ // Scan through table looking for exited children. havekids = 0; for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->parent != curproc) continue; havekids = 1; if(p->state == ZOMBIE){ // Found one. pid = p->pid; kfree(p->kstack); p->kstack = 0; freevm(p->pgdir); p->pid = 0; p->parent = 0; p->name[0] = 0; p->killed = 0; p->state = UNUSED; release(&ptable.lock); return pid; } } // No point waiting if we don't have any children. if(!havekids || curproc->killed){ release(&ptable.lock); return -1; } // Wait for children to exit. (See wakeup1 call in proc_exit.) sleep(curproc, &ptable.lock); //DOC: wait-sleep } } ``` - 首先須取得鎖。 - 接著尋找是否有子 process,如果有,並且還沒退出,則睡眠,等待子 process 退出。 - 如果找到已退出的子 process,紀錄該子 process 的 pid,清理 `struct proc`,釋放相關記憶體。 ### `exit()` ```c=166 void exit(void) { struct proc *curproc = myproc(); struct proc *p; int fd; if(curproc == initproc) panic("init exiting"); // Close all open files. for(fd = 0; fd < NOFILE; fd++){ if(curproc->ofile[fd]){ fileclose(curproc->ofile[fd]); curproc->ofile[fd] = 0; } } begin_op(); iput(curproc->cwd); end_op(); curproc->cwd = 0; acquire(&ptable.lock); // Parent might be sleeping in wait(). wakeup1(curproc->parent); // Pass abandoned children to init. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->parent == curproc){ p->parent = initproc; if(p->state == ZOMBIE) wakeup1(initproc); } } // Jump into the scheduler, never to return. curproc->state = ZOMBIE; sched(); panic("zombie exit"); } ``` - 首先須取得鎖。 - 喚醒父 process,將子 process 交給 *initproc*,修改狀態,呼叫 `sched` switch 至 scheduler。 ### `kill()` ```c=402 int kill(int pid) { struct proc *p; acquire(&ptable.lock); for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->pid == pid){ p->killed = 1; // Wake process from sleep if necessary. if(p->state == SLEEPING) p->state = RUNNABLE; release(&ptable.lock); return 0; } } release(&ptable.lock); return -1; } ``` - `kill` 將 `p->killed` 設為 1 ,當此 process 發生中斷或是 system call 進入 kernel,離開時 `trap` 會檢查 `p->killed`,如果被設置了,則呼叫 `exit`。 - 當要被 kill 的 process 處於睡眠狀態,則喚醒它。 ###### tags: `xv6` `kernel` `scheduler`