linux互斥同步之等待队列与Completion

等待队列并不是互斥机制,之所以放在这里,是因为等待队列是一些内核设施的实现机制,下面要讲的完成接口completion就是利用工作队列实现的。

1.等待队列

等待队列本质上是双向链表,由等待队列头和队列节点构成,当运行的进程要获得某一资源而暂不可得时,进程有时候需要等待,此时它可以进入睡眠状态,内核为此生成一个新的等待队列节点将睡眠的进程挂载到等待队列中。

1.1队列头队列节点

等待队列头定义:

struct __wait_queue_head {
    
    
	spinlock_t		lock;
	struct list_head	task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
  • lock:等待队列自旋锁,用做等待队列被并发访问时的互斥机制。
  • task_list:双向链表结构体,用来将等待队列构成链表

如果程序需要定义等待队列头,有两种方式:

  • 通过DECLARE_WAIT_QUEUE_HEAD静态定义

    #define __WAIT_QUEUE_HEAD_INITIALIZER(name) {				\
    	.lock		= __SPIN_LOCK_UNLOCKED(name.lock),		\
    	.task_list	= { &(name).task_list, &(name).task_list } }
    
    #define DECLARE_WAIT_QUEUE_HEAD(name) \
    	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
    
  • 通过init_wait_queue_head宏在程序运行期间初始

    #define init_waitqueue_head(q)				\
    	do {						\
    		static struct lock_class_key __key;	\
    							\
    		__init_waitqueue_head((q), #q, &__key);	\
    	} while (0)
    	
    void __init_waitqueue_head(wait_queue_head_t *q, const char *name, struct lock_class_key *key)
    {
          
          
    	spin_lock_init(&q->lock);
    	lockdep_set_class_and_name(&q->lock, key, name);
    	INIT_LIST_HEAD(&q->task_list);
    }
    

等待队列节点定义:

typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);

/* __wait_queue::flags */
#define WQ_FLAG_EXCLUSIVE	0x01
#define WQ_FLAG_WOKEN		0x02

struct __wait_queue {
    
    
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	task_list;
};
  • flags:唤醒等待队列上进程时,该标志会影响唤醒的操作模式,如上面的WQ_FLAG_EXCLUSIVE如果等待节点设置该标志位,表明睡眠在其上的进程在被唤醒时具有排他性
  • private:等待队列的私有数据,实际使用中用来指向睡眠在该节点上的进程的task_struct结构
  • func:当该节点睡眠进程需要唤醒时执行的唤醒函数。
  • task_list:用来将独立的等待队列节点链接起来形成链表

程序可以通过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)

2.等待队列应用

等待队列常用的模式便是实现进程的睡眠等待,当某一进程在运行过程中需要的资源暂时无法获得时,进程将进入睡眠状态以让出处理器资源给其他进程。进程进入睡眠状态,意味着进程将从调度器的运行队列中移除,此时进程被挂载到某一等待队列的节点中,为了实现进程的睡眠机制,系统会产生一个新的等待队列节点,然后将进程的task_struct对象放到等待队列节点对象的private成员中。下面看下等待队列在completion中的应用。

2.1 completion定义

完成接口在内核的定义如下:

struct completion {
    
    
	unsigned int done;
	wait_queue_head_t wait;
};
  • done:表示当前completion的状态
  • wait:等待队列,用来管理当前等待在该completion上的所有进程

静态定义completion,可以用INIT_COMPLETION宏:

#define COMPLETION_INITIALIZER_ONSTACK(work) \
	({ init_completion(&work); work; })


#define DECLARE_COMPLETION(work) \
	struct completion work = COMPLETION_INITIALIZER(work)

动态初始化,可以使用init_completion:

static inline void init_completion(struct completion *x)
{
    
    
	x->done = 0;
	init_waitqueue_head(&x->wait);
}
2.2完成者与等待者

完成接口 completion对执行路径的同步可以通过等待者与完成者模型来表述。对于等待者的行为。内核定义的典型函数是wait_for_completion:

