xv6ProcessControlBlock - ccc-sp/riscv2os GitHub Wiki

xv6: 行程的資料結構

行程的資料結構,作業系統課本裏常稱為《行程控制區》(Process Control Block, PCB)。

xv6 的 PCB 記錄在 proc.h 的 struct proc 當中,包含 context 內文與 pagetable 分頁表。

kernel/proc.h


enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; // 行程狀態

// 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 (等待 channel)
  int killed;                  // If non-zero, have been killed (行程已死)
  int xstate;                  // Exit status to be returned to parent's wait (exit 的返回值)
  int pid;                     // Process ID (行程代號)

  // proc_tree_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) (該行程的名稱)
};

除了行程原本的堆疊之外,還有一個核心堆疊 kstack,可以在進入 kernel 時儲存一些資料在核心堆疊區。

在 proc.c 裏宣告了行程表,可以用來創建最多 NPROC 個行程。

kernel/proc.c

// 行程管理模組 (process)
struct cpu cpus[NCPU];    // 處理器 (核心)

struct proc proc[NPROC];  // 行程

struct proc *initproc;    // init 行程:第一個被啟動的使用者行程

行程表被初始化時,會先設定鎖與核心堆疊:

// initialize the proc table at boot time.
void
procinit(void) // 初始化行程表
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
  initlock(&wait_lock, "wait_lock");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");
      p->kstack = KSTACK((int) (p - proc));
  }
}

myproc() 可以傳回目前的行程記錄

// Return the current struct proc *, or zero if none.
struct proc*
myproc(void) { // 傳回目前行程
  push_off();
  struct cpu *c = mycpu();
  struct proc *p = c->proc;
  pop_off();
  return p;
}

當執行 fork() 動作時,會分配新的行程:

// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void) // 取得行程表中未使用的一格分配出去
{
  struct proc *p;
  // 尋找未使用的一格
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

found:
  p->pid = allocpid(); // 分配行程代號
  p->state = USED;     // 狀態改為已使用

  // Allocate a trapframe page. // 分配彈跳床頁
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // An empty user page table. // 初始化分頁表
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // 初始化內文區域,設定內文中的堆疊 sp 與 ra 
  // 返回位址 ra 設為 forkret(),這樣才會初始化檔案表等結構
  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

行程初始化時,會先分配一個 page table,一開始只有一個彈跳床頁:

// Create a user page table for a given process,
// with no user memory, but with trampoline pages.
pagetable_t
proc_pagetable(struct proc *p) // 創建新行程的分頁表 (只有一頁彈跳床)
{
  pagetable_t pagetable;

  // An empty page table.
  pagetable = uvmcreate(); // 創建空的分頁表
  if(pagetable == 0)
    return 0;

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  // 映射彈跳床頁 TRAMPOLINE 到實體頁 trampoline
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }
  // 將彈跳床後的那頁設為防護頁 (trapframe)
  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  return pagetable;
}

fork() 的動作,會複製並創建新行程:

// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void) // 行程 fork() 
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  if((np = allocproc()) == 0){ // 分配新的子行程
    return -1;
  }

  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){ // 將分頁表複製給子行程。
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz; // 子行程大小和父行程相同

  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe); // 暫存器也相同

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0; // 子行程的 fork 傳回值應為 0 (注意,父行程傳回值沒修改)

  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]); // 複製檔案表
  np->cwd = idup(p->cwd); // 複製 cwd 目前目錄 

  safestrcpy(np->name, p->name, sizeof(p->name)); // 複製名稱

  pid = np->pid;

  release(&np->lock);

  acquire(&wait_lock);
  np->parent = p;
  release(&wait_lock);

  acquire(&np->lock);
  np->state = RUNNABLE; // 設定子行程為 RUNNABLE
  release(&np->lock);

  return pid;
}

排程器會存取 PCB 中的資訊,挑選 RUNNABLE 的行程來執行。

// Per-CPU process scheduler. // 每個 CPU 都有自己的排程器
// 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();
    // 當回到本排程器時,挑選下一個 RUNNABLE 的行程來執行。
    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);
    }
  }
}

按下 Ctrl-P 時,會印出行程狀態


// Print a process listing to console.  For debugging.
// Runs when user types ^P on console.
// No lock to avoid wedging a stuck machine further.
void
procdump(void)
{
  static char *states[] = {
  [UNUSED]    "unused",
  [SLEEPING]  "sleep ",
  [RUNNABLE]  "runble",
  [RUNNING]   "run   ",
  [ZOMBIE]    "zombie"
  };
  struct proc *p;
  char *state;

  printf("\n");
  for(p = proc; p < &proc[NPROC]; p++){
    if(p->state == UNUSED)
      continue;
    if(p->state >= 0 && p->state < NELEM(states) && states[p->state])
      state = states[p->state];
    else
      state = "???";
    printf("%d %s %s", p->pid, state, p->name);
    printf("\n");
  }
}