6.S081参考书笔记 —— 第7章调度、进程交互

第七章 调度 笔记

主要讲了进程切换的细节

进程切换

  • 进程切换的两种情况

    • 第一:当进程等待设备或管道I/O完成,或等待子进程退出,或在sleep系统调用中等待时,xv6使用睡眠(sleep)和唤醒(wakeup)机制切换。
    • 第二:xv6周期性地强制切换以处理长时间计算而不睡眠的进程。这种多路复用产生了每个进程都有自己的CPU的错觉,就像xv6使用内存分配器和硬件页表来产生每个进程都有自己内存的错觉一样。
  • 进程切换的实现问题

    • 原理——上下文切换
    • 如何以对用户进程透明的方式强制切换? —— 定时器中断驱动上下文切换(系统内核中执行)
    • 如何释放原来进程的内存以及其他资源? —— CPU的每个核心都有一个switch内核线程,且核心得记住自己在执行那个进程
    • sleep允许一个进程放弃CPU,wakeup允许另一个进程唤醒第一个进程。 —— 需要小心避免导致唤醒通知丢失的竞争。
  • 上下文切换

    • 从一个用户进程(旧进程)切换到另一个用户进程(新进程)所涉及的步骤:

      • 一个到旧进程内核线程的用户-内核转换(系统调用或中断),
      • 一个到当前CPU调度程序线程的上下文切换,一个到新进程内核线程的上下文切换,
      • 以及一个返回到用户级进程的trap。
    • 函数swtch为内核线程切换执行保存和恢复操作。swtch对线程没有直接的了解;它只是保存和恢复寄存器集,称为上下文(contexts)。当某个进程要放弃CPU时,该进程的内核线程调用swtch来保存自己的上下文并返回到调度程序的上下文。

    • Swtchkernel/swtch.S:3)只保存被调用方保存的寄存器(callee-saved registers);调用方保存的寄存器(caller-saved registers)通过调用C代码保存在栈上(如果需要)。Swtch知道struct context中每个寄存器字段的偏移量。它不保存程序计数器。但swtch保存ra寄存器,该寄存器保存调用swtch的返回地址。现在,swtch从新进程的上下文中恢复寄存器,该上下文保存前一个swtch保存的寄存器值。当swtch返回时,它返回到由ra寄存器指定的指令,即新线程以前调用swtch的指令。另外,它在新线程的栈上返回。

      PC程序计数器会随着函数调用更新

6.S081参考书笔记 —— 第7章调度、进程交互_第1张图片

  • 调度线程

    • 调度器(scheduler)以每个CPU上一个特殊线程的形式存在,每个线程都运行scheduler函数。此函数负责选择下一个要运行的进程。想要放弃CPU的进程必须先获得自己的进程锁p->lock,并释放它持有的任何其他锁,更新自己的状态(p->state),然后调用sched
    • 锁的传递 —— 特例且必要
      • xv6在对swtch的调用中持有p->lockswtch的调用者必须已经持有了锁,并且锁的控制权传递给切换到的代码。这种约定在锁上是不寻常的;通常,获取锁的线程还负责释放锁,这使得对正确性进行推理更加容易。对于上下文切换,有必要打破这个惯例,因为p->lock保护进程statecontext字段上的不变量,而这些不变量在swtch中执行时不成立。

sleep & wakeup wake传递锁、sleep释放锁

调度和锁有助于隐藏一个进程对另一个进程的存在,但到目前为止,我们还没有帮助进程进行有意交互的抽象。为解决这个问题已经发明了许多机制。Xv6使用了一种称为sleepwakeup的方法,它允许一个进程在等待事件时休眠,而另一个进程在事件发生后将其唤醒。睡眠和唤醒通常被称为序列协调(sequence coordination)或条件同步机制(conditional synchronization mechanisms)。

为了说明,让我们考虑一个称为信号量(semaphore)的同步机制,它可以协调生产者和消费者。信号量维护一个计数并提供两个操作。“V”操作(对于生产者)增加计数。“P”操作(对于使用者)等待计数为非零,然后递减并返回。如果只有一个生产者线程和一个消费者线程,并且它们在不同的CPU上执行,并且编译器没有进行过积极的优化,那么此实现将是正确的:

