译至:http://geeki.wordpress.com/2010/10/30/ways-of-sleeping-in-linux-kernel/

在Linux中睡眠有2-3种不同的方法。

睡眠的第一种简单的方法是将当前进程的状态设置为INTERRUPTIBLE或NON_INTERRUPTIBLE然后调用schedule。将进程设置为RUNNING之外状态很重要,因为只有这样,内核会将进程移出运行队列。进程被调度出去后,它需要以某种方式被调度回来 - 用wake_up()来实现。 这需要进程的task_struct中作为参数。 下面是一段来自Linux Journal的示例代码段:

//Process A:
spin_lock(&list_lock);
if(list_empty(&list_head)) {
spin_unlock(&list_lock);
set_current_state(TASK_INTERRUPTIBLE);
schedule();
spin_lock(&list_lock);
}

/* Rest of the code ... */
spin_unlock(&list_lock);

//Process B:
spin_lock(&list_lock);
list_add_tail(&list_head, new_node);
spin_unlock(&list_lock);
wake_up_process(processA_task);



上面的一段代码会导致一个“无效唤醒”的竞争条件问题。 说明如下:该进程对一些条件检查,然后设置任务状态为可中断并进入睡眠。 这时可能有一个小的竞争条件,当满足该条件的进程在条件检查之后唤醒睡眠进程时(唤醒任务仅仅是将任务状态设置成RUNNING,并将它保持在运行队列) - 这会导致一种情况:睡眠进程在唤醒进程将其唤醒后进入睡眠 - 这就是无效唤醒的问题。 这样做的后果可能会很严重也可能并不严重。 如果睡眠进程将条件标记为假,然后继续睡眠,那么这个无效唤醒的问题将导致睡眠进程永远保持该状态。 但是,如果条件是通过外部满足的话,那么最终条件将变为真,唤醒进程将唤醒睡眠进程。 这种情况的解决方法是在检查前设置任务状态 - 因此,如果这个进程进入睡眠状态,那么它将保持在运行队列而不是等待队列。 (译者注:wake_up_process 在set_current_state(TASK_INTERRUPTIBLE)之后执行,会将任务状态设置成RUNNING,并将它保持在运行队列,所以在 schedule()之前它保持在运行队列)。如果任务状态是running的话,schedule() API将保持进程只在运行队列。

还有一个问题,这种形式的睡眠要求waker进程中必须知道睡眠进程的task_struct。当存在一个以上的睡眠进程时这可能令人厌倦。 因此,在内核中睡眠的另一种形式是使用等待队列。 下面是一个示例一段代码,声明了一个等待队列,并把自己保持在等待队列。



DECLARE_WAIT_QUEUE_HEAD(my_event);
wait_event_interruptible(my_event, (cond == x));

void my_wake_up(void)
{
if (cond == x)
return;
set_bit(2, &my_flags);
wake_up_interruptible(&my_event);
}



正如在上面这段代码中可以看出,当我们在一个队列中等待时,我们也传递了一个条件用于检查 - 这样内核在将进程保持在等待队列之前要确保这个条件不为真。内核将改变进程的状态,然后检查条件,并把该进程中加入等待队列。 当时间到时,waker进程将来唤醒等待队列上进程 - 这样,它不需要知道哪些进程正在睡眠,它只需知道等待队列。 在一定程度上比之前的不可扩展的睡眠形式更好。 还有一个API,wake_up_all(),将唤醒所有的进程 - 这个API如果不能正确使用会导致惊群问题。 不过,也有这个API的相关使用场景- 例如,多个读者和一个写者的问题可以使用这个API - 当写者已经获得了锁,所有的读者都被放在等待队列 - 当写者完成后,它可以用wake_up_all()唤醒在等待队列中的所有进程。 所有的读者会成功获得锁。


睡觉的另一种方法是睡眠一定的时间段。 进程可以以jiffies或毫秒/秒形式睡眠。 下面是以毫秒形式睡眠的示例代码。



read_done = 0;
while (read_done == 0) {
msleep(2); //sleep for a couple of milliseconds.
}

// Another thread
read_done = 1;



这个进程不知道它要花多少时间,但是它确保不会需要很长时间 - 因此它选择了避免创建另一个等待队列,而是简单地使用msleep()API来睡2毫秒。 迟早,条件变成真然后进程会继续处理。这带来上下文切换等方面的一些小的CPU开销,但它是一个简单的设计。 顺便说一句,如果我们想用jiffies睡眠的话,可以使用schedule_timeout()API。 它的内部处理是,把进程加入了某个等待队列,然后到了时间后唤醒它。 msleep()也是做同样的事情。