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");
}
}