在详细讲解运行进程的组织及调度的一些列课题前,先把Linux内核中如何组织非运行状态进程的那些机制梳理一遍。
运行队列链表rq把处于TASK_RUNNING状态的所有进程组织在一起。当要把其他状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之一:
(1) 没有为处于TASK_STOPPED、EXIT_ZOMBIE 或 EXIT_DEAD状态的进程建立专门的链表。由于处于暂停、僵死、死亡状态进程的访问比较简单,或者通过PID,或者通过特定父进程的子进程链表,所以不必对这三种状态进程分组。
(2) 对于处于TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态的进程将会被划分进若干的类别中,每个类别代表了一个指定的事件。在这里,进程状态并没有提供足够的信息来迅速地访问到进程,所以有必要引入一个额外的进程链表。它们叫做等待队列,我们下面将会着重讨论。
1 等待队列
等待队列在内核中有很多用途,尤其用在中断处理、进程同步及定时。因为这些主题将在以后博文中讨论,所以我们只在这里说明,进程必须经常等待某些事件发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间经过固定的间隔。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。
等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列都有一个等待队列头,等待队列头 是一个类型为wait_queue_head_t的数据结构:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护以免对其进行同时访问,因为同时访问会导致不可预测的后果。同步是通过等待队列头中的lock自旋锁达到的。task_list字段是等待进程链表的头。
等待进程链表的元素 类型为wait_queue_t:
struct __wait_queue {
unsigned int flags;
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
等待队列链表中的每个元素代表一个睡眠进程 ,该进程等待某已事件的发生;它的描述符地址存放在task字段中。task_list字段中包含的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中 。
然而,要唤醒等待队列中所有睡眠的进程有时并不方便。例如,如果两个或多个进程正在等待互斥访问某一要释放的资源,仅唤醒等待队列中的一个进程才有意义。这个进程占有资源,而其他进程继续睡眠。
因此,有两种睡眠进程:互斥进程(队列元素的flags字段为1 )由内核有选择地唤醒,而非互斥进程(flags值为0 )总是由内核在事件发生时全部唤醒。等待访问临界资源的进程就是互斥进程的典型例子。等待相关事件的进程是非互斥的。例如,我们考虑等待磁盘传输结束的一组进程:一旦磁盘传输完成,所有等待的进程都会被唤醒。下面,我们将详细讲解,等待队列元素func字段用来表示等待队列中睡眠进程应该用什么方式唤醒。
2 等待队列的操作
我们用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新等待队列的头,它静态地申明一个叫name的等待队列的头变量,并对该变量的lock和task_list字段进行初始化。函数init_waitqueue_head()可以用来初始化动态分配的等待队列的头变量。
函数init_waitqueue_entry(q,p)如下所示初始化wait_queue_t结构的变量q:
q->flags = 0;
q->task = p;
q->func = default_wake_function;
非互斥进程p将由default_wake_function()函数唤醒,default_wake_function()函数是在我们在后面博文要讨论的try_to_wake_up( )函数的一个简单的封装,该函数将此进程插入适当的运行队列中。
也可以选择DEFINE_WAIT宏申明一个wait_queue_t类型的新变量,并用CPU上运行的当前进程的描述符和唤醒函数autoremove_wake_function( )的地址初始化这个新变量的func字段。这个函数调用default_wake_function()函数来唤醒进程,然后从等待队列的链表中删除对应的元素(每个等待队列链表中的一个元素其实就是指向睡眠进程描述符的指针)。最后,内核开发者可以通过init_waitqueue_func_entry( )函数来自定义唤醒函数,该函数负责初始化等待队列的元素。
static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func)
{
q->flags = 0;
q->private = NULL;
q->func = func;
}
一旦定义了一个元素,必须把它插入等待队列。add_wait_queue( )函数把一个非互斥进程 插入等待队列链表的第一个位置。
add_wait_queue_exclusive( )函数把一个互斥进程 插入等待队列链表的最后一个位置(跟上个函数的区别仅仅是flags字段)。
remove_wait_queue( )函数从等待队列链表中删除一个进程。
waitqueue_active( )函数检查一个给定的等待队列是否为空。
好了,上面把数据结构和底层函数都理清了,下面我们讲讲如何使用它们。要等待特定条件的进程可以调用如下的任何一个函数。:
(1)sleep_on():操作当前进程:
void sleep_on(wait_queue_head_t *wq)
{
wait_queue_t wait;