---
# System prepended metadata

title: XV6 Ch5 Scheduling
tags: [kernel, scheduler, xv6]

---

# 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`