目录
一、概述
在OS中,一种同步的手段是wait/signal,用于请求资源的延迟可能较长的情形
二、wait & signal基本原理
一个wait的实现应该像下面这样,依赖于操作系统的调度:
sleeping_task = current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
func1();
先保存current,然后设置当前进程的状态为TASK_INTERRUPTIBLE,这样在schedule的时候该进程就会从运行队列中移出。
signal的实现就是将进程的状态重新置为TASK_RUNNING
一般的,在实际应用中,进程都是等待资源条件不满足时进行睡眠。这样在实现时需要注意几种情况:
2.1 Lost Wake-Up Problem
错过唤醒的问题,如下代码:
Process A:
1 spin_lock(&list_lock);
2 if(list_empty(&list_head)) {
3 spin_unlock(&list_lock);
4 set_current_state(TASK_INTERRUPTIBLE);
5 schedule();
6 spin_lock(&list_lock);
7 }
8
9 /* Rest of the code ... */
10 spin_unlock(&list_lock);
Process B:
100 spin_lock(&list_lock);
101 list_add_tail(&list_head, new_node);
102 spin_unlock(&list_lock);
103 wake_up_process(processa_task);
如上图所示,先不看问题,先做以下几点说明:
- 如果条件判断涉及到共享资源,要加锁避免race condition,如上图 1 spin_lock
- 如果条件不满足,在主动让出cpu之前,要先释放锁,在这种条件下,持有锁睡眠会导致死锁。
下面说明一下代码存在的问题,我们知道,在释放锁的那一刻,一定要分析之后产生的窗口,看看有没有造成问题。如在3-4之间切换出去了,完全执行Process B,将进程状态变成TASK_RUNNING,此时切换回Process A,注意,此时A认为conditon还没有满足,但实际上B执行过后条件已经满足了!如果A仍将进程状态设置为TASK_INTERRUPTIBLE,调用schedule,就会出现一直睡眠无法被唤醒的情况!
- TIPS:在if/while语句块内,出现窗口切换,很容易出现条件与假设不一致的情况,这时一定要重点分析。
上述问题可以通过如下的代码片段解决:
Process A:
1 set_current_state(TASK_INTERRUPTIBLE);
2 spin_lock(&list_lock);
3 if(list_empty(&list_head)) {
4 spin_unlock(&list_lock);
5 schedule();
6 spin_lock(&list_lock);
7 }
8 set_current_state(TASK_RUNNING);
9
10 /* Rest of the code ... */
11 spin_unlock(&list_lock);
可以看到,将set_current_state(TASK_INTERRUPTIBLE) 提前了
下面说明一下为什么这样的改动可以避免漏掉wake-up的问题,假设在4-5出现了调度,执行Process B调用wake_up,注意此时进程状态是TASK_INTERRUPTIBLE,waku-up在检测到进程是TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE,会将其状态改变成TASK_RUNNING. B执行完毕切换回A,此时进程状态为TASK_RUNNING,这时候调用schedule(),schedule检测到进程状态为TASK_RUNNING会直接返回而不是陷入睡眠。这样就避免了上述问题。
三、linux中的实现
3.1 等待队列
等待队列是内核的数据结构,当内核请求的资源需要等待时,需要进入睡眠状态,此时进程通过进入等待队列保存,实现上内核提供了一个等待队列头和等待队列节点,二者构成一个链表。
先看一下等待队列头:
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
队列头初始化可以是静态的或动态的
[include/linux/wait.h]
- DECLARE_WAIT_QUEUE_HEAD
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.head = { &(name).head, &(name).head } }
#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
- init_waitqueue_head
接下来看一下等待队列节点:
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
typedef struct wait_queue_entry wait_queue_entry_t;
其初始化也分为两种:
- DECLARE_WAITQUEUE
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.entry = { NULL, NULL } }
#define DECLARE_WAITQUEUE(name, tsk) \
struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)
- init_waitqueue_entry
static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p)
{
wq_entry->flags = 0;
wq_entry->private = p;
wq_entry->func = default_wake_function;
}
3.2 wait_event_interruptible
#define wait_event_interruptible(wq_head, condition) \
({ \
int __ret = 0; \
might_sleep(); \
if (!(condition)) \
__ret = __wait_event_interruptible(wq_head, condition); \
__ret; \
})
#define __wait_event_interruptible(wq_head, condition) \
___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
schedule())
重点看一下__wait_event
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
for (;;) { \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
if (condition) \
break; \
\
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
cmd; \
} \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
上述代码实际上就是执行以下逻辑
DEFINE_WAIT(wait);
while (1) {
prepare_to_wait(&queue, &wait, state);
if (condition)
break;
schedule();
}
finish_wait(&queue, &wait);
prepare_to_wait
void prepare_to_wait(struct wait_queue_head *wq_head,
struct wait_queue_entry *wq_entry, int state)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
if (list_empty(&wq_entry->entry))
__add_wait_queue(wq_head, wq_entry);
set_current_state(state);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
从上面我们看到,conditon在某些情况下是共享资源,是要加锁保护的,但是一般内核解锁的位置相对于我们开始的描述提前了,向下面这样:
- lock
- while (!conditon) {
- unlock
- DEFINE_WAIT(wait);
- while (1) {
- prepare_to_wait(&queue, &wait, state);
- if (condition)
- break;
- schedule();
- }
- finish_wait(&queue, &wait);
- lock
- }
- statements
- unlock
- 当然如果condition不是共享资源,或者判断condition自带保护,可以不用锁。
- 蓝色部分是手工休眠,内核实现对应wait_event
- 对应标准教材中的wait(enqueue,unlock,sleep,lock)顺序不同,相当于3,4互换,这样是不是有问题呢?而且我们在并发一节中说过,如果在调度之前解锁,产生一个窗口(8,9)条件成立情况下schedule就会产生丢失event情况,可能导致无法唤醒。
可以看到内核一般的使用和上面有所不同——其解决丢失唤醒事件的方法并不是将set_current_state 提前,而是在其中加了一个if条件对从condition的判断, 在此就分析一下linux内核这样实现为何能不丢失唤醒事件,我用下面的表格进行分析,在解锁之后,调度之前产生的每个窗口造成对condition的影响都列在下表,其中3-5意味着在上图3和4之间产生一个窗口,列举了可能的切换之后conditon的变化。记住,我们进入while时条件是不满足的。
3-6 | 6-7 | 7-8 | 8-9 | 结论 |
真 | 真 | 真 | 真(x) | 不调度,我们看到,只要在6-7之间条件成立, 重新获取锁尝试,这是没有问题的。这同时也说明 了,在linux这种写法下,解锁操作在入队prepare_to_wait操作之前是没有问题的。实际上因为linux上加入等待队列并不意味着发生了调度。
|
真 | 真 | 真 | 假(x) | |
真 | 真 | 假 | 真(x) | |
真 | 真 | 假 | 假(x) | |
真 | 假 | 真 | 真 | 这个说明参考了LDD3,这种情况会调用schedule函数,却不会发生调度,因为此时进程被唤醒时检测到其是TASK_INTERRUPTIBLE状态,唤醒进程会将其状态重新置为RUNNING,这样schedule就直接返回了。 |
真 | 假 | 真 | 假 | |
真 | 假 | 假 | 真 | 这个是我们预期的操作,在解锁和调度之前条件都不成立,进程正常睡眠即可,当进程被唤醒条件可能是真也可能是假,即不假设任何条件,此时老老实实获取锁在去进行尝试即可。 |
真 | 假 | 假 | 假 |
3.3 wake_up_interruptible
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
留待以后分析。
3.3.1 Thundering Herd Problem
惊群现象,wake_up_all