Coordination

1 进程切换过程

  1. 一个进程出于某种原因想要进入休眠状态,比如说出让 CPU 或者等待数据,它会先获取自己的锁;
  2. 之后进程将自己的状态从 RUNNING 设置为 RUNNABLE;
  3. 之后进程调用 switch 函数,其实是调用 sched 函数在 sched 函数中再调用的 switch 函数;
  4. switch 函数将当前的线程切换到调度器线程;
  5. 调度器线程之前也调用了 switch 函数,现在恢复执行会从自己的 switch 函数返回;
  6. 返回之后,调度器线程会释放刚刚出让了 CPU 的进程的锁
// 需要切换的进程
acquire(&p->lock);
p->state = RUNNABLE;
swtch();
// 调度器进程
swtch();
release(&p->lock);
  • 在进程切换的最开始,进程先获取自己的锁,并且直到调用 swtch 函数时也不释放锁。而另一个线程,也就是调度器线程会在进程的线程完全停止使用自己的栈之后,再释放进程的锁。

    • 如果在 swtch()前释放锁,可能有另一个 CPU 核心运行同样的进程;会造成程序崩溃
  • XV6 中,不允许进程在执行 switch 函数的过程中,持有任何其他的锁。

    • 如果 p1 持有另一个锁 l1,当 p1 切换到 p2 时;p1 的 l1 未释放,而如果 p2 需要 l1;会造成死锁:p2 需要 l1 来进行进程切换,p1 拥有 l1,但不能进行进程切换
    • 我们不能在等待锁的时候处理中断;所以定时器中断 (调用 yield,让进程出让 CPU) 不能打破死锁

2 Sleep & Wakeup

  • 通过循环等待硬件设备在现在等待的时间是不可接受的;
  • xv6 中通过 uart 硬件进行读取字符到控制台;当写入一个字符后写入进程 sleep,触发中断 uartintr;中断判断是否已经写入完成,如果写入完成,唤醒写入进程进行下一次写入
  • sleep 和 wakeup 需要特定的参数一个 channel;sleep 需要传递一个锁

2.1 lose wakeup

  • 使用下面的 sleep 和 wakeup;会造成丢失唤醒问题
  • 可能在进程未 sleep 前,中断例程执行成功,wakeup 未唤醒任何进程
void broken_sleep(chan) {
p->state = SLEEPING;
p->chan = chan;
swtch();
}
void wakeup(chan) {
for(each p in prics[]) {
if(p->state == SLEEPING&&p->chan == chan) {
p->state = RUNNABLE;
}
}
}

void uartwrite(buf) {
for each c in buf:
lock;
while not done;
unlock;
// 中断发生地
sleep(&tx_chan);
// 再次获得锁进行下一步操作
lock
send c;
done = 0;
unlock;
}

void uartintr() {
lock;
done = 1;
wakeup(&tx_chan);
unlock;
}
  • sleep 需要传入一个保护条件的锁;调用 sleep 时,锁被当前线程持有,之后这个锁被传递给 sleep
  • wakeup 必须在持有条件锁(进程的锁)时才能唤醒进程

2.2 exit

  • 关闭所有打开的文件;父进程退出,子进程由 init 进程管理;将自己变为僵尸进程;进入 sched()函数
  • 一个进程 exit 后,父进程如果调用了 wait,父进程会返回子进程退出值
    • 扫描进程表,找到子进程为僵尸进程的进程;调用 freeproc()
static void
freeproc(struct proc *p) {
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
  • 在 Unix 中,对于每一个退出的进程,都需要有一个对应的 wait 系统调用

2.3 kill

  • Unix 中的一个进程可以将另一个进程的 ID 传递给 kill 系统调用,并让另一个进程停止运行
  • 它先扫描进程表单,找到目标进程。然后只是将进程的 proc 结构体中 killed 标志位设置为 1。如果进程正在 SLEEPING 状态,将其设置为 RUNNABLE。这里只是将 killed 标志位设置为 1,并没有停止进程的运行。所以 kill 系统调用本身还是很温和的。
  • 而目标进程运行到内核代码中能安全停止运行的位置时,会检查自己的 killed 标志位,如果设置为 1,目标进程会自愿的执行 exit 系统调用
  • 如果进程在用户空间,那么下一次它执行系统调用它就会退出,又或者目标进程正在执行用户代码,当时下一次定时器中断或者其他中断触发了,进程才会退出。所以从一个进程调用 kill,到另一个进程真正退出,中间可能有很明显的延时
    • 在内核态 kill 进程,会将进程唤醒;在 sleep 循环中进行 killed 标志位的检查
    • 在驱动中,我们期望文件操作不会中断;所以不会在 sleep 循环中进行 killed 标志位的检查
  • init 进程的目标就是不退出,它就是在一个循环中不停的调用 wait。如果 init 进程退出了,我认为这是一个 Fatal 级别的错误,然后系统会崩溃。在 exit 函数的最开始就会有如下检查
void
exit(int status) {
struct proc *p = myproc();
if(p == initproc)
panic("init exiting");
}