MIT6.1810: xv6 book Chapter 9: Sleep and Wakeup 笔记

Overview

void sleep(void *chan, struct spinlock *lk)

void wakeup(void *chan)

chan 称作 等待通道wait channel ,wakeup会唤醒所有通过相同chan值调用sleep 的进程,因此chan值一般选用一些方便的对象的地址

lk 是用于保护临界区域的条件锁condition lock ,调用sleep和wakeup的线程一定会有共享的临界区域 e.g. 若在一个线程检查条件和调用sleep之间,另一个线程调用wakeup,则不会唤醒任何线程,第一个线程会永远沉睡。这称为丢失唤醒lost wakeup

Code: Sleep and wakeup

sleep&wakeup对于condition lock&p->lock的规则保证了进程的唤醒

上图展示了sleep过程的代码,从检查条件(缓存内没有数据供读取)前 ,到更新p->state = SLEEPING(scheduler)后 ,进程至少持有condition lock和p->lock中的其中一把锁

而wakeup要唤醒进程,要求必须同时持有condition lock和p->lock两把锁

所以wakeup唤醒sleep的进程就有两种情况:

1.将要sleep的进程检查条件前,调用wakeup的进程修改了条件,将要sleep的进程检查条件为真,不进入睡眠

2.sleep的进程正常睡眠,wakeup搜索到p->state == SLEEPING的进程后正常将其唤醒

有时等待通道中有多个睡眠的进程,wakeup会将所有进程唤醒,但只有一个进程首先运行,并读取所有数据(这里以pipe为例)

sleep总是在一个while循环中被调用,while判断条件为假,再次调用sleep,即其他进程被唤醒后发现条件仍然为假,继续睡眠

Code: Wait, exit and kill

kexit允许进程终止自己

kill并不会直接杀死当前进程,因为当前进程可能在其他CPU上执行重要操作

取而代之的是,kkill函数只设置进程的p->killed,并且如果进程正在睡眠则将其唤醒

最终进程在usertrap里判断p->killed从而调用kexit,终止进程

然而直接唤醒进程可能是危险的,因为唤醒的进程不一定满足唤醒条件

为此xv6部分包含sleep的while循环的判断条件会包含对进程是否被杀死的判断,例如pipewrite中若发现killed(pr)为真,会返回-1,最终触发trap,调用kexit

c 复制代码
// File: pipe.c
if(pi->readopen == 0 || killed(pr)){
  release(&pi->lock);
  return -1;
}

Process Locking

p->lock在读写下列数据时必须被持有:p->state, p->chan, p->killed, p->xstate, p->pid

因为这些数据会被其他进程,调度器线程,其他CPU使用

p->lock还有许多其他用途:

1.为新进程分配proc\[\] slots时避免竞争(with p->state)

2.进程创建和销毁时隐藏进程

3.防止父进程发现已经设置状态为ZOMBIE但还未放弃CPU的子进程

4.防止其他CPU的调度器线程发现已经设置状态为RUNNABLE但还未结束swtch的进程

5.保证只有一个CPU调度器决定要运行RUNNABLE状态的进程

6.防止时钟中断yield在进程调用swtch时放弃CPU

7.避免唤醒已经调用sleep但还未放弃CPU的进程(with condition lock)

8.防止kill的进程退出后在kkill检查p->pid和设置p->killed之间被重新创建

9.使kkill对p->killed的检查和修改原子化

Real world

同步方法还有很多其他种类,如信号量semaphores

为解决丢失唤醒,原版Unix内核(单核CPU)的sleep,FreeBSD的msleep关闭了中断;Plan 9的sleep在持有调度锁的情况下,在进程进入睡眠之前最后一刻调用callback function检查,来避免丢失唤醒;Linux维护了一个进程队列,称作等待队列wait queue ,该队列有自己的内置锁

wakeup扫描所有线程也很耗费时间,为解决该问题,更好的方案是将chan替换成Linux的等待队列那样的列出进程的数据结构,条件变量condition variable 就是一个例子,它通过wait&signal实现sleep&wakeup

条件变量用signal和broadcast区分唤醒一个进程和唤醒全部进程,避免xv6的wakeup唤醒全部进程导致的CPU浪费

强制杀死进程可能带来部分问题。比如某些"陷入内核"的进程,要杀死它得一步步展开堆栈小心删除,这对没有异常处理的C语言来说并不容易;其次,被唤醒的进程不一定满足等待条件,如Unix的signal可以唤醒进程,并让read系统调用返回-1,设置错误码为EINTR(Interrupted system call)