xv6 进程
xv6进程
第一个进程的创建和进入
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if(cpuid() == 0){
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
trapinit(); // trap vectors
trapinithart(); // install kernel trap vector
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache
iinit(); // inode table
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
scheduler();
}
上面是xv6从start进入到超级用户模式时的代码。xv6支持多核,那么操作系统怎么初始化呢?从上面代码看出,xv6主要都在CPU0进行初始化,其他CPU等待CPU0初始化完后,初始化页表,进行中断向量表的映射,使能PLIC(platform level interrupt controller)。那么CPU0做些啥呢?
首先初始化consle和printf。然后初始化物理内存分配器,创建kernel page table,STAP寄存器写入kernel page table。初始化page table的一些东西,初始化中断的一些东西(PLIC,中断向量表定向,中断等初始化),xv6文件系统相关的初始化(LOG层,inode层,文件层,buffer cache层等初始化)。然后所有CPU都进入scheduler(); 进行调度。
进程调度
总述
xv6的进程分为UNUSED,USED,RUNABLE,RUNNING,SLEEPING,ZOMBIE态,下面是这些态的转换关系。

到达USED态
USED态表示操作系统从PROC全局数组分配了一个proc结构体,但是进程相关的东西还没有准备好。
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;
}
// 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;
}
从上面的代码可以看出,在allocproc()函数里,xv6循环遍历proc数组,找到一个UNUSED的proc,然后进行初始化。当然由于P->State可能会被系统的多个任务同时使用,于是采用自旋锁来保证并发并行问题。然后为分配的进程分配trapframe以及一个空的用户表。p->context是called saved寄存器的集合,由于是新的进程,没有在kernel实现过函数,所以为0。ra赋值给forkret,代表该进程第一次调度时进入的是该函数。sp = p->kstack + PGSIZE,是因为RISCV calling convention中,栈帧是向下生长的。
到达RUNABLE态
RUNABLE态表示进程已经准备好了,可以进入调度。
从USED态进入
其实从USED进入RUNABLE态,就是要把进程相关的东西准备好,比如进程的代码段数据段,栈,堆,进程上下文的建立和初始化等等。XV6有两种方式从USED态进入RUNABLE态,分别是fork()和userinit(),userinit()是xv6 bootloader时调用的。
从sleeping态进入
从SLEEPING态进入RUNABLE态有两种方式,分别是kill()和wakeup()。但二者调用所代表的意义完全不同,调用kill代表要杀死这个进程,但是要等到该进程进入kernel space时杀死,而wakeup()是xv6同步机制的一部分,代表唤醒这个进程,详细后面讲。
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;
}
上面是kill()的代码,是p->killed为1,让该进程进入trap时被杀死。
从RUNNING态进入
在xv6里,RUNNING态进入RUNABLE态,代表该进程被CPU调度器暂时放弃了,CPU调度器正在要调度别的进程。详细可以看下面的yeild。
到达RUNING态
到达RUNNING态本质还是要调用scheduler,yeild本质还是让这个进程RUNABLE,然后选择一个进程RUNNING。
到达zombie态
xv6进程结束要么p->killed = 1,在trap里进行回收,要么exit(x)。否则就会出现一些问题,如果父进程结束了,子进程还在继续,那么子进程的父进程就会变为init()进程,这种子进程被称为孤儿进程,当然孤儿进程其实也没啥危害。如果子进程结束了,父进程没有使用wait或者waitpid就这个子进程进行回收,那么就是僵尸进程。
xv6调度讲解
void
yield(void)
{
struct proc *p = myproc();
acquire(&p->lock);
p->state = RUNNABLE;
sched();
release(&p->lock);
}
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get())
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}
.globl swtch
swtch:
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret
// Per-CPU process scheduler.
// 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();
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);
}
}
}
// 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);
}
usertrapret();
}
// Per-CPU state.
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()?
};
//noff是中断嵌套的深度,intena代表的是第一次使用push_off()时中断是否开启?
void
push_off(void)
{
int old = intr_get();
intr_off();
if(mycpu()->noff == 0)
mycpu()->intena = old;
mycpu()->noff += 1;
}
void
pop_off(void)
{
struct cpu *c = mycpu();
if(intr_get())
panic("pop_off - interruptible");
if(c->noff < 1)
panic("pop_off");
c->noff -= 1;
if(c->noff == 0 && c->intena)
intr_on();
}
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc();
// Must acquire p->lock in order to
// change p->state and then call sched.
// Once we hold p->lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup locks p->lock),
// so it's okay to release lk.
acquire(&p->lock); //DOC: sleeplock1
release(lk);
// Go to sleep.
p->chan = chan;
p->state = SLEEPING;
sched();
// Tidy up.
p->chan = 0;
// Reacquire original lock.
release(&p->lock);
acquire(lk);
}
//allocproc()中部分代码
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
首先,当一个进程被创建是,它context.ra = (uint64)forkret;现在假设进程被(sleep wakeup)(yeild)导致进程从RUNNING状态到RUNABLEZ状态,路径不同,但是他们进入sched前,进程自旋锁都被获取了。(userinit即bootloader时不一样)。
sched()先判断,是否满足一些条件,比如进入前,进程有自旋锁,且自旋锁的拥有者CPU是本CPU,否则panic。然后把本地CPU被push_off()前的属性值保存到本地CPU的context中,然后进行swtch()。
swtch()通过ret返回,但是它此时返回的是scheduler()的c->proc = 0;这一行代码,然后继续for循环,找到有RUNABLE的进程,然后获取到进程的自旋锁,再通过swtch()返回进程kernel 栈,如果该进程第一次调度,即返回到forket(),如果不是第一次调度,则返回到内核调用sched()的地方。
锁一般是本任务获取,本任务释放,但是这个代码是特殊的,sched()获取的锁,在scheduler()中swthch()下被释放,scheduler()中swtch()上被获取,在返回中被释放。
而且你观察,scheduler()中for循环是从0进程到最后一个进程的,保证了调度了公平性,保证每个进程都有可能被调度。
上面的过程可以被简单归纳为如下图。