struct semaphore {
    struct spinlock lock;
    int count;
};

void V(struct semaphore* s) {
    acquire(&s->lock);
    s->count += 1;
    release(&s->lock);
}

void P(struct semaphore* s) {
    while (s->count == 0)
        ;
    acquire(&s->lock);
    s->count -= 1;
    release(&s->lock);
}

上面的实现代价昂贵。如果生产者很少采取行动,消费者将把大部分时间花在while循环中,希望得到非零计数。消费者的CPU可以找到比通过反复轮询s->count繁忙等待更有成效的工作。要避免繁忙等待,消费者需要一种方法来释放CPU,并且只有在V增加计数后才能恢复。

这是朝着这个方向迈出的一步,尽管我们将看到这是不够的。让我们想象一对调用,sleepwakeup,工作流程如下。Sleep(chan)在任意值chan上睡眠,称为等待通道(wait channel)。Sleep将调用进程置于睡眠状态,释放CPU用于其他工作。Wakeup(chan)唤醒所有在chan上睡眠的进程(如果有),使其sleep调用返回。如果没有进程在chan上等待,则wakeup不执行任何操作。我们可以将信号量实现更改为使用sleepwakeup(更改的行添加了注释):

void V(struct semaphore* s) {
    acquire(&s->lock);
    s->count += 1;
    wakeup(s);  // !pay attention
    release(&s->lock);
}

void P(struct semaphore* s) {
    while (s->count == 0)
        sleep(s);  // !pay attention
    acquire(&s->lock);
    s->count -= 1;
    release(&s->lock);
}

P现在放弃CPU而不是自旋,这很好。然而,事实证明,使用此接口设计sleepwakeup而不遭受所谓的丢失唤醒(lost wake-up)问题并非易事。假设P在第9行发现s->count==0。==当P在第9行和第10行之间时,V在另一个CPU上运行:它将s->count更改为非零,并调用wakeup,这样就不会发现进程处于休眠状态,因此不会执行任何操作。现在P继续在第10行执行:它调用sleep并进入睡眠。这会导致一个问题:P正在休眠,等待调用V,而V已经被调用。==除非我们运气好,生产者再次呼叫V,否则消费者将永远等待,即使count为非零。

这个问题的根源是V在错误的时刻运行,违反了P仅在s->count==0时才休眠的不变量。保护不变量的一种不正确的方法是将锁的获取(下面以黄色突出显示)移动到P中,以便其检查count和调用sleep是原子的:

void V(struct semaphore* s) {
    acquire(&s->lock);
    s->count += 1;
    wakeup(s);
    release(&s->lock);
}

void P(struct semaphore* s) {
    acquire(&s->lock);  // !pay attention
    while (s->count == 0)
        sleep(s);
    s->count -= 1;
    release(&s->lock);
}

人们可能希望这个版本的P能够避免丢失唤醒,因为锁阻止V在第10行和第11行之间执行。它确实这样做了,但它会导致死锁:P在睡眠时持有锁,因此V将永远阻塞等待锁。

**==我们将通过更改sleep的接口来修复前面的方案:调用方必须将条件锁(condition lock)传递给sleep,以便在调用进程被标记为asleep并在睡眠通道上等待后sleep可以释放锁。==如果有一个并发的V操作,锁将强制它在P将自己置于睡眠状态前一直等待,因此wakeup将找到睡眠的消费者并将其唤醒。**一旦消费者再次醒来,sleep会在返回前重新获得锁。我们新的正确的sleep/wakeup方案可用如下(更改以黄色突出显示):

void V(struct semaphore* s) {
    acquire(&s->lock);
    s->count += 1;
    wakeup(s);
    release(&s->lock);
}

void P(struct semaphore* s) {
    acquire(&s->lock);

    while (s->count == 0)
        sleep(s, &s->lock);  // !pay attention    sleep后释放锁、sleep返回获取锁
    s->count -= 1;
    release(&s->lock);
}

P持有s->lock的事实阻止VP检查s->count和调用sleep之间试图唤醒它。然而请注意,我们需要sleep释放s->lock并使消费者进程进入睡眠状态的操作是原子的。

你可能感兴趣的:(MIT,6.S081课程记录,操作系统)