全面解析Linux内核的同步与互斥机制--同步篇
Sailor_forever 转载请注明
【摘要】本文分析了内核的同步及互斥的几种机制:原子运算符(atomic operator)、自旋锁Spinlock、等待队列Waitqueue、事件Event、completion、信号量Semaphore及其优化版互斥锁,详细分析了其实现流程。Event及Semaphore本质上都是基于Waitqueue和自旋锁实现的。本文还探讨了每种机制最适合应用到哪些地方,以及如何构建安全高效的内核及驱动代码。
【关键词】原子操作;Spinlock;Waitqueue;completion;Event;Semaphore
---------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------
1 休眠与同步
一个驱动当它无法立刻满足请求应当如何响应? 一个对 read 的调用可能当没有数据时到来, 而以后会期待更多的数据。或者一个进程可能试图写, 但是你的设备没有准备好接受数据, 因为你的输出缓冲满了。调用进程往往不关心这种问题; 程序员只希望调用 read 或 write 并且使调用返回, 在必要的工作已完成后. 这样, 在这样的情形中。驱动应当(缺省地)阻塞进程, 使它进入睡眠直到请求可继续。
进程被置为休眠,意味着它被标识为处于一个特殊的状态并且从调度器的运行队列中移走。直到发生某些事情改变了那个状态,否则这个进程将不被任何 CPU调度运行。
安全地进入休眠的两条规则:
1) 永远不要在原子上下文中进入休眠。
当驱动在持有一个自旋锁、seqlock或者 RCU 锁时不能睡眠;
关闭中断也不能睡眠;
持有一个信号量时休眠是合法的,但你应当仔细查看代码:如果代码在持有一个信号量时睡眠,任何其他的等待这个信号量的线程也会休眠。因此发生在持有信号量时的休眠必须短暂,而且决不能阻塞那个将最终唤醒你的进程。
2) 当进程被唤醒,重新检查其所需资源。
它并不知道休眠了多长时间以及休眠时发生什么;也不知道是否另有进程也在休眠等待同一事件,且那个进程可能在它之前醒来并获取了所等待的资源。所以不能对唤醒后的系统状态做任何的假设,并必须重新检查等待条件来确保正确的响应。
除非确信其他进程会在其他地方唤醒休眠的进程,否则也不能睡眠。使进程可被找到意味着:需要维护一个称为等待队列的数据结构。它是一个进程链表,其中饱含了等待某个特定事件的所有进程。在 Linux 中, 一个等待队列由一个wait_queue_head_t 结构体来管理。
2 休眠的基础
2.1 wait_queue系列数据结构
2.1.1 wait_queue_head_t
\include\linux\wait.h
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
它包含一个自旋锁和一个链表。这个链表是一个等待队列入口。
关于自定义结构体的风格,若需要提供别名,则原始类型前面加”__”或者“tag_”,表示其为内部数据类型,对外是不可见的。typedef之后的类型为了和原始类型分开一般会在后面添加“_t”,表示是typedef的,对外使用。
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = { &(name).task_list, &(name).task_list } \
}
因为Linux内核对于链表的遍历方式的问题,通常一个双向循环链表中有一个头节点,其与其他节点的结构不一样,并且通常无有效信息。此处的等待队列头有两个域:
1) 操作循环链表的互斥锁;
2) 嵌入到等待队列头中的链表头。
为了用“.”域的形式初始化成员不能采用单独的初始化锁和链表头部的宏,但可以采用声明一个结构体类型的宏,如__SPIN_LOCK_UNLOCKED(name.lock);.task_list的初始化应该采用LIST_HEAD_INIT宏的,这样提高了可移植性。定义等待队列头部的同时,初始化了其成员,尤其是链表头的初始化是添加后续等待队列的前提。
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
定义一个等待队列头同时分配内存并进行初始化。对外的接口。
extern void init_waitqueue_head(wait_queue_head_t *q);
void init_waitqueue_head(wait_queue_head_t *q)
{
spin_lock_init(&q->lock);
INIT_LIST_HEAD(&q->task_list);
}
动态初始化一个已经分配了内存的wait_queue_head_t结构。当wait_queue_head_t类型成员内嵌到其他结构体中时需要采用此方法,而不能采用DECLARE_WAIT_QUEUE_HEAD全局或在栈中定义初始化一个wait_queue_head_t结构。
2.1.2 wait_queue_t
struct __wait_queue {
unsigned int flags; //在等待队列上唤醒时是否具备排他性
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func; //从等待队列中唤醒进程时执行的统一操作,将进程更改为可运行状态,具体谁运行将由调度策略决定
struct list_head task_list; //与该等待队列对应的链表
};
/*Macros for declaration and initialisaton of the datatypes*/
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } \
}
GNU语法中对于结构体成员赋初值采用了域的形式,如“.private =”,其好处在于:
a) 可以选择性的对部分成员赋值。当结构体成员变量较多而大部分无须初始值时,此方法显得尤为重要,因此减少了不必要的赋值。
b) 赋值顺序与数据结构定义中成员的顺序无关,因此若结构体成员顺序变化,初始化部分不会受到任何影响。
c) 各个域之间用“,”分隔,最后一个无“,”。
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
全局或者在栈中定义一个wait_queue_t类型变量的同时对其初始化,这保证了系统的可靠性,避免因用户忘记初始化时导致的问题。通用的初始化宏,tsk为任意指针。分两步:
1) 内部宏__WAITQUEUE_INITIALIZER初始化相应成员;当wq内嵌在别的结构体中时,此宏很重要,提高了可移植性;
2) 提供给外部的接口,定义一个变量,并将第一步的结果赋值给该变量。
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
q->flags = 0;
q->private = p;
q->func = default_wake_function;
}
动态初始化一个等待队列入口项,将其和当前进程关联起来,以便唤醒当前进程。
2.1.3 数据结构设计规则
今后凡遇到新设计一类结构体,若此类结构体变量必须初始化且有相对集中的操作,则应提供以下两个操作接口:
a) 定义新建一个结构体变量,并初始化之;
b) 动态初始化一个已经分配内存的该类变量
为了适应在堆栈及全局等任意地方分配的该变量,其应该接收指向该类变量的指针。
2.2 陈旧sleep_on系列
//初始化一个wait_queue_t
#define SLEEP_ON_VAR \
unsigned long flags; \
wait_queue_t wait; \
init_waitqueue_entry(&wait, current);
//添加到队列中
#define SLEEP_ON_HEAD \
spin_lock_irqsave(&q->lock,flags); \
__add_wait_queue(q, &wait); \
spin_unlock(&q->lock);
//从队列中删除
#define SLEEP_ON_TAIL \
spin_lock_irq(&q->lock); \
__remove_wait_queue(q, &wait); \
spin_unlock_irqrestore(&q->lock, flags);
SLEEP_ON_VAR、SLEEP_ON_HEAD及SLEEP_ON_ TAIL总是同时出现,不可分割。上述宏不是函数,只是连续的表达式而已,因为函数就将他们隔离开来了,函数他退出后变量就无意义了。也不能写成do――while的多语句宏,变量定义离开“{}”后就没有意义了。是为了编写代码更清晰明了,同时避免多写字,实际上就是代码封装复用。
void fastcall __sched interruptible_sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR //注意没“;”
current->state = TASK_INTERRUPTIBLE;
SLEEP_ON_HEAD
schedule(); //此处可能出问题
SLEEP_ON_TAIL
}
EXPORT_SYMBOL(interruptible_sleep_on);
添加到队列和从队列中删除由同一个模块做,符合模块设计规则,减小了耦合性。
唤醒的wakeup只负责改变进程状态,进程重新获得cpu后从队列中删除。
long fastcall __sched
interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout)
{
SLEEP_ON_VAR
current->state = TASK_INTERRUPTIBLE;
SLEEP_ON_HEAD
timeout = schedule_timeout(timeout);
SLEEP_ON_TAIL
return timeout;
}
EXPORT_SYMBOL(interruptible_sleep_on_timeout);
void fastcall __sched sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR
current->state = TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD
schedule();
SLEEP_ON_TAIL
}
EXPORT_SYMBOL(sleep_on);
long fastcall __sched sleep_on_timeout(wait_queue_head_t *q, long timeout)
{
SLEEP_ON_VAR
current->state = TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD
timeout = schedule_timeout(timeout);
SLEEP_ON_TAIL
return timeout;
}
EXPORT_SYMBOL(sleep_on_timeout);
在决定调用sleep_on系列函数到真正调用schedule系列函数期间,若等待的条件为真,若此时继续schedule,相当于丢失了一次唤醒机会。因此sleep_on系列函数会引入竞态,导致系统的不安全。
另外对于interruptible系列函数,其返回时并不能确定是因为资源可用返回还是遇到了signal,因此在程序中用户需要再次判断资源是否可用。如:
static ssize_t at91_mcp2510_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
CanData candata_ret;
retry:
if (mcp2510dev.nCanReadpos != mcp2510dev.nCanRevpos) {// 需求的资源
int count;
count = MCP2510_RevRead(&candata_ret);
if (count) copy_to_user(buffer, (char *)&candata_ret, count);
return count;
} else { //不可用
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
interruptible_sleep_on(&(mcp2510dev.wq));
if (signal_pending(current)) // 遇到信号,返回
return -ERESTARTSYS;
goto retry; //重新判断资源是否可用
}
DPRINTK("read data size=%d\n", sizeof(candata_ret));
return sizeof(candata_ret);
}
综合上述两个因素,sleep_on系列函数应避免在驱动程序中出现,未来的2.7版内核中将删除此类函数。
2.3 等待队列的接口
2.3.1 初始化等待队列
#define DEFINE_WAIT(name) \
wait_queue_t name = { \
.private = current, \
.func = autoremove_wake_function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
#define init_wait(wait) \
do { \
(wait)->private = current; \
(wait)->func = autoremove_wake_function; \
INIT_LIST_HEAD(&(wait)->task_list); \
} while (0)
专用的初始化等待队列函数,将当前进程添加到等待队列中,注意和通用的接口DECLARE_WAITQUEUE及init_waitqueue_entry区别。同时唤醒处理不一样,autoremove_wake_function在default_wake_function的基础之上,将当前进程从等待队列中删除。
2.3.2 添加或从等待队列中删除
添加删除的原始接口
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
list_add(&new->task_list, &head->task_list);
}
static inline void __add_wait_queue_tail(wait_queue_head_t *head,
wait_queue_t *new)
{
list_add_tail(&new->task_list, &head->task_list);
}
static inline void __remove_wait_queue(wait_queue_head_t *head,
wait_queue_t *old)
{
list_del(&old->task_list);
}
等待队列是公用资源,但上述接口没有加任何保护措施,适用于已经获得锁的情况下使用。
带锁并设置属性的添加删除
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue);
void fastcall add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
wait->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue_tail(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(add_wait_queue_exclusive);
void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__remove_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(remove_wait_queue);
此三对函数的特点是调用同名的内部函数,同时添加一些保障安全特性的代码。WQ_FLAG_EXCLUSIVE属性的进程添加到队尾,而非WQ_FLAG_EXCLUSIVE从队头添加。这样可以保证每次都能唤醒所有的非WQ_FLAG_EXCLUSIVE进程。
无锁但设置属性的添加删除
static inline void add_wait_queue_exclusive_locked(wait_queue_head_t *q,
wait_queue_t * wait)
{
wait->flags |= WQ_FLAG_EXCLUSIVE;
__add_wait_queue_tail(q, wait);
}
/* Must be called with the spinlock in the wait_queue_head_t held.*/
static inline void remove_wait_queue_locked(wait_queue_head_t *q,
wait_queue_t * wait)
{
__remove_wait_queue(q, wait);
}
Locked系列适用于在已经获得锁的情况下调用,通常用于信号量后者complete系列函数中。
上述三组函数,__add_wait_queue是内部函数,无任何保护,无任何属性设置;而另外两组分别适用于当前是否加锁的两种场合,是对外的接口函数。这种根据适用场合不同提供不同的接口函数的方法值得借鉴。
2.3.3 prepare_to_wait和finish_wait
/*
* Used to distinguish between sync and async io wait context:
* sync i/o typically specifies a NULL wait queue entry or a wait
* queue entry bound to a task (current task) to wake up.
* aio specifies a wait queue entry with an async notification
* callback routine, not associated with any task.
*/
#define is_sync_wait(wait) (!(wait) || ((wait)->private))
同步io等待将唤醒当前进程,异步io等待和当前进程无关,时间到后执行安装的回调函数
void fastcall
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE; //非排它性的等待将都被唤醒
spin_lock_irqsave(&q->lock, flags);
//等待节点尚未添加到任何等待队列中,防止重复加入
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait);
if (is_sync_wait(wait))
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
wait->flags |= WQ_FLAG_EXCLUSIVE;
排他性等待,其余和prepare_to_wait一样
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING); //确保进程状态为running
//若有等待进程,则将其从等待队列中删除
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
3 等待事件event
Linux 内核中最简单的休眠方式是称为 wait_event的宏(及其变种),它实现了休眠和进程等待的条件的检查。形式如下:
wait_event(queue, condition)/*不可中断休眠,不推荐*/ wait_event_interruptible(queue, condition)/*推荐,返回非零值意味着休眠被中断,且驱动应返回 -ERESTARTSYS*/ wait_event_timeout(queue, condition, timeout) wait_event_interruptible_timeout(queue, condition, timeout) /*有限的时间的休眠;若超时,则不管条件为何值返回0,*/
上述四个宏函数为内核对外的接口,其他的休眠函数应避免使用。因为宏并不是函数,参数所做的任何修改对调用环境仍然有效,所以queue都是“值传递”,在宏内部会调用底层函数,采用的指针传递。Linux内核中存在大部分这样的宏,其都在接口上都是值传递。
3.1 wait_event
认真地看简单休眠中的 wait_event(queue, condition) 和 wait_event_interruptible(queue, condition) 底层源码会发现,其实他们只是手工休眠中的函数的组合。因此在驱动程序中应避免使用手动休眠代码,而应该采用内核已经封装好的四个wait_event系列函数。
\include\linux\wait.h
#define __wait_event(wq,condition) \
do { \
DEFINE_WAIT(__wait); \
\
for(;;) { \
prepare_to_wait(&wq,&__wait,TASK_UNINTERRUPTIBLE); \
/// 添加到等待队列中,同时更改进程状态;若已经加入则不会重复添加
if (condition) \
break; \
schedule(); //何时返回呢??? \
} \
finish_wait(&wq,&__wait); \
} while (0)
// “__”表示内部函数,默认为condition不满足,添加至等待队列,调度
注意prepare_to_wait和finish_wait的匹配关系
#define wait_event(wq,condition) \
do { \
if(condition) \
break; \
__wait_event(wq,condition); \
}while (0)
//对外的接口函数,需要判断condition,若假则等待;若真则直接退出
--------------------------------------------------------------------------------------------------------------
等待系列函数架构设计:
3.2 wait_event_timeout
#define __wait_event_timeout(wq, condition, ret) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) \
break; \
ret = schedule_timeout(ret); \
if (!ret) \
break; //延时到,退出 \
} \
finish_wait(&wq, &__wait); \
} while (0)
#define wait_event_timeout(wq,condition,timeout) \
({ \
long __ret=timeout; \
if( !(condition) ) \
__wait_event_timeout( wq,condition,__ret); \
__ret; \
})
3.3 wait_event_interruptible
#define __wait_event_interruptible(wq, condition, ret) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if (!signal_pending(current)) { \
schedule(); \
continue; \
} \
ret = -ERESTARTSYS; \
break; \
} \
finish_wait(&wq, &__wait); \
} while (0)
/**
* wait_event_interruptible - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_INTERRUPTIBLE) until the
* @condition evaluates to true or a signal is received.
* The @condition is checked each time the waitqueue @wq is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*
* The function will return -ERESTARTSYS if it was interrupted by a
* signal and 0 if @condition evaluated to true.
*/
#define wait_event_interruptible(wq, condition) \
({ \
int __ret = 0; \
if (!(condition)) \
__wait_event_interruptible(wq, condition, __ret); \
__ret; \
})
3.4 wait_event_interruptible_timeout
#define __wait_event_interruptible_timeout(wq,condition,ret) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq,&__wait,TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if(!signal_pending(current)) { \
// 当前进程无信号需要处理
ret = schedule_timeout(ret); \
if(!ret) \
break; //时间片用完唤醒 \
continue; \ .
} \
ret = _ERESTARTSYS; //被信号唤醒 \
break; \
} \
finish_wait(&wq,&__wait); \
} while (0)
#define wait_event_interruptible_timeout(wq,condition,timeout) \
( { \
long__ret = timeout; \
if(!(condition)) \
__wait_event_interruptible_timeout(wq,condition,__ret); \
__ret; \
})
wait_event_interruptible_timeout()类架构:
4 唤醒系列wake_up
4.1 wake_up 的API
惯例:用 wake_up 唤醒 wait_event;用 wake_up_interruptible 唤醒wait_event_interruptible。很少会需要调用wake_up_interruptible 之外的唤醒函数,但为完整起见,这里是整个集合:
wake_up(wait_queue_head_t *queue); wake_up_interruptible(wait_queue_head_t *queue); /*wake_up 唤醒队列中的每个非独占等待进程和一个独占等待进程。wake_up_interruptible 同样, 但它跳过处于不可中断休眠的进程。它们在返回之前, 使一个或多个进程被唤醒、被调度(如果它们被从一个原子上下文调用, 这就不会发生).*/
--------------------------------------------------------------------------------
wake_up_nr(wait_queue_head_t *queue, int nr); wake_up_interruptible_nr(wait_queue_head_t *queue, int nr); /*这些函数类似 wake_up, 除了它们能够唤醒多达 nr 个独占等待者, 而不只是一个. 注意传递 0 被解释为请求所有的互斥等待者都被唤醒*/
--------------------------------------------------------------------------------
wake_up_all(wait_queue_head_t *queue); wake_up_interruptible_all(wait_queue_head_t *queue); /*这种 wake_up 唤醒所有的进程, 不管它们是否进行独占等待(可中断的类型仍然跳过在做不可中断等待的进程)*/
--------------------------------------------------------------------------------
wake_up_interruptible_sync(wait_queue_head_t *queue); /*一个被唤醒的进程可能抢占当前进程, 并且在 wake_up 返回之前被调度到处理器。 但是, 如果你需要不要被调度出处理器时,可以使用 wake_up_interruptible 的"同步"变体. 这个函数最常用在调用者首先要完成剩下的少量工作,且不希望被调度出处理器时。*/
4.2 wake_up 的实现细节
\kernel \sched.c
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int sync, void *key)
{
struct list_head *tmp, *next;
list_for_each_safe(tmp, next, &q->task_list) {
wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);
unsigned flags = curr->flags;
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
// 循环结束的条件包括list_for_each_safe本身、排他性(flags & WQ_FLAG_EXCLUSIVE)以及唤醒非0个进程,!--nr_exclusive非常巧妙,当传入0时,!--nr_exclusive的结果总是0,if条件不可能成立,就无法break,即表示唤醒所有的进程。对于非WQ_FLAG_EXCLUSIVE进程,由于(flags & WQ_FLAG_EXCLUSIVE)为0后,就不计算!--nr_exclusive,因此这个过程可以唤醒所有的非WQ_FLAG_EXCLUSIVE进程。但遇到WQ_FLAG_EXCLUSEVE之后的任意进程无法唤醒。
最终哪个进程运行是由schedule决定的。
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*/
void fastcall __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
//通用的wakeup,不可重入的,需要__wake_up对之进行封装
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(__wake_up);
根据int nr_exclusive值唤醒对应的进程,只是更改了进程的状态,具体何时运行由schedule决定。
并没有将唤醒的进程从等待队列中删除,只有当schedule获得cpu时才从等待队列中删除。
\include\linux\wait.h
void FASTCALL(__wake_up(wait_queue_head_t *q, unsigned int mode, int nr, void *key));
extern void FASTCALL(__wake_up_locked(wait_queue_head_t *q, unsigned int mode));
extern void FASTCALL(__wake_up_sync(wait_queue_head_t *q, unsigned int mode, int nr));
#define wake_up(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
各种wakeup通过宏定义的形式本质上就是一个函数__wake_up,但对外的接口不一样,这样对外的意义明确,相当于采用了默认参数,而不是在各个wakeup内部调用函数,省掉了函数调用的开销。在实现代码复用的同时保证了对外的明确接口,值得借鉴。
#define wake_up_locked(x) __wake_up_locked((x), TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)
#define wake_up_interruptible_sync(x) __wake_up_sync((x),TASK_INTERRUPTIBLE, 1)
5 独占等待和高级休眠
5.1 独占等待
当一个进程调用 wake_up 在等待队列上,所有的在这个队列上等待的进程被置为可运行的。 这在许多情况下是正确的做法。但有时,可能只有一个被唤醒的进程将成功获得需要的资源,而其余的将再次休眠。这时如果等待队列中的进程数目大,这可能严重降低系统性能。为此,内核开发者增加了一个“独占等待”选项。它与一个正常的睡眠有 2 个重要的不同:
² 当等待队列入口设置了 WQ_FLAG_EXCLUSEVE 标志,它被添加到等待队列的尾部;否则,添加到头部。因为唤醒一个WQ_FLAG_EXCLUSEVE标志的进程后就不再唤醒其他任意类型的进程。添加在尾部可以保证FIFO。
² 当 wake_up 被在一个等待队列上调用, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止唤醒。但内核仍然会唤醒所有的非独占等待进程,因为所有的非WQ_FLAG_EXCLUSIVE进程在队头。
采用独占等待要满足以下条件:
² 希望对资源进行有效竞争;
² 当资源可用时,唤醒一个进程就足够来完全消耗资源;
² 所有使用该资源的进程都应采用统一的独占等待规则。
使一个进程进入独占等待,可调用:
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
注意:无法使用通用的wait_event 和它的变体宏函数来进行独占等待。
此时需要使用休眠的高级特性,利用等待队列的prepare_to_wait_exclusive和finish_wait接口函数手动编写相关代码,
5.2 高级休眠的基本步骤:
(1)分配和初始化一个 wait_queue_t 结构, 随后将其添加到正确的等待队列。
(2)设置进程状态,标记为休眠。TASK_RUNNING 意思是进程能够运行。有 2 个状态指示一个进程是在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE。2.6 内核的驱动代码通常不需要直接操作进程状态。但如果需要这样做使用的代码是:
void set_current_state(int new_state);
在老的代码中, 你常常见到如此的东西:current->state = TASK_INTERRUPTIBLE; 但是象这样直接改变 current 是不推荐的,当数据结构改变时这样的代码将会失效。通过改变 current 状态,只改变了调度器对待进程的方式,但进程还未让出处理器。
(3)最后一步是放弃处理器。 但必须先检查进入休眠的条件。如果不做检查会引入竞态: 如果决定休眠后,在做上述准备工作到真正调用schedule时,若等待的条件变为真,不对条件重新进行判断,则你可能错过唤醒且长时间休眠。因此典型的代码下:
if (!condition) schedule();
在调用schedule前,应对条件再次进行检查。
(4)更改进程状态并将进程从等待队列中删除。
如果代码是从 schedule 返回,则进程肯定处于TASK_RUNNING 状态。 如果不需睡眠而跳过对 schedule 的调用,必须将任务状态重置为 TASK_RUNNING。无论是否调用过schedule,都需要从等待队列中去除这个进程,否则它可能被多次唤醒。
5.3 手工休眠的具体函数执行流
特殊睡眠要求程序员手动处理所有上面的步骤. 它是一个繁琐的过程, 包含相当多的易出错的样板式的代码. 程序员如果愿意还是可能用那种方式手动睡眠。
(1)创建和初始化一个等待队列。常由宏定义完成: DEFINE_WAIT(my_wait); /*name 是等待队列入口项的名字. 也可以用2步来做:*/ wait_queue_t my_wait; init_wait(&my_wait); /*常用的做法是放一个 DEFINE_WAIT 在循环的顶部,来实现休眠。*/
(2)添加等待队列入口到队列,并设置进程状态: void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); /*queue 和 wait 分别地是等待队列头和进程入口。state 是进程的新状态:TASK_INTERRUPTIBLE(可中断休眠,推荐)或TASK_UNINTERRUPTIBLE(不可中断休眠,不推荐)。*/ prepare_to_wait_exclusive
(3)在检查确认仍然需要休眠之后调用 schedule
schedule();
(4)schedule 返回,重新判断等待条件,若为真则退出,否则继续schedule
(5)条件满足退出后,确保状态为running,同时将进程从等待队列中删除。
void finish_wait(wait_queue_head_t *queue, wait_queue_t *wait);
5.4 wait_event_interruptible_exclusive
为了避免手工编写上述复杂代码,内核提供了最常见的interruptible类型排他性等待函数。而对于非排他性的可以直接利用等待事件event系列函数。
#define __wait_event_interruptible_exclusive(wq, condition, ret) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait_exclusive(&wq, &__wait, \
TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if (!signal_pending(current)) { \
schedule(); \
continue; \
} \
ret = -ERESTARTSYS; \
break; \
} \
finish_wait(&wq, &__wait); \
} while (0)
#define wait_event_interruptible_exclusive(wq, condition) \
({ \
int __ret = 0; \
if (!(condition)) \
__wait_event_interruptible_exclusive(wq, condition, __ret);\
__ret; \
})
6 Completion
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码必须包含。使用的代码如下:
DECLARE_COMPLETION(my_completion);/* 创建completion(声明+初始化) */ struct completion my_completion;/* 动态声明completion 结构体*/ static inline void init_completion(&my_completion);/*动态初始化completion*/ void wait_for_completion(struct completion *c);/* 等待completion */ void complete(struct completion *c);/*唤醒一个等待completion的线程*/ void complete_all(struct completion *c);/*唤醒所有等待completion的线程*/ /*如果未使用completion_all,completion可重复使用;否则必须使用以下函数重新初始化completion*/ INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/
completion的典型应用是模块退出时的内核线程终止。在这种模式,某些驱动程序的内部工作有一个内核线程在while(1)循环中完成。当内核准备清除该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:
void complete_and_exit(struct completion *c, long retval);