4.5.4 睡眠和唤醒
休眠(被阻塞)的进程处于一个特殊的不可运行状态。这点非常重要,如果没有这种特殊状态的话,调度程序就可能选出一个本不愿意被执行的进程,槽糕的是,休眠必须以轮询的方式实现了。进程休眠有多种原因,都是为了等待一些事件。事件可能是一段时间从文件IO读更多的数据,或者是某个硬件。一个进程还有可能在尝试获取一个已被占用的内核信号量时被迫进入休眠。休眠的一个常见原因是文件IO——如进程对一个文件执行了read()操作,而这需要从磁盘里读取。还有,进程在获取键盘输入时也需要等待。无论哪种情况,内核的操作都相同:进程把自己标记成休眠状态,从可执行红黑树中移除,放入等待队列,调用schedule()选择和执行一个其他进程。唤醒的过程:进程被设置为可执行状态,从等待队列中移到可执行红黑树中。
休眠有两种相关的进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE。唯一的区别是处于TASK_UNINTERRUPTIBLE的进程会忽略信号,而处于TASK_INTERRUPTIBLE状态的进程如果接收到一个信号,会被提前唤醒并响应该信号。两种状态的进程位于同一个等待队列上,等待某些事件,不能够运行。
1、等待队列
休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的链表。内核用wait_queue_head_t来代表等待队列,定义在linux/wait.h文件中。
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;//包含链表
};
typedef struct __wait_queue_head wait_queue_head_t;
等待队列可通过DECLARE_WAITQUEUE宏静态创建,
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } }
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
也可由init_waitqueue_head()动态创建。
extern void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *);
#define init_waitqueue_head(q) \
do { \
static struct lock_class_key __key; \
\
__init_waitqueue_head((q), &__key); \
} while (0)
进程把自己放入等待队列中并设置成不可执行状态。当与等待队列相关的事件发生时,队列上的进程被唤醒。为了避免产生竞争条件,休眠和唤醒的实现不能有纰漏。
针对休眠,曾经使用过简单的接口。但那些接口会带来竞争条件:有可能导致在判断条件变为真后,进程却开始了休眠,那样会使进程无限期地休眠下去。在内核中的进行休眠的操作相对复杂了一些:
进程通过执行下面几个步骤将自己加入到一个等待队列中:
1)调用宏DEFINE_WAIT()创建一个等待队列的项。
2)调用add_wait_queue()把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它。必须在其他地方写相关代码,在事件发生时,对等待队列执行wake_up()操作。
3)调用prepare_to_wait()方法将进程的状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。而且该函数如果有必要的话会将进程加回到等待队列。
4)如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程。这就是所谓的伪唤醒(唤醒不是因为事件的发生),因此检查并处理信号。
5)当进程被唤醒时,它会再次检查条件是否为真。如果是,退出循环;否则,再次调用schedule()并一直重复这步操作。
6)当条件满足后,进程将自己设置为TASK_RUNNING并调用finish_wait()方法把自己移出等待队列。
如果在进程开始休眠之前条件已经达成,那么退出循环,进程不会存在错误地进入休眠的倾向。注意:内核代码在循环体内需要完成一些其他的任务,比如,它可能在调用schedule()之前需要释放掉锁,而在这以后再重新获取它们,或者响应其他事件。
函数inotify_read(),位于fs/notify/inotify/inotify_user.c文件中,负责从通知文件描述符中读取信息,它的实现是等待队列的一个用法:
static ssize_t inotify_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
struct fsnotify_group *group;
struct fsnotify_event *kevent;
char __user *start;
int ret;
DEFINE_WAIT(wait);
start = buf;
group = file->private_data;
while (1) {
prepare_to_wait(&group->notification_waitq, &wait, TASK_INTERRUPTIBLE);
mutex_lock(&group->notification_mutex);
kevent = get_one_event(group, count);
mutex_unlock(&group->notification_mutex);
if (kevent) {
ret = PTR_ERR(kevent);
if (IS_ERR(kevent))
break;
ret = copy_event_to_user(group, kevent, buf);
fsnotify_put_event(kevent);
if (ret < 0)
break;
buf += ret;
count -= ret;
continue;
}
ret = -EAGAIN;
if (file->f_flags & O_NONBLOCK)
break;
ret = -EINTR;
if (signal_pending(current))
break;
if (start != buf)
break;
schedule();
}
finish_wait(&group->notification_waitq, &wait);
if (start != buf && ret != -EFAULT)
ret = buf - start;
return ret;
}
这个函数遵循上面的使用模式,主要的区别是它在while循环中检查了状态,原因是该条件的检测更复杂些,而且需要获得锁。循环退出是通过break完成的。
2、唤醒
唤醒操作是通过函数wake_up()进行,唤醒指定的等待队列上的所有进程。这个函数调用try_to_wake_up(),该函数负责将进程设置为TASK_RUNNING状态,调用enqueue_task()将此进程放入红黑树,如果被唤醒的进程优先级比当前正在执行的进程的优先级高,还要设置need_resched标志。通常哪段代码促使等待条件达成,它就要负责随后调用wake_up()函数。例如,当磁盘数据到来时,VFS就要负责对等待队列调用wake_up(),唤醒队列中等待这些数据的进程。
关于休眠需要注意:存在虚假的唤醒。有时进程被唤醒并不是因为它所等待的条件达成了才需要用一个循环处理来保证它等待的条件真正达成。