多任务,中断和多CPU之间的同步手段
自旋锁
锁是操作系统里对于一定时间内对某一物体拥有者的抽象。多核和多任务在同一时间只有一个才能拥有某个物体的自旋锁。自旋锁获取不到锁时,会一直循环检测条件,所以自旋锁应该用于CPU获取时间比较小的任务。
void acquire(struct spinlock*);
int holding(struct spinlock*);
void initlock(struct spinlock*, char*);
void release(struct spinlock*);
void push_off(void);
void pop_off(void);
// Acquire the lock.
// Loops (spins) until the lock is acquired.
void
acquire(struct spinlock *lk)
{
push_off(); // disable interrupts to avoid deadlock.
if(holding(lk))
panic("acquire");
// On RISC-V, sync_lock_test_and_set turns into an atomic swap:
// a5 = 1
// s1 = &lk->locked
// amoswap.w.aq a5, a5, (s1)
while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
;
// Tell the C compiler and the processor to not move loads or stores
// past this point, to ensure that the critical section's memory
// references happen strictly after the lock is acquired.
// On RISC-V, this emits a fence instruction.
__sync_synchronize();
// Record info about lock acquisition for holding() and debugging.
lk->cpu = mycpu();
}
// Release the lock.
void
release(struct spinlock *lk)
{
if(!holding(lk))
panic("release");
lk->cpu = 0;
// Tell the C compiler and the CPU to not move loads or stores
// past this point, to ensure that all the stores in the critical
// section are visible to other CPUs before the lock is released,
// and that loads in the critical section occur strictly before
// the lock is released.
// On RISC-V, this emits a fence instruction.
__sync_synchronize();
// Release the lock, equivalent to lk->locked = 0.
// This code doesn't use a C assignment, since the C standard
// implies that an assignment might be implemented with
// multiple store instructions.
// On RISC-V, sync_lock_release turns into an atomic swap:
// s1 = &lk->locked
// amoswap.w zero, zero, (s1)
__sync_lock_release(&lk->locked);
pop_off();
}
首先看acquire(),pushoff()则是把本CPU的中断管掉,因为自旋锁不是递归锁,所以用holding来检查是否本CPU已经要递归获取,如果是,则panic。最后用比较与置换的原子指令(多核下的)来保证多个CPU只有一个可以获取到自旋锁,__sync_synchronize();用来告诉编译器不要乱序执行。
release则是用来保证如果不是同一个cpu和锁来获取,然后用比较与置换的原子指令来保证释放锁。
单核的原子操作可以用关中断或者保证单个指令不被大端,就可以实现原子操作,而多核和linux,则需要硬件上的保证(每家都不一样,看具体实现),才可以实现原子操作。
sleep()和wakeup()机制
上面的自旋锁在获取不到资源时,CPU会空转直到获取资源。对于磁盘等读写操作,进程要等待则要浪费很多资源,所以就应该有另一种方法,在获取不到时,让别的任务区执行,直到某个条件达成时,再返回去执行这个任务。代码分析看下面。
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc();
// Must acquire p->lock in order to
// change p->state and then call sched.
// Once we hold p->lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup locks p->lock),
// so it's okay to release lk.
acquire(&p->lock); //DOC: sleeplock1
release(lk);
// Go to sleep.
p->chan = chan;
p->state = SLEEPING;
sched();
// Tidy up.
p->chan = 0;
// Reacquire original lock.
release(&p->lock);
acquire(lk);
}
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
if(p != myproc()){
acquire(&p->lock);
if(p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
release(&p->lock);
}
}
}
// called at the start of each FS system call.
void
begin_op(void)
{
acquire(&log.lock);
while(1){
if(log.committing){
sleep(&log, &log.lock);
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
// this op might exhaust log space; wait for commit.
sleep(&log, &log.lock);
} else {
log.outstanding += 1;
release(&log.lock);
break;
}
}
}
// called at the end of each FS system call.
// commits if this was the last outstanding operation.
void
end_op(void)
{
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1;
if(log.committing)
panic("log.committing");
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space,
// and decrementing log.outstanding has decreased
// the amount of reserved space.
wakeup(&log);
}
release(&log.lock);
if(do_commit){
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit();
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
}
从上面的代码可以看出,sleep使用前,会获取对应的自旋锁,过后会释放自旋锁。而wakeup()直接赋值给那个chan就可以了。
锁意味着保护不变量,锁保护的不变量在锁释放后,情况就有可能改变;还有避免死锁;以及避免wakeup没唤醒一些进程。这就是sleep和wakeup中一些代码的原因。
wakeup()的代码保证了可以一唤多。
嵌套中断开关
void
push_off(void)
{
int old = intr_get();
intr_off();
if(mycpu()->noff == 0)
mycpu()->intena = old;
mycpu()->noff += 1;
}
void
pop_off(void)
{
struct cpu *c = mycpu();
if(intr_get())
panic("pop_off - interruptible");
if(c->noff < 1)
panic("pop_off");
c->noff -= 1;
if(c->noff == 0 && c->intena)
intr_on();
}
noff是中断嵌套的次数,intena是用来判断关中断前中断的情况。
多任务之间传递数据
xv6采用pipe父子进程传递数据;至于不同进程(非父子)之间传递数据,xv6并没有保证。
xv6进程与文件系统
// 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
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_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)
};
xv6只有一些文件IO。对于file descriptor这个抽象,0,1,2,3代表的是在proc结构体中struct file *ofile结构体数组的偏移,偏移所找到的文件就是file descriptor所对应的文件。
对于每个进程文件读写的同步,每个操作系统都有自己的一套同步机制,详细见xv6文件系统。
xv6进程与中断异常
本文以system call为的超级用户环境异常为例。
下面是xv6的内存布局。

程序首先把系统调用号放在a7上,然后调用ecall指令,因为提前设置了MEDELEG和MIDELEG寄存器,所以ecall进入了超级用户模式权限的中断。跳转中断的代码会在tramponline中执行,平常的进程是碰不到这个地方的,体现了进程了操作系统的隔离性。可以看如下图,是玄铁C906异常的响应流程,**如果是外部中断,那么需要PLIC(platform level interrupt control)的配合,xv6在这上面就
显的更加玩具了,就不说明了。

具体就不讲了,调到tramponline的useruvc函数,然后保存上下文,切换出内核页表,切换出内核栈,让中断响应为kerneltrap。
具体可以看下面链接https://www.cnblogs.com/LuRenZ1002/articles/18669413

浙公网安备 33010602011771号