xv6源码阅读——进程切换

本文深入剖析XV6操作系统中进程切换的具体流程,包括从用户进程到内核线程的转变、进程间的上下文切换及如何从内核线程返回用户线程等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说明

  • 阅读的代码是 xv6-riscv 版本的

概述

在这里插入图片描述
从一个用户进程(旧进程)切换到另一个用户进程(新进程)所涉及的步骤:

  • 通过中断机制,从trap机制进入内核线程
  • 调用swtch从该进程进行上下文切换,切换到到调度进程
  • 通过调度进程切换到新进程的内核线程
  • 在通过trap机制返回到新进程用户线程

上面过程就简单阐述,是怎样进行进程切换的
当然,里面会有很多细节,后面会详细介绍

从用户线程到内核线程

这一部分在上一章是有过介绍的,通过中断机制,通过uservec保存寄存器,然后进入usertrap()

usertrap

void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");
   //省略 ··········
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

其他部分就不过多赘述了(上一篇博客讲的很详细),通过判断中断类型,这是一个定时器中断,就调用yield()函数

从旧进程进入调度进程

yield

// Give up the CPU for one scheduling round.
void
yield(void)
{
  struct proc *p = myproc();
  acquire(&p->lock);
  p->state = RUNNABLE;
  sched();
  release(&p->lock);
}

该函数干的事情很简单,先获取该进程的锁,将进程状态设置为RUNNABLE
然后调用sched()函数进行后续操作
题:为什么再设置进程状态前,要先获取该进程的锁呢
因为一旦将进程状态设置为RUNNABLE,调度器就会认为该进程处于等待状态的可能会让他接下来运行,但实际上,该进程还是处于运行状态的
如果这是一个多核CPU的,就可能会发生一个进程被两个CPU同时运行,程序可能会立马崩溃。
所以我们要保证改变进程状态,保存寄存器,载入另一个进程的寄存器这三步是原子的。

sched

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;
}
  • 该函数操作也很简单,进行一系列判断(防御性编程)
  • 将mycpu()->intena保存下来,因为切换进程后,他会被改变
  • 调用swtch切换上下文,该函数完成后就会进行入了调度进程
  • 恢复mycpu()->intena(要等到该进程再次拿到CPU)

swtch

这是一段汇编代码,主要干的工作是将该进程的寄存器保存下来,然后载入调度进程的寄存器


.globl swtch
swtch:
	# 保存寄存器
        sd ra, 0(a0)
     # 省略```````````````````
        sd s11, 104(a0)
   # 恢复所要切换进程的寄存器
        ld ra, 0(a1)
     # 省略```````````````````
        ld s11, 104(a1)
      
        ret

问题:为什么RISC-V中有32个寄存器,但是swtch函数中只保存并恢复了14个寄存器?
因为swtch函数是从C代码调用的,所以我们知道Caller Saved Register会被C编译器保存在当前的栈上。

该函数完成后,就进入了调度进程

调度

scheduler

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    //通过确保设备可以中断来避免死锁。
    intr_on();

    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // 切换到所选的进(释放它的锁,然后在跳回我们之前重新获取它)
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->context, &p->context);

        // 进程目前已完成运行。
        c->proc = 0;
      }
      release(&p->lock);
    }
  }
}
  • 进程切换过来,是切换到swtch(&c->context, &p->context)这一行的,后面他会先释放锁
    (避免该进程永远无法被调度,该锁是在yield()函数中获取的)

  • scheduler在进程表上循环查找可运行的进程,该进程具有p->state == RUNNABLE。一旦找到一个进程,它将设置CPU当前进程变量c->proc,将该进程标记为RUNINING,然后调用swtch开始运行它

  • scheduler在一开始要获取进程锁,将RUNNABLE进程转换为RUNNING,在内核线程完全运行之前(在swtch之后,例如在yield中)绝不能释放锁。

调用swtch后就会进入另一个进程了

从调度进程进入新进程

该过程也是在完成swtch后完成切换的
他会从sched函数返回

void
sched(void)
{
  int intena;
  struct proc *p = myproc();

  //、、、、、、、、、、、、、、
  swtch(&p->context, &mycpu()->context);
  mycpu()->intena = intena;
}

他会返回到swtch处,然后恢复 mycpu()->intena

从内核线程返回用户线程

这后面就是一步步返回,通过trap机制返回用户层,在上一篇博客讲的很详细,不过多赘述

XV6线程第一次调用swtch函数

allocproc

static struct proc*
allocproc(void)
{
  struct proc *p;
  //、、、、、、、、、、省略
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

在创建进程时,会设置ra和sp
ra很重要,因为这是进程的第一个switch调用会返回的位置。同时因为进程需要有自己的栈,所以ra和sp都被设置了。这里设置的forkret函数就是进程的第一次调用swtch函数会切换到的“另一个”线程位置。

当调度线程将CPU交给该进程时,该进程会从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);
  }

  usertrapret();
}

从代码中看,它的工作其实就是释放调度器之前获取的锁。函数最后的usertrapret函数其实也是一个假的函数,它会使得程序表现的看起来像是从trap中返回,但是对应的trapframe其实也是假的,这样才能跳到用户的第一个指令中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

binary~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值