void __sched wait_for_completion(struct completion *x)
{
    
    
	wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}

wait_for_completion内调用wait_for_common来使当前进程以task_uninterruptible睡眠在completion x上的wait队列中。wait_for_common内核调用do_wait_for_common来做这件事:

static inline long __sched
do_wait_for_common(struct completion *x,
		   long (*action)(long), long timeout, int state)
{
    
    
	if (!x->done) {
    
    
		DECLARE_WAITQUEUE(wait, current);

		__add_wait_queue_tail_exclusive(&x->wait, &wait);
		do {
    
    
			if (signal_pending_state(state, current)) {
    
    
				timeout = -ERESTARTSYS;
				break;
			}
			__set_current_state(state);
			spin_unlock_irq(&x->wait.lock);
			timeout = action(timeout);
			spin_lock_irq(&x->wait.lock);
		} while (!x->done && timeout);
		__remove_wait_queue(&x->wait, &wait);
		if (!x->done)
			return timeout;
	}
	x->done--;
	return timeout ?: 1;
}

等待者首先检查completion中的done成员,表示当前在completion上的完成者数量,如果没有完成者,那么等待者将进入睡眠队列等待。这种睡眠是不可中断的。DECLARE_WAITQUEUE定义初始化了一个等待节点wait,代表当前进程的current变量将会记录到wait的private变量,wait中的func函数指针指向default_wake_function,当wait上的进程被唤醒时将调用该函数。进程需要睡眠时,通过__add_wait_queue_tail_exclusive把wait节点加入到completion管理的等待队列的尾部。

若干时间之后,进程因某种原因被唤醒,表现为从shedule_timeout函数返回,它将检查done成员和timeout变量以决定后续的行为。timeout>0表示进程没有超时,x->done=0表示completion还没有完成者,此时当前进程如果没有信号需要处理,将继续睡眠。

如果进程进程睡眠超时,将返回timeout的值。如果没有超时且有完成者在completion上出现,那么进程将离开睡眠队列,再将完成者数量减1之后,等待者结束等待状态返回。

对于完成者的行为,内核为其定义的函数是complete和complete_all,前者只唤醒一个等待者,后者将唤醒所有的等待者。

void complete(struct completion *x)
{
    
    
	unsigned long flags;

	spin_lock_irqsave(&x->wait.lock, flags);
	x->done++;
	__wake_up_locked(&x->wait, TASK_NORMAL, 1);
	spin_unlock_irqrestore(&x->wait.lock, flags);
}
void __wake_up_locked(wait_queue_head_t *q, unsigned int mode, int nr)
{
    
    
	__wake_up_common(q, mode, nr, 0, NULL);
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
    
    
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
    
    
		unsigned flags = curr->flags;

		if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;
	}
}

函数先将完成者的数据加1,然后调用__wake_up_common函数执行唤醒等待者的操作,注意这里的第三和第四参数,分别表示排他性唤醒的个数和唤醒标志。

函数遍历当前completion所管理的等待队列的每一个节点,此时nr_exclusive=1,flags中包含有WQ_FLAG_EXCLUSIVE标志,意味着本次唤醒只会唤醒一个等待者。func指向default_wake_function,用来做实际的唤醒工作。

相对于complete一次只唤醒一个等待者,complete_all用来唤醒completion等待队列上的所有等待者进程:

void complete_all(struct completion *x)
{
    
    
	unsigned long flags;

	spin_lock_irqsave(&x->wait.lock, flags);
	x->done += UINT_MAX/2;
	__wake_up_locked(&x->wait, TASK_NORMAL, 0);
	spin_unlock_irqrestore(&x->wait.lock, flags);
}

注意complete_all在这里假设完成者的最大数量是(~0)/2,这是很大的值,现实系统中很少有等待者进程数量会到达该值,因此complete_all之后completion中的done值将失去其本来的意义,如果后面要继续该completion,应该调用前面提过的INIT_COMPLETION宏。

猜你喜欢

转载自blog.csdn.net/wll1228/article/details/108086938