xv6源码精读(一)——内核代码中进程操作相关proc.c
proc.c主要用来完成进程在内核的init、kill、scheduler、exit、fork等操作,与进程的资源分配、回收、新建进程、调度等相关的函数实现。
定义与工具函数
struct cpu cpus[NCPU];//cpu核的抽象
// cpu的抽象如下
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()?
};
struct proc proc[NPROC];//process的抽象
//process的抽象
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
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
// 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)
};
struct proc *initproc;
int nextpid = 1;
struct spinlock pid_lock;
extern void forkret(void);//fork后调用,用于在内核fork出的子进程返回用户态之前的必须执行的回调函数
static void wakeup1(struct proc *chan);//区别于wakeup,wakeup搭配sleep完成资源上的唤醒。wakeup1进用于子进程退出后触发父进程的资源回收
static void freeproc(struct proc *p);//释放process
extern char trampoline[]; // trampoline.S
进程运行必须有进程的内核栈,否则在内核态的资源无法保存,如下函数将分配内核栈,但该内核栈与进程槽位绑定,其生命周期并不随着进程创建和消亡:
void
proc_mapstacks(pagetable_t kpgtbl) {
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);//所以这个va是栈顶(栈页底部就是栈顶,因为栈向下增长)
}
}
初始化进程表(注意不是初始化进程,是proc[Nproc]这个进程的表):
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
p->kstack = KSTACK((int) (p - proc));
}
}
获取当前运行进程的cpuid
// Must be called with interrupts disabled,
// to prevent race with process being moved
// to a different CPU.
int
cpuid()
{
int id = r_tp();//tp寄存器在内核态就是cpuid,并由内核函数确保其不会被修改(硬件不确保)
return id;
}
获取当前进程结构体:
// Return this CPU's cpu struct.
// Interrupts must be disabled.
struct cpu*
mycpu(void) {
int id = cpuid();
struct cpu *c = &cpus[id];
return c;
}
获取当前CPU核心在运行的process,所以流程就是,关中断(避免tp寄存器被修改,注意这个是push_off不是intr_off,因为有可能嵌套中断,为了记录中断嵌套层数使用push_off)-> 获取当前CPU核心 -> 获取当前CPU核心运行的process结构 -> 开中断(pop_off),这里关于中断开关的问题可以移步关于spinlock.c的代码精度部分。
// 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;
}
分配pid,注意xv6的pid是单调递增的,pid不会被复用,在教学操作系统不会溢出,而真正的操作系统会做PID的回收。此处的pid_lock确保了分配pid和pid自增的逻辑是原子的:
int
allocpid() {
int pid;
acquire(&pid_lock);
pid = nextpid;
nextpid = nextpid + 1;
release(&pid_lock);
return pid;
}
以上都是一些工具函数,以下则是真正对process的操作
分配与释放process。对于分配process而言,需要注意这里的是:
- trapframe页是创建process的时候动态分配的(因为它是进程资源),内核栈算内核资源所以是在boot的时候就分配的。
- 正常allocproc创建的proc在函数结束的时候并不会release进程锁,而是交给它的调用者userinit、fork释放
- 这里规定了p->context.ra = (uint64)forkret;的作用是,由于被创建的进程是没有内核态的上下文的,所以一开始被调度运行就会出错,由于一般的调度运行的醒来的地方是sched(调用链见https://blog.csdn.net/weixin_52764246/article/details/157391164?spm=1001.2014.3001.5502),所以forkret也是类似sched+yeild类似的事情,包括释放p->lock等(虽然应该是yeild内的)、初始化filesystem、调用usertrapret等(手动调用也是因为,没有对应的内核栈,如果正常被调度则yeild运行结束就会回到usertrap执行中断的位置,自动调用usertrapret,但是第一次运行没有这些,就需要手动规定)
- 这里规定了p->context.sp = p->kstack + PGSIZE;的作用是让p的内核栈达到kstack的栈顶,原因是p->kstack其实是栈顶,而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();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
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;
}
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));//上下文初始化为0,这个上下文指的是内核运行process的内核态的调度上下文
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
// 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.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
// 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;
}
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}
关于forkret,是进程在被创建之后返回用户态之前进行的初始化操作,如下:
// A fork child's very first scheduling by scheduler()
// will swtch to forkret.
void
forkret(void)
{
static int first = 1;
// Still holding p->lock from scheduler.
release(&myproc()->lock);
if (first) {
// File system initialization must be run in the context of a
// regular process (e.g., because it calls sleep), and thus cannot
// be run from main().
first = 0;
fsinit(ROOTDEV);//关于filesystem的初始化操作,可见fs.c,详见对文件系统的源码精读
}
usertrapret();
}
在内核态创建完的进程,回到用户态从哪里运行呢,这里分为两种情况,一种是0号进程Init进程的运行,另一个是fork出来的进程的运行,init进程走userinit,fork则从fork函数调用点醒来(因为fork的时候,页表trapframe、打开文件等等,一并被复制了),init进程涉及到的如下:
// a user program that calls exec("/init")
// od -t xC initcode
uchar initcode[] = {
0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,
0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,
0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,
0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,
0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
// Set up first user process.
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// allocate one user page and copy init's instructions
// and data into it.
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// prepare for the very first "return" from kernel to user.
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
p->state = RUNNABLE;
release(&p->lock);
}
fork则涉及到父子进程的行为,此处把设计父子进程的操作一并说明:
- fork主要用来从父进程创建子进程的系统调用,首先创建一个proc,然后将父进程的页表项深拷贝到子进程,拷贝size,拷贝trapframe,但是修改a0返回值(因为这个fork在父进程要返回子进程的pid,在子进程返回0,从而让他们各自在fork之后可以区分父子),拷贝打开的文件描述符,修改子进程状态为RUNNABLE就绪,释放子进程的lock
- reparent寻找p的子进程,把其子进程托付给init进程
- exit为退出进程,比如进程A在自己的进程中调用了exit使A退出,那么应该关闭其打开的所有文件描述符,释放 A 对当前工作目录 inode 的一次引用,然后唤醒Init进程用于回收进程A的子进程中可能出现的ZOMBIE进程,唤醒A的父进程用于回收进程A的资源,修改进程A的所有子进程的父进程为Init进程托管,然后修改本进程的状态为ZOMBIE,然后调用sched让出CPU。
- wait的作用就是等待其子进程退出,所以作用自然就是获取p的锁,然后循环查找是否有自己的子进程成为了ZOMBIE进程,有的话就把np->states拷贝到对应的addr位置,然后释放该子进程,否则就睡眠在自己的proc struct p上,等exit的时候wakeup1会用这个channel来叫醒的。
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
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;
np->parent = p;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 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);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
// Pass p's abandoned children to init.
// Caller must hold p->lock.
void
reparent(struct proc *p)
{
struct proc *pp;
for(pp = proc; pp < &proc[NPROC]; pp++){
// this code uses pp->parent without holding pp->lock.
// acquiring the lock first could cause a deadlock
// if pp or a child of pp were also in exit()
// and about to try to lock p.
if(pp->parent == p){
// pp->parent can't change between the check and the acquire()
// because only the parent changes it, and we're the parent.
acquire(&pp->lock);
pp->parent = initproc;
// we should wake up init here, but that would require
// initproc->lock, which would be a deadlock, since we hold
// the lock on one of init's children (pp). this is why
// exit() always wakes init (before acquiring any locks).
release(&pp->lock);
}
}
}
// Exit the current process. Does not return.
// An exited process remains in the zombie state
// until its parent calls wait().
void
exit(int status)
{
struct proc *p = myproc();
if(p == initproc)
panic("init exiting");
// Close all open files.
for(int fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd]){
struct file *f = p->ofile[fd];
fileclose(f);
p->ofile[fd] = 0;
}
}
begin_op();
iput(p->cwd);
end_op();
p->cwd = 0;
// we might re-parent a child to init. we can't be precise about
// waking up init, since we can't acquire its lock once we've
// acquired any other proc lock. so wake up init whether that's
// necessary or not. init may miss this wakeup, but that seems
// harmless.
acquire(&initproc->lock);
wakeup1(initproc);
release(&initproc->lock);
// grab a copy of p->parent, to ensure that we unlock the same
// parent we locked. in case our parent gives us away to init while
// we're waiting for the parent lock. we may then race with an
// exiting parent, but the result will be a harmless spurious wakeup
// to a dead or wrong process; proc structs are never re-allocated
// as anything else.
acquire(&p->lock);
struct proc *original_parent = p->parent;
release(&p->lock);
// we need the parent's lock in order to wake it up from wait().
// the parent-then-child rule says we have to lock it first.
acquire(&original_parent->lock);
acquire(&p->lock);
// Give any children to init.
reparent(p);
// Parent might be sleeping in wait().
wakeup1(original_parent);
p->xstate = status;
p->state = ZOMBIE;
release(&original_parent->lock);
// Jump into the scheduler, never to return.
sched();
panic("zombie exit");
}
// Wait for a child process to exit and return its pid.
// Return -1 if this process has no children.
int
wait(uint64 addr)
{
struct proc *np;
int havekids, pid;
struct proc *p = myproc();
// hold p->lock for the whole time to avoid lost
// wakeups from a child's exit().
acquire(&p->lock);
for(;;){
// Scan through table looking for exited children.
havekids = 0;
for(np = proc; np < &proc[NPROC]; np++){
// this code uses np->parent without holding np->lock.
// acquiring the lock first would cause a deadlock,
// since np might be an ancestor, and we already hold p->lock.
if(np->parent == p){
// np->parent can't change between the check and the acquire()
// because only the parent changes it, and we're the parent.
acquire(&np->lock);
havekids = 1;
if(np->state == ZOMBIE){
// Found one.
pid = np->pid;
if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate,
sizeof(np->xstate)) < 0) {
release(&np->lock);
release(&p->lock);
return -1;
}
freeproc(np);
release(&np->lock);
release(&p->lock);
return pid;
}
release(&np->lock);
}
}
// No point waiting if we don't have any children.
if(!havekids || p->killed){
release(&p->lock);
return -1;
}
// Wait for a child to exit.
sleep(p, &p->lock); //DOC: wait-sleep
}
}
讲完退出相关的,就看调度相关的,调度可以参考https://blog.csdn.net/weixin_52764246/article/details/157391164?spm=1001.2014.3001.5502
kill进程则如下,主要流程包括将对应PID的进程的状态变为RUNNAABLE,然后将kiiled置为1,这样在瑕疵调度的时候,就可以被回收掉:
// Kill the process with the given pid.
// The victim won't exit until it tries to return
// to user space (see usertrap() in trap.c).
int
kill(int pid)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock);
if(p->pid == pid){
p->killed = 1;
if(p->state == SLEEPING){
// Wake process from sleep().
p->state = RUNNABLE;
}
release(&p->lock);
return 0;
}
release(&p->lock);
}
return -1;
}
// Copy to either a user address, or kernel address,
// depending on usr_dst.
// Returns 0 on success, -1 on error.
int
either_copyout(int user_dst, uint64 dst, void *src, uint64 len)
{
struct proc *p = myproc();
if(user_dst){
return copyout(p->pagetable, dst, src, len);
} else {
memmove((char *)dst, src, len);
return 0;
}
}
// Copy from either a user address, or kernel address,
// depending on usr_src.
// Returns 0 on success, -1 on error.
int
either_copyin(void *dst, int user_src, uint64 src, uint64 len)
{
struct proc *p = myproc();
if(user_src){
return copyin(p->pagetable, dst, src, len);
} else {
memmove(dst, (char*)src, len);
return 0;
}
}
// 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");
}
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)