xv6内核剖析 010

xv6内核剖析 010

今天继续我们之前的proc.hproc.c


void reparent(struct proc *p)

这个函数将一个将亡进程的子进程(也就是即将称为孤儿进程的子进程)交给进程树的根进程(第一个被初始化的进程)。我们可以称之为,托孤,哈哈哈

void
reparent(struct proc *p) // p是父进程
{
  struct proc *pp;
  // 遍历进程表找到父进程的所有子进程
  for(pp = proc; pp < &proc[NPROC]; pp++){
    // 注释:这段代码在访问pp->parent的时候并没有
    // 锁住pp->lock,因为如果pp或者pp的一个子进程
    // 正在exit()中,先锁住pp->lock可能会导致
    // 死锁
    if(pp->parent == p){
      // 必须先获取互斥锁在进程操作前,
      // 注释:pp->parent在检查和acquire()之间不能
      // 被改变,因为只有父进程能够改变它,并且当前进
      // 进程就是父进程。
      acquire(&pp->lock);
      pp->parent = initproc;
      // 注释:我们应该在这里唤醒init进程,但是那样
      // 我们需要获取initproc->lock,这将会导致死锁,
      // 因为我们已经锁住了init进程的一个子进程(pp),
      // 这就是为什么exit()总是会在获取任何的锁之前
      // 唤醒init进程
      release(&pp->lock);
    }
  }
}

说实话,本来很简单的代码,愣是被这个注释搞蒙蔽了,其实最主要的还是死锁预防,我们看看exit()先吧。

void exit(int status)

这个函数有很多地方值得我们学习的,尤其是死锁预防策略。

void
exit(int status)
{
  struct proc *p = myproc();

  if(p == initproc)
    panic("init exiting");

  for(int fd = 0; fd < NOFILE; fd++){
    if(p->ofile[fd]){
      struct file *f = p->ofile[fd];
      fileclose(f);
      p->ofile[fd] = 0;
    }
  }
    
  // cwd是进程当前所在的位置,
  begin_op();	// 开启一个事务transmition
  iput(p->cwd);	// 这是一个inode
  end_op();		// 提交事务
  p->cwd = 0;

  // 唤醒initproc进程
  // 将initproc->state设置成RUNNABLE
  acquire(&initproc->lock);
  wakeup1(initproc);
  release(&initproc->lock);
  // 注释:我们可能会将一个子进程的传递给init。我们
  // 无法精准地唤醒它,因为我们一旦获取了其他任何锁之后,
  // 我们就不能够获取init的进程锁。所以不管是否需要我们
  // 都需要唤醒init,init的唤醒信号可能会丢失,当时这
  // 看起来没有损害。
  
  acquire(&p->lock);
  struct proc *original_parent = p->parent;
  release(&p->lock);
  
  // 严格保持锁的获取顺序,
  // 不然会导致死锁
  acquire(&original_parent->lock);

  acquire(&p->lock);

  reparent(p);

  wakeup1(original_parent);

  p->xstate = status;
  p->state = ZOMBIE;

  release(&original_parent->lock);
  
  // 调度
  sched();
  panic("zombie exit");
}

wait(uint64 addr)

等待一个子进程退出并将它的进程id返回

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++){
      // 避免死锁,所以不需要锁住np->parent
      if(np->parent == p){
        acquire(&np->lock);
        havekids = 1;
        if(np->state == ZOMBIE){
          // Found one.
          // 找到一个退出的子进程
          pid = np->pid;
          // 将子进程的进程id复制到
          // 用户空间中指定的位置
          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
  }
}

进程调度器void scheduler(void)

这个函数涉及到进程上下文切换(swith context),第一次看的话可能会比较抽象。

但是理解这个进程的上下文切换有助于我们理解协程(corutine),线程(thread)切换。

回顾一下前面,我们在讨论struct cpu的时候提到,在这个cpu的结构体中有一个context成员属性,这个context就是保存了cpu对应的调度器的上下文,在调度的时候,被调度程序用struct cpu中的context取代原本的context,这样就切换到了调度函数中了,就能够进行调度,而原来的进程就作为一个就绪状态调度进程等待cpu的调度。一般来说,引起上下文切换的原因主要是:

  • 用户进程调用了yield
  • 时钟中断产生,强制进行上下文切换,但是在xv6中,acquire的调用会使当前的cpu屏蔽中断。不进行切换。其实这在linux中的rcu(read-copy-update)中也有体现,可以去看看我之前写的文章
void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();
    
    int found = 0;
    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      // 在进程表中找到一个处于就绪状态的进程
      // 并运行它
      if(p->state == RUNNABLE) {
        p->state = RUNNING;
        c->proc = p;
        // 上下文切换将被调度的进程上下文切换到当前cpu中
        // 这里需要我们注意--->我们在下面展开说
        // 先把代码看完
        swtch(&c->context, &p->context);
		
        // 又回到调度线程中或者没有找到处于
        // 就绪状态的进行
        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
    // 如果当前没有就绪状态的进程,cou就陷入休眠
    if(found == 0) {
      // 打开中断
      intr_on();
      // 下面详细说明
      asm volatile("wfi");
    }
  }
}

