〇、前言
本文将会结合源代码谈论 sleep、wakeup 这两个系统调用。
一、sleep()系统调用
以下是sleep()
函数源码:
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc();
// Must acquire p->lock in order to
// change p->state and then call sched.
// Once we hold p->lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup locks p->lock),
// so it's okay to release lk.
acquire(&p->lock); //DOC: sleeplock1
release(lk);
// Go to sleep.
p->chan = chan;
p->state = SLEEPING;
sched();
// Tidy up.
p->chan = 0;
// Reacquire original lock.
release(&p->lock);
acquire(lk);
}
先来看看 lost wakeup 问题。当一个进程在 sleep()
时,如果 sleep()
了一半,状态还没来得及修改为SLEEPING,这时候发生了中断,并且被某些进程调用了 wakeup()
,那么这个 wakeup()
肯定不能把这个进程唤醒。而且,在被中断恢复后,它将永远等不到唤醒,因为唤醒已经错过。所以,在这里必须要正不可中断性和操作先后性。因为在下面就会看到 wakeup()
只唤醒状态为 SLEEPING 的进程。
因此我们必须保证,sleep()
是一个原子操作,在 sleep()
执行过程中,要么执行完全,要么没有被执行。所以这里必须加一个进程锁。所以在下面就会看到 wakeup()
中也会尝试获取休眠的进程锁。
在持有进程锁的时候,将进程的状态设置为 SLEEPING 并记录sleep channel,之后再调用 sched()
函数,这个函数中会再调用 swtch()
函数(而这会返回到 scheduler()
函数中),此时 sleep()
函数中仍然持有了进程的锁,wakeup()
仍然不能做任何事情。
因此在 sleep()之后,这个锁必须释放。我们来看看细节:
void
scheduler(void)
{
...
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0; // 返回的位置,此刻继续执行
}
release(&p->lock);
...
}
在这里,它会继续执行上一次执行到的位置,即 c->proc = 0
,然后执行 release(&p->lock)
,也就是释放锁,而且释放的是 sleep()
中的当前进程的锁。(这一点不是很好理解,可以理解为用的上一个进程的代码释放当前进程的锁?总之,这些代码就冰冷冷的放在内存里,被 pc 不断地指一遍又一遍)。更有意思的是,在 sched() 函数返回之后,继续运行:
void
sleep(void *chan, struct spinlock *lk)
{
...
sched();
// Tidy up.
p->chan = 0; // 就绪执行的位置
// Reacquire original lock.
release(&p->lock);
acquire(lk);
}
这里 release(&p->lock)
实际上释放的是 scheduler()
中选中的进程的锁。
所以在调度器线程释放进程锁之后,wakeup()
才能终于获取进程的锁,发现它正在 SLEEPING状态,并唤醒它。
这里的效果是由之前定义的一些规则确保的,这些规则包括了:
- 调用 sleep 时需要持有condition lock,这样 sleep 函数才能知道相应的锁;
- sleep函数只有在获取到进程的锁
p->lock
之后,才能释放 condition lock; - wakeup需要同时持有两个锁才能查看进程。
二、wakeup()调用
以下是 wakeup()
的源码:
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
if(p != myproc()){
acquire(&p->lock);
if(p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
release(&p->lock);
}
}
}
可以看到它的工作很简单,检查两个条件之后,就修改进程的状态为 RUNNABLE。
三、总结
这篇文章详细地介绍了 xv6 操作系统中的 sleep()
和 wakeup()
系统调用的实现原理以及相关的内部工作机制。主要强调了在 sleep()
中的原子操作性,确保了操作的完整性,以及在 wakeup()
中唤醒休眠进程的方式。
关于 sleep()
:
强调了 sleep()
操作的原子性,使用进程锁确保 sleep()
操作是一个原子操作,避免了 “lost wakeup” 问题的发生。
通过释放持有的锁,让出 CPU 控制权,进入 SLEEPING 状态,然后释放进程锁,使得其他进程能够继续运行。
调度器在合适的时机恢复了进程的执行,完成 sleep()
操作。
关于 wakeup()
:
wakeup()
通过遍历进程列表,并获取每个进程的锁,查看处于 SLEEPING 状态且 sleep channel 匹配的进程,将其状态设置为 RUNNABLE,唤醒进程。
整体上,这篇文章清晰地解释了 sleep()
和 wakeup()
这两个关键系统调用的工作原理和实现细节,突出了在并发环境下确保原子性操作和避免死锁的重要性。
全文完,感谢阅读。