背景

互斥

  • Peterson 算法
  • 原子指令和自旋锁

并发编程

  • 数据竞争
  • 并发数据结构

大家已经掌握了越来越多编写并发程序的工具了!

本次课内容与目标

复习:操作系统内核中的并发编程

  • 写一个能得分的 OSLab1

学习 (操作系统内核中) 自旋锁的实现

  • 分析操作系统内核代码中自旋锁的问题
  • 欣赏 xv6 自旋锁实现
  • 调试多处理器操作系统内核 (OSLab 必备生存技能)

回到操作系统内核

多处理器上的操作系统内核

多处理器系统

  • 状态:$(M, R_1, R_2, \ldots)$
  • 在没有中断的多处理器系统,每个处理器运行一个 “线程” (Lab1)
  • 每个处理器独立响应中断,并在线程间切换 (Lab2)

AbstractMachine 和多处理器

bool mpe_init    (void (*entry)());       // 启动多处理器
int  cpu_count   (void);                  // 返回 CPU 数量
int  cpu_current (void);                  // 返回当前 CPU 编号
int  atomic_xchg (int *addr, int newval); // xchg()

在内核中实现自旋锁

sum-baremetal.c

typedef struct {
  int locked;
} lock_t;
#define LOCK_INIT()     ((lock_t) { .locked = 0 })
void lock(lock_t *lk)   { while (atomic_xchg(&lk->locked, 1)); }
void unlock(lock_t *lk) { atomic_xchg(&lk->locked, 0); }
static lock_t lk = LOCK_INIT();

lock(&lk);
sum++;
unlock(&lk);

Lab1: PMM

通过一个 Easy Test Case 很容易:

void *malloc(size_t sz) {
  void *ret;
  lock(&malloc_lock);
  ret = trivial_malloc(sz);
  unlock(&malloc_lock);
  return ret;
}

void free(void *ptr) {
  lock(&malloc_lock);
  trivial_free(ptr);
  unlock(&malloc_lock);
}

操作系统中的自旋锁

自旋锁 v1: 刚才的版本

typedef struct {
  int locked;
} lock_t;
#define LOCK_INIT()     ((lock_t) { .locked = 0 })
void lock(lock_t *lk)   { while (atomic_xchg(&lk->locked, 1)); }
void unlock(lock_t *lk) { atomic_xchg(&lk->locked, 0); }

似乎挺不错的……

自旋:非常耗时的操作

和上节课一样:“一核有难,他人围观” 或者 “全体摸鱼”

Bug #1: 修复

在操作系统内核中不能随便 yield/睡眠

  • 操作系统维护了很多重要数据结构,更新不能被打断

void spawn() {
  lock(&procs->lock);
  // 中断/yield
  //    ... schedule()
  //    ... lock(&procs->lock);

  unlock(&procs->lock);
}

Bug #1: 修复 (cont'd)

关中断 + 自旋:关中断可以保证单处理器的互斥。

void spin_lock(spinlock_t &lk) {
  iset(false);
  while (atomic_xchg(&lk->locked, 1)) ;
}

void spin_unlock(spinlock_t &lk) {
  atomic_xchg(&lk->locked, 0);
  iset(true);
}

细节 (为什么?)

  • 先关中断再自旋;先释放锁再开中断
  • 不如画个状态机吧!

关中断也许比大家想象得要难一些……

void foo() {
  /* +-----*/ spin_lock(&lock_a);
  /* |     */
  /* |  +--*/ spin_lock(&lock_b);
  /* |  |  */ ...
  /* |  +--*/ spin_unlock(&lock_b);
  /* |     */ assert(ienabled() == false); // 仍然在临界区
  /* +-----*/ spin_unlock(&lock_a);
  /*       */ assert(ienabled() == true);  // 出临界区
}

(如何修复这个问题?)