这个函数有两点需要说明的

  1. swtch(&c->context, &p->context);

    调度器执行完这个函数,并不会返回,因为这个函数底层是汇编代码,它改变了一系列寄存器的值:加载了被调度进程的上下文,并且将原来的调度器的上下文保存cpu中的context属性中以备下一次调度,我们可以理解为调度器在调用完swtch()之后就被暂停了,然后cpu通过ra寄存器返回了被调度进程上下文,那什么时候调度器的上下文才会被加载呢?这跟我们之前说的是一样的,(当运行的进程调用了yield或者一个时钟中断(以软中断的形式产生)时)。

  2. asm volatile("wfi");

    这段内联汇编是什么意思呢?我们直接看满分回答

    使用 C 语言的内联汇编语法(asm 关键字),插入了一条对处理器的特殊指令 "wfi"。这里的 volatile 关键字表明这条汇编指令对程序行为有重要影响,编译器不应对其进行优化。"wfi" 是一个广泛应用于基于ARM架构和其他某些处理器体系结构的指令,其全称为 “Wait For Interrupt”(等待中断)。

    当执行到这条指令时,处理器会进入低功耗模式,停止执行任何指令(除了监控中断请求),直到接收到一个有效的中断信号。这样做的目的是节省能源,同时保持对中断事件的即时响应能力。一旦有任何中断发生,处理器将立即退出 wfi 模式,处理相应的中断服务例程,然后返回到执行流的下一条指令(即 wfi 后面的代码)。

    综上所述,这段代码的主要作用是:

    • 当变量found == 0时,
      • 开启处理器的中断功能,以便能及时响应外部事件;
      • 通过执行 wfi 指令,使处理器进入低功耗、等待中断的状态,直到有中断发生。

    这样的设计常出现在实时系统或嵌入式系统中,用于在满足特定条件时,使系统进入节能模式并等待外部事件触发进一步操作。

sched(void)

这个函数是用户态进程在内核空间中切换到调度器线程时调用的

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

yield(void)

进程让出cpu,比较简单

void
yield(void)
{
  struct proc *p = myproc();
  acquire(&p->lock);
  p->state = RUNNABLE;
  sched();
  release(&p->lock);
}

forkret(void)

fork系统调用从内核空间中返回

void
forkret(void)
{
  static int first = 1;

  // Still holding p->lock from scheduler.
  release(&myproc()->lock);

  if (first) {
    // 初始化文件系统,只初始化一次
    first = 0;
    fsinit(ROOTDEV);
  }
  // 返回用户空间
  usertrapret();
}

为什么需要在第一次fork返回之前初始化文件系统呢?

注释里面说到了,在fsinit中有sleep之类的调用,所以需要在进程中调用,也就是说,文件系统是由init进程进行初始化的。因为进程可能需要从外部存储设备中读取或者写入数据所以需要在fork返回之前调用,这个时候,进行已经初始化好了,就相当于内核将文件系统初始化的操作均摊在这一次fork中(类似于我们高中时候说的上课的小动作)。


sleep(void *chan, struct spinlock *k)

单单看这个声明就有点像我们平常开发中用到的条件变量(condition variable),我们可以将chan(channal)看成是一个条件变量,k看成是平时用到的互斥锁

void
sleep(void *chan, struct spinlock *lk)
{
  struct proc *p = myproc();
  
  // 获取进程锁,防止唤醒信号的丢失
  if(lk != &p->lock){  //DOC: sleeplock0
    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.
  // 恢复调用sleep之前的状态,重新获取互斥锁
  if(lk != &p->lock){
    release(&p->lock);
    acquire(lk);
  }
}

还有几个函数就能结束了,我们明天继续吧。

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值