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

posted @ 2025-02-28 15:02  我们的歌谣  阅读(31)  评论(0)    收藏  举报