关中断也许比大家想象得要难一些…… (cont'd)

void on_interrupt(_Event *ev, _Context *ctx) {
  assert(ienabled() == false);

  // 刚才原封不动的代码
  /* +-----*/ spin_lock(&lock_a);
  /* |     */
  /* |  +--*/ spin_lock(&lock_b);
  /* |  |  */ ...
  /* |  +--*/ spin_unlock(&lock_b);
  /* |     */ assert(ienabled() == false);
  /* +-----*/ spin_unlock(&lock_a);

  assert(ienabled() == false);
}

Bug #2: 修复

正确的实现

  • 在线程第一次 lock 时保存中断状态 (FL_IF in %rflags)、关闭中断
    • 此后线程独占 CPU 执行,将不会被切换
  • 在线程最后一次 unlock 时恢复保存的中断状态
    • lock -> “pushcli
    • unlock -> “popcli

flags 栈应该保存在何处?

  • 保存在锁里 (struct spinlock_t)?
  • 保存在线程 (struct task) 里?
  • 保存在 CPU (struct cpu_local) 里?

Bug #3: 重入 (Reentrance)

A standard joke is that a bug can be turned into a feature simply by documenting it (then theoretically no one can complain about it because it’s in the manual), or even by simply declaring it to be good. “That’s not a bug, that’s a feature!” is a common catchphrase.

// It's not a bug, it's a feature!
lock(&lk);
lock(&lk);
printf(...);
unlock(&lk);
unlock(&lk);

(不要笑……经过三次函数调用你也不知道是否持有 lk 了)

操作系统中的自旋锁:小结

关中断 + 自旋;用来保护一段较短的临界区

  • 持有锁期间,线程 (处理器) 不能被中断
  • 其他等待锁的线程 (处理器) 在关中断的前提下自旋
    • “无法等待、必须立即执行否则不能继续” 的场景

自旋锁在操作系统中的应用

  • 在少量步数之内结束的并发数据结构操作
    • 操作执行不成功,系统剩余部分无法继续运行
    • 拥堵严重的资源:一核享用,九核围观
      • S. Boyd-Wickizer, et al. An analysis of Linux scalability to many cores. In Proc. of OSDI, 2010.

xv6 自旋锁实现

xv6: 世界上最好的教学入门操作系统

Xv6 is a teaching operating system developed in the summer of 2006, which we ported xv6 to RISC-V for a new undergraduate class 6.S081.

作者: Russ Cox, Frans Kaashoek, Robert Morris


今年跟上 RISC-V

  • 工具链和文档比以前成熟了
  • 让大家看看真正专业人士写出的代码
    • 每个细节都处理得非常好、值得我们学习

准备

使用到的工具 (apt 安装即可)

  • gcc-riscv64-linux-gnu
  • gdb-multiarch
  • qemu-system (你们已经有了)

项目组织

  • Makefile - 比 AbstractMachine 友好多了
    • 注意到目标:qemu, qemu-gdb
    • 老规矩:make -nB qemu | grep -v objdump | vim -
  • make qemu
    • 怎么退出???
  • .gdbinit

xv6 spinlock 实现讲解

spinlock-xv6.c

  • per-cpu 数据
    • noff (number of offs) - 关中断计数
    • intena (interrupt enabled) - 锁释放时是否需要开中断
  • per-lock 数据
    • locked - 锁变量
    • name, cpu - 调试信息

xv6 spinlock 中的编程哲学

随处可见的 specification 检查 (防御性编程)

  • acquire
    • 禁止重入 (同一 CPU 两次获得同一把锁)
      • panic_on(holding(lk), "acquire");
  • release
    • 必须由持有锁的 CPU 释放
      • panic_on(!holding(lk), "release");
  • popoff
    • 持有锁期间中断处于关闭状态
      • panic_on(intr_get(), "pop_off - interruptible");
    • 配对检查
      • panic_on(mycpu()->noff < 1, "pop_off");

调试 xv6

make qemu-gdb

  • 提供了丰富的基础设施 (.gdbinit 等)

调试的一些小技巧

  • (重要) 没有技巧:RTFM
  • set scheduler-locking on (我们额外添加到 .gdbinit)
    • prevents other threads from preempting the current thread while you are stepping
  • 调试多线程
    • info threads
    • thread X

总结

总结

本次课内容与目标

  • 复习操作系统内核中的并发编程
  • 实现操作系统内核自旋锁 (xv6 spinlock; 调试多处理器内核)

Take-away messages

  • 并发很复杂
    • 多多人肉模型检验 (画状态机)
  • 操作系统内核反而很简单
    • 无非就是一段可中断的多线程